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

  • Provide agents agents.provideMediumAdds agents to the app.Provide the Claude Code agent and pass its tool calls to plugin tools
  • Run named programs processMediumStarts the listed programs.Run the Claude Code CLI (sessions, sign-in, `claude update`), and ask the npm installation that owns it about a newer versionPrograms: claudenpm
  • Read files fs.readMediumReads files in the listed places.Read Claude Code's sessions, settings and skills (in ~/.claude, other accounts' ~/.claude-* folders, the shared skills and the folder you choose in Settings), the project's .claude folder, the administrator's skill policy, and once, what the previous provider keptPlaces: ~/.claude/**~/.claude*/**~/.agents/skills/**the open workspaceits own data folder/Library/Application Support/ClaudeCode/managed-settings.json/etc/claude-code/managed-settings.json${settings.configDir}
  • Environment variables envMediumReads the listed environment variables.Find Claude Code's configuration directory, and the launch settings you set in CLAUDE_* variables (extra arguments, MCP servers, setting sources, appended system prompt, extra folders)Variables: HOMECLAUDE_CONFIG_DIRCLAUDE_EXTRA_ARGSCLAUDE_MCP_CONFIGCLAUDE_STRICT_MCP_CONFIGCLAUDE_SETTING_SOURCESCLAUDE_APPEND_SYSTEM_PROMPTCLAUDE_ADD_DIRS

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

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <3128.7 KB4 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.