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
legacy.ts4.5 KB
// What the Rust plugin kept in `<data dir>/agents/claude/`: the accounts
// (`instances.json`) and, for each host session, the id the CLI files it
// under and the records its prompts became (`sessions/<id>.json`).
//
// The first time this plugin loads in the Rust plugin's place, the host
// moves that folder into the plugin's data folder as `legacy/`
// (`api.paths.data`, read under the `data` scope of `fs.read`). The plugin
// reads it once into its storage: the accounts into `instances` (entries
// whose id storage lacks), each session into `session:<id>` (unless
// storage has one). The storage key `legacy` then says it was read, so the
// old files are never read again.
import * as Effect from "effect/Effect";
import * as Result from "effect/Result";
import { Fs, Host } from "convergence/effect";
import type { Api } from "convergence";
import { objectOf } from "./wire.ts";
import { join, mapLimit } from "./files.ts";
const MARK = "legacy";
/// The legacy folder, or `null` when the runtime names no data folder.
export function legacyDir(api: Api) {
const data = api.paths?.data;
return typeof data === "string" && data ? join(data, "legacy") : null;
}
const idOf = (entry: unknown) => {
const id = objectOf(entry).id;
return typeof id === "string" ? id.trim() : "";
};
/// The stored accounts with the legacy ones whose id they lack after them;
/// `null` when that changes nothing.
export function mergeInstances(stored: unknown, legacy: unknown) {
if (!Array.isArray(legacy)) return null;
const current = Array.isArray(stored) ? stored : [];
const ids = new Set(current.map(idOf));
const added = legacy.filter((entry) => {
const id = idOf(entry);
if (!entry || typeof entry !== "object" || ids.has(id)) return false;
ids.add(id);
return true;
});
return added.length ? [...current, ...added] : null;
}
/// The legacy file's JSON value; `undefined` when it is not there or is
/// not JSON. A read the plugin may not make throws.
const readJson = Effect.fn("Claude.legacy.readJson")(function* (path: string) {
const result = yield* Effect.result((yield* Fs).read(path));
if (Result.isFailure(result)) {
if (result.failure._tag === "PermissionNotGranted" || result.failure._tag === "NeedsReview")
return yield* result.failure;
return undefined;
}
try {
const value: unknown = JSON.parse(result.success);
return value;
} catch {
console.warn(`claude: ${path} is not JSON; it is left out`);
return undefined;
}
});
export const adoptLegacy = Effect.fn("Claude.adoptLegacy")(
function* (dir: string | null) {
if (!dir) return false;
const host = yield* Host;
const fs = yield* Fs;
if ((yield* host.call("host/storage.get", { key: MARK })).value) return false;
let changed = false;
const legacy = yield* readJson(join(dir, "instances.json"));
if (legacy !== undefined) {
const merged = mergeInstances((yield* host.call("host/storage.get", { key: "instances" })).value ?? null, legacy);
if (merged) {
yield* host.call("host/storage.set", { key: "instances", value: merged });
changed = true;
}
}
const listing = yield* Effect.result(fs.list(join(dir, "sessions")));
if (
Result.isFailure(listing) &&
(listing.failure._tag === "PermissionNotGranted" || listing.failure._tag === "NeedsReview")
)
return yield* listing.failure;
const files = (Result.isSuccess(listing) ? listing.success : []).filter(
(entry) => entry.kind === "file" && entry.name.endsWith(".json"),
);
let sessions = 0;
yield* mapLimit(
files,
8,
Effect.fn(function* (entry) {
const id = entry.name.slice(0, -".json".length);
const state = yield* readJson(join(dir, "sessions", entry.name));
if (!id || !state || typeof state !== "object") return;
const key = `session:${id}`;
if ((yield* host.call("host/storage.get", { key })).value) return;
yield* host.call("host/storage.set", { key, value: state });
sessions += 1;
}),
);
yield* host.call("host/storage.set", { key: MARK, value: { at: Date.now(), sessions } });
if (changed || sessions)
console.info(`claude: took the Rust plugin's ${changed ? "accounts and " : ""}${sessions} session states`);
return changed;
},
Effect.catchTag(["HostCallFailed", "PermissionNotGranted", "NeedsReview"], (error) =>
Effect.sync(() => {
console.warn(`claude: the Rust plugin's files could not be read: ${error.message}`);
return false;
}),
),
);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.