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.test.ts5.1 KB
// Legacy account and session migration through the scoped host and file services.
import * as z from "zod";
import assert from "node:assert/strict";
import { objectOf } from "./wire.ts";
import type { Api } from "convergence";
import { test, run as runEffect, activateAccounts as activate } from "./testing.ts";
import { mkdtemp, mkdir, writeFile, symlink } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { adoptLegacy as adoptLegacyEffect, legacyDir, mergeInstances } from "./legacy.ts";
import { SessionStore } from "./state.ts";
import { diskFs, fakeApi, memoryStorage, settle } from "../sdk/testing.ts";
/// A data folder with what the Rust plugin kept, moved there by the host.
async function dataFolder() {
const data = await mkdtemp(join(tmpdir(), "claude-legacy-"));
await mkdir(join(data, "legacy", "sessions"), { recursive: true });
await writeFile(
join(data, "legacy", "instances.json"),
JSON.stringify([
{ id: "work", name: "Claude Work", configDir: "~/.claude_work" },
{ id: "home", configDir: "~/.claude_home" },
]),
);
await writeFile(join(data, "legacy", "sessions", "s1.json"), JSON.stringify({ cli: "cli-1", sent: { "item-1": 0 } }));
await writeFile(join(data, "legacy", "sessions", "s2.json"), JSON.stringify({ sent: { "item-9": 4 } }));
await writeFile(join(data, "legacy", "sessions", "broken.json"), "{ not json");
return data;
}
function apiWith(data: string, storage: ReturnType<typeof memoryStorage>, fs = diskFs()) {
const api = fakeApi({ onHost: storage, fs });
api.paths = { data };
return api;
}
test("the stored accounts keep theirs and gain the legacy ones they lack", () => {
assert.equal(mergeInstances(null, null), null);
assert.deepEqual(mergeInstances(null, [{ id: "a" }]), [{ id: "a" }]);
assert.deepEqual(
mergeInstances([{ id: "a", name: "Mine" }], [{ id: "a", name: "Old" }, { id: "b" }, { id: "b" }, "x"]),
[{ id: "a", name: "Mine" }, { id: "b" }],
);
assert.equal(mergeInstances([{ id: "a" }], [{ id: " a " }]), null, "nothing new");
});
test("the Rust plugin's accounts and session states go into storage once", async () => {
const data = await dataFolder();
const storage = memoryStorage({ "session:s2": { cli: "newer", sent: {} } });
const api = apiWith(data, storage);
assert.equal(legacyDir(api), `${data}/legacy`);
assert.equal(await adoptLegacy(api), true);
assert.deepEqual(
z
.array(z.looseObject({ id: z.string() }))
.parse(storage.values.get("instances"))
.map((entry) => entry.id),
["work", "home"],
);
assert.deepEqual(storage.values.get("session:s1"), { cli: "cli-1", sent: { "item-1": 0 } });
assert.deepEqual(storage.values.get("session:s2"), { cli: "newer", sent: {} }, "what storage has wins");
assert.equal(storage.values.has("session:broken"), false);
assert.equal(objectOf(storage.values.get("legacy")).sessions, 1);
// Read once: a later change to the old files changes nothing.
await writeFile(join(data, "legacy", "instances.json"), JSON.stringify([{ id: "late" }]));
assert.equal(await adoptLegacy(api), false);
assert.deepEqual(
z
.array(z.looseObject({ id: z.string() }))
.parse(storage.values.get("instances"))
.map((entry) => entry.id),
["work", "home"],
);
});
test("no data folder, no legacy folder, or no grant", async () => {
const storage = memoryStorage();
assert.equal(await adoptLegacy(fakeApi({ onHost: storage })), false, "a runtime without paths");
assert.equal(storage.values.size, 0);
const empty = await mkdtemp(join(tmpdir(), "claude-legacy-empty-"));
assert.equal(await adoptLegacy(apiWith(empty, storage)), false);
assert.ok(storage.values.get("legacy"), "nothing to take is marked too");
const data = await dataFolder();
const refused = memoryStorage();
assert.equal(await adoptLegacy(apiWith(data, refused, diskFs({ allow: () => false }))), false);
assert.equal(refused.values.has("legacy"), false, "a refused read is tried again at the next load");
});
test("activate registers the legacy accounts, and session states wait for them", async () => {
const data = await dataFolder();
const storage = memoryStorage();
const api = apiWith(data, storage);
activate(api);
assert.deepEqual(
api.registered.map((def) => def.id),
["claude"],
);
await settle(20);
assert.deepEqual(
api.registered.map((def) => def.id),
["claude", "claude:work", "claude:home"],
);
assert.deepEqual(await runEffect(new SessionStore().load("s1")), { cli: "cli-1", sent: { "item-1": 0 } });
});
test("a link in the legacy folder is not followed", async () => {
const data = await dataFolder();
const outside = await mkdtemp(join(tmpdir(), "claude-legacy-outside-"));
await writeFile(join(outside, "s9.json"), JSON.stringify({ cli: "x", sent: {} }));
await symlink(join(outside, "s9.json"), join(data, "legacy", "sessions", "s9.json"));
const storage = memoryStorage();
await adoptLegacy(apiWith(data, storage));
assert.equal(storage.values.has("session:s9"), false, "only files are read, not links");
});
const adoptLegacy = (api: Api) => runEffect(adoptLegacyEffect(legacyDir(api)), api);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.