Official
claude
Claude Code agent provider: runs the Claude Code CLI headless for each account.
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/claude@0.2.0
Permissions in 0.2.0
Files
state.ts3.5 KB
// What the plugin remembers about one host session beyond what the CLI's
// own transcript holds (the Rust `SessionState`): the id the CLI files it
// under, and the user record each of the host's prompts became.
//
// A rewind is resolved with the map, and it lives in this plugin alone:
// the CLI's file does not know the host's ids. The Rust plugin kept it in
// `<data dir>/agents/claude/sessions/<id>.json`; this one keeps it in its
// plugin storage under `session:<id>`, so a restart of the app, or of the
// session after an earlier rewind, can still rewind to a message sent
// before it. The old files are read into it once (`legacy.ts`); `ready`
// is that read, which every load waits for.
import * as Context from "effect/Context";
import * as Layer from "effect/Layer";
import * as Effect from "effect/Effect";
import { Plugin, Host } from "convergence/effect";
import type { PluginServices } from "convergence/effect";
import { adoptLegacy, legacyDir } from "./legacy.ts";
import { objectOf } from "./wire.ts";
export interface SessionState {
cli: string | null;
sent: Record<string, number>;
}
const PREFIX = "session:";
export function emptyState(): SessionState {
return { cli: null, sent: {} };
}
function parse(raw: unknown) {
const value = objectOf(raw);
const state = emptyState();
if (!value || typeof value !== "object") return state;
if (typeof value.cli === "string" && value.cli) state.cli = value.cli;
if (value.sent && typeof value.sent === "object") {
for (const [item, index] of Object.entries(value.sent))
if (typeof index === "number" && Number.isInteger(index) && index >= 0) state.sent[item] = index;
}
return state;
}
/// The session states in the plugin's storage.
export class SessionStore {
constructor(readonlyReady: Effect.Effect<unknown, never, PluginServices> = Effect.void) {
this.ready = readonlyReady;
}
ready: Effect.Effect<unknown, never, PluginServices>;
load = Effect.fn("Claude.state.load")(function* (this: SessionStore, id: string) {
yield* this.ready;
const host = yield* Host;
return yield* host.call("host/storage.get", { key: PREFIX + id }).pipe(
Effect.map((result) => parse(result.value)),
Effect.catchTag(["HostCallFailed", "PermissionNotGranted", "NeedsReview"], (error) =>
Effect.sync(() => {
console.warn(`claude: the state of session ${id} could not be read: ${error.message}`);
return emptyState();
}),
),
);
});
save = Effect.fn("Claude.state.save")(function* (this: SessionStore, id: string, state: SessionState) {
const value: { sent: Record<string, number>; cli?: string } = { sent: state.sent };
if (state.cli) value.cli = state.cli;
yield* (yield* Host).call("host/storage.set", { key: PREFIX + id, value }).pipe(
Effect.catchTag(["HostCallFailed", "PermissionNotGranted", "NeedsReview"], (error) =>
Effect.sync(() => {
console.warn(`claude: the state of session ${id} could not be kept: ${error.message}`);
}),
),
);
});
cliIdOf = Effect.fn("Claude.state.cliIdOf")(function* (this: SessionStore, id: string) {
return (yield* this.load(id)).cli ?? id;
});
}
export class SessionStates extends Context.Service<SessionStates, SessionStore>()("claude/SessionStates") {
static readonly layer = Layer.effect(
SessionStates,
Effect.gen(function* () {
const plugin = yield* Plugin;
const legacy = yield* Effect.cached(adoptLegacy(legacyDir(plugin.api)));
yield* Effect.forkScoped(legacy);
return new SessionStore(legacy);
}),
);
}Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 128.7 KB | 4 permissions | Listed |
No comments yet.