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

history.test.ts14.9 KB
// Ported from the Rust plugin's history.rs and subagents.rs tests.
import * as wire from "./wire.ts";
import assert from "node:assert/strict";
import { itemAs, required, test, run as runEffect, files, tempDir } from "./testing.ts";
import type { TranscriptItem } from "./types.ts";
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import {
  encodeWorkspace,
  listSessions,
  readFile,
  readSession,
  rewindBefore,
  rewindTarget,
  userRecords,
} from "./history.ts";

function write(path: string, records: unknown[]) {
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(
    path,
    records.map((record) => (typeof record === "string" ? record : JSON.stringify(record))).join("\n"),
  );
}

/// The CLI's own records of a background task's end and of a stop are not
/// the user's words: no prompt bubble for either.
test("the CLI's own records are not prompts", async () => {
  const dir = tempDir();
  const path = join(dir, "s.jsonl");
  write(path, [
    { type: "user", uuid: "u1", message: { role: "user", content: [{ type: "text", text: "hello" }] } },
    {
      type: "user",
      uuid: "u2",
      origin: { kind: "task-notification" },
      message: { role: "user", content: "<task-notification>\n<task-id>a1</task-id>\n</task-notification>" },
    },
    {
      type: "user",
      uuid: "u3",
      message: { role: "user", content: [{ type: "text", text: "[Request interrupted by user for tool use]" }] },
    },
  ]);
  const items = await runEffect(readFile(files(), path));
  assert.equal(items.length, 2);
  assert.equal(items[0].role, "user");
  assert.deepEqual(items[1], { id: "u3", role: "notice", text: "Stopped." });
});

test("the workspace encoding matches the CLI", () => {
  assert.equal(encodeWorkspace("/private/tmp"), "-private-tmp");
  assert.equal(encodeWorkspace("/private/tmp/pr.obe_dir/x"), "-private-tmp-pr-obe-dir-x");
});

test("a transcript becomes items, and the sessions are listed", async () => {
  const home = tempDir();
  const workspace = "/w/project";
  write(join(home, "projects", encodeWorkspace(workspace), "s1.jsonl"), [
    '{"type":"ai-title","aiTitle":"Rename the widget","sessionId":"s1"}',
    '{"type":"user","uuid":"u1","timestamp":"2026-09-14T12:00:00.000Z","message":{"role":"user","content":[{"type":"text","text":"rename it"}]}}',
    '{"type":"assistant","uuid":"a1","timestamp":"2026-09-14T12:00:01.000Z","message":{"content":[{"type":"thinking","thinking":"plan"},{"type":"text","text":"on it"},{"type":"tool_use","id":"t1","name":"Bash","input":{"command":"ls"}}]}}',
    '{"type":"user","uuid":"u2","timestamp":"2026-09-14T12:00:02.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":"a.txt\\n"}]}}',
  ]);
  const sessions = await runEffect(listSessions(files(), home, workspace));
  assert.deepEqual(sessions, [
    {
      id: "s1",
      title: "Rename the widget",
      createdAt: "2026-09-14T12:00:00.000Z",
      updatedAt: "2026-09-14T12:00:02.000Z",
    },
  ]);

  const items = await runEffect(readSession(files(), home, workspace, "s1"));
  assert.equal(items[0].role, "user");
  assert.equal(items[0].blocks.length, 1);
  assert.deepEqual([items[1].role, itemAs(items[1], "reasoning").text], ["reasoning", "plan"]);
  assert.deepEqual([items[2].role, itemAs(items[2], "assistant").text], ["assistant", "on it"]);
  assert.equal(itemAs(items[3], "tool").call.status, "completed");
  assert.equal(itemAs(items[3], "tool").call.kind, "execute");
  assert.deepEqual(itemAs(items[3], "tool").call.content, [{ type: "terminal", command: "ls", output: "a.txt\n" }]);
  assert.equal(items.length, 4, "the tool result updates the call instead of adding an item");
  assert.deepEqual(await runEffect(listSessions(files(), home, "/w/none")), []);
});

/// The host names its own message ids, which never appear in the CLI's
/// file; the prompt is matched to its user record by position instead.
test("a prompt is found again by its position among user records", () => {
  const records = [
    { type: "user", uuid: "u1", parentUuid: null, message: { role: "user", content: [{ type: "text", text: "one" }] } },
    {
      type: "assistant",
      uuid: "a1",
      parentUuid: "u1",
      message: { role: "assistant", content: [{ type: "text", text: "ok" }] },
    },
    {
      type: "user",
      uuid: "side",
      isSidechain: true,
      parentUuid: "a1",
      message: { role: "user", content: [{ type: "text", text: "sub" }] },
    },
    { type: "user", uuid: "u2", parentUuid: "a1", message: { role: "user", content: [{ type: "text", text: "two" }] } },
  ];
  const users = userRecords(records);
  assert.equal(users.length, 2, "sidechain records are not the user's own messages");
  assert.deepEqual(rewindBefore(users[1]), { at: "a1" });
  // The first message has nothing before it: forgetting it means an empty
  // conversation, not an error.
  assert.deepEqual(rewindBefore(users[0]), { fresh: true });
});

/// `--resume-session-at` keeps the message it names, so rewinding to an
/// item means resuming at the item's parent.
test("rewinding resumes at the record before the item", async () => {
  const home = tempDir();
  const workspace = "/w/rewind";
  write(join(home, "projects", encodeWorkspace(workspace), "s3.jsonl"), [
    { type: "user", uuid: "u1", parentUuid: null, message: { role: "user", content: [{ type: "text", text: "one" }] } },
    { type: "assistant", uuid: "a1", parentUuid: "u1", message: { content: [{ type: "text", text: "done" }] } },
    { type: "user", uuid: "u2", parentUuid: "a1", message: { role: "user", content: [{ type: "text", text: "two" }] } },
  ]);
  assert.equal(await runEffect(rewindTarget(files(), home, workspace, "s3", "u2")), "a1");
  // An assistant item's id carries its block index.
  assert.equal(await runEffect(rewindTarget(files(), home, workspace, "s3", "a1:0")), "u1");
  // Nothing comes before the first message: refused, not half done.
  assert.equal(await runEffect(rewindTarget(files(), home, workspace, "s3", "u1")), null);
  assert.equal(await runEffect(rewindTarget(files(), home, workspace, "s3", "missing")), null);
});

test("the title falls back to the first prompt", async () => {
  const home = tempDir();
  const workspace = "/w/other";
  write(join(home, "projects", encodeWorkspace(workspace), "s2.jsonl"), [
    '{"type":"user","uuid":"u1","timestamp":"2026-09-14T12:00:00.000Z","message":{"role":"user","content":[{"type":"text","text":"fix the build\\nplease"}]}}',
  ]);
  assert.equal((await runEffect(listSessions(files(), home, workspace)))[0].title, "fix the build");
});

// --- stored subagents (subagents.rs) --------------------------------------------

const user = (uuid: string, parent: string | null, time: string, content: unknown) =>
  wire.record.parse({ type: "user", uuid, parentUuid: parent, timestamp: time, message: { role: "user", content } });
const assistant = (uuid: string, parent: string | null, time: string, content: unknown) =>
  wire.record.parse({
    type: "assistant",
    uuid,
    parentUuid: parent,
    timestamp: time,
    message: { model: "claude-haiku-4-5", content },
  });
const sidechain = (record: wire.Record, agent: string) => ({ ...record, isSidechain: true, agentId: agent });
const toolId = (item: TranscriptItem) => (item.role === "tool" ? item.call.id : null);

/// A redacted copy of the layout claude 2.1.280 writes: a foreground agent
/// that starts a nested one, and a background agent that reports through a
/// queued notification.
test("stored subagents come back as nested tasks", async () => {
  const home = tempDir();
  const main = join(home, "s1.jsonl");
  const dir = join(home, "s1", "subagents");
  write(main, [
    user("m1", null, "2026-09-22T10:00:00Z", "look around"),
    assistant("m2", "m1", "2026-09-22T10:00:01Z", [
      {
        type: "tool_use",
        id: "toolu_A",
        name: "Agent",
        input: { description: "Survey the parser", prompt: "Read the parser.", subagent_type: "Explore" },
      },
      {
        type: "tool_use",
        id: "toolu_C",
        name: "Agent",
        input: { prompt: "\nWatch the build\nand report", run_in_background: true },
      },
    ]),
    {
      type: "user",
      uuid: "m3",
      parentUuid: "m2",
      timestamp: "2026-09-22T10:00:02Z",
      toolUseResult: { isAsync: true, status: "async_launched", agentId: "ccc" },
      message: { role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_C", content: "Launched" }] },
    },
    {
      type: "user",
      uuid: "m4",
      parentUuid: "m3",
      timestamp: "2026-09-22T10:01:00Z",
      toolUseResult: {
        status: "completed",
        agentId: "aaa",
        totalTokens: 900,
        totalToolUseCount: 2,
        content: [{ type: "text", text: "The parser is fine." }],
      },
      message: {
        role: "user",
        content: [
          {
            type: "tool_result",
            tool_use_id: "toolu_A",
            content: [
              { type: "text", text: "The parser is fine." },
              { type: "text", text: "agentId: aaa" },
            ],
          },
        ],
      },
    },
    {
      type: "queue-operation",
      operation: "enqueue",
      timestamp: "2026-09-22T10:02:00Z",
      content:
        '<task-notification>\n<task-id>ccc</task-id>\n<status>completed</status>\n<summary>Agent "Watch" finished</summary>\n<result>Build is green &amp; fast.</result>\n</task-notification>',
    },
  ]);
  write(join(dir, "agent-aaa.jsonl"), [
    sidechain(user("a1", null, "2026-09-22T10:00:03Z", "Read the parser."), "aaa"),
    sidechain({ type: "attachment", uuid: "a1x", parentUuid: "a1", attachment: {} }, "aaa"),
    sidechain(
      assistant("a2", "a1x", "2026-09-22T10:00:04Z", [
        { type: "text", text: "Starting." },
        { type: "tool_use", id: "toolu_B", name: "Agent", input: { description: "Check tokens", prompt: "Check." } },
      ]),
      "aaa",
    ),
    // An abandoned branch the chain must skip.
    sidechain(assistant("a2dead", "a1", "2026-09-22T10:00:04Z", [{ type: "text", text: "dead" }]), "aaa"),
    sidechain(
      {
        type: "user",
        uuid: "a3",
        parentUuid: "a2",
        timestamp: "2026-09-22T10:00:30Z",
        toolUseResult: { status: "completed", agentId: "bbb", content: [{ type: "text", text: "Tokens ok." }] },
        message: { role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_B", content: "Tokens ok." }] },
      },
      "aaa",
    ),
    sidechain(assistant("a4", "a3", "2026-09-22T10:00:59Z", [{ type: "text", text: "The parser is fine." }]), "aaa"),
  ]);
  write(join(dir, "agent-aaa.meta.json"), [
    {
      agentType: "Explore",
      description: "Survey the parser",
      toolUseId: "toolu_A",
      spawnDepth: 1,
      requestShape: "foreground",
    },
  ]);
  const nested = join(dir, "workflows", "agent-bbb.jsonl");
  write(nested, [
    sidechain(user("b1", null, "2026-09-22T10:00:05Z", "Check."), "bbb"),
    sidechain(assistant("b2", "b1", "2026-09-22T10:00:06Z", [{ type: "text", text: "Tokens ok." }]), "bbb"),
  ]);
  write(join(dir, "workflows", "agent-bbb.meta.json"), [
    {
      agentType: "general-purpose",
      description: "Check tokens",
      toolUseId: "toolu_B",
      parentAgentId: "aaa",
      spawnDepth: 2,
      model: "claude-sonnet-5",
    },
  ]);
  write(join(dir, "agent-ccc.jsonl"), [
    sidechain(user("c1", null, "2026-09-22T10:00:03Z", "Watch the build"), "ccc"),
    sidechain(assistant("c2", "c1", "2026-09-22T10:01:50Z", [{ type: "text", text: "Green." }]), "ccc"),
  ]);
  write(join(dir, "agent-ccc.meta.json"), [
    { agentType: "general-purpose", toolUseId: "toolu_C", spawnDepth: 1, requestShape: "background" },
  ]);

  const items = await runEffect(readFile(files(), main));
  const atA = items.findIndex((item) => toolId(item) === "toolu_A");
  const atC = items.findIndex((item) => toolId(item) === "toolu_C");
  assert.equal(items[atA + 1].id, "task-toolu_A");
  assert.equal(items[atC + 1].id, "task-toolu_C");

  const { task: a, items: aItems } = itemAs(items[atA + 1], "task");
  assert.equal(a.id, "toolu_A");
  assert.equal(a.toolCallId, "toolu_A");
  assert.equal(a.title, "Survey the parser");
  assert.equal(a.name, "Explore");
  assert.equal(a.prompt, "Read the parser.");
  assert.equal(a.model, "claude-haiku-4-5");
  assert.equal(a.status, "completed");
  assert.equal(a.background, undefined);
  assert.equal(a.summary, "The parser is fine.");
  assert.equal(required(a.usage).usedTokens, 900);
  assert.equal(a.toolUses, 2);
  assert.ok(a.startedAt && a.endedAt);
  // Its own transcript, without the prompt record or the dead branch, and
  // the nested agent right after the call that started it.
  assert.deepEqual(
    required(aItems)
      .filter((item) => item.role === "assistant")
      .map((item) => item.text),
    ["Starting.", "The parser is fine."],
  );
  assert.ok(required(aItems).every((item) => item.role !== "user"));
  const atB = required(aItems).findIndex((item) => toolId(item) === "toolu_B");
  const { task: b, items: bItems } = itemAs(required(aItems)[atB + 1], "task");
  assert.equal(b.title, "Check tokens");
  assert.equal(b.parentTaskId, "toolu_A");
  assert.equal(b.summary, "Tokens ok.");
  assert.equal(required(bItems).length, 1);
  assert.ok(!items.some((item) => item.id === "task-toolu_B"), "a nested agent is not at the top level");

  const { task: c, items: cItems } = itemAs(items[atC + 1], "task");
  assert.equal(c.title, "Watch the build", "the prompt's first line when there is no description");
  assert.equal(c.background, true);
  assert.equal(c.status, "completed");
  assert.equal(c.summary, "Build is green & fast.", "the notification's result, not its status line");
  assert.equal(required(cItems).length, 1);
});

/// A notification that names several agents is a status line about all of
/// them; it sets their status and never their result.
test("a shared notification is not a result", async () => {
  const home = tempDir();
  const main = join(home, "s2.jsonl");
  write(main, [
    assistant("m1", null, "2026-09-22T10:00:00Z", [
      { type: "tool_use", id: "toolu_X", name: "Agent", input: { description: "X", run_in_background: true } },
    ]),
    user(
      "m2",
      "m1",
      "2026-09-22T10:05:00Z",
      "<task-notification>\n<task-id>xxx</task-id>\n<task-id>yyy</task-id>\n<status>stopped</status>\n<summary>2 background agents didn't finish</summary>\n</task-notification>",
    ),
  ]);
  const dir = join(home, "s2", "subagents");
  write(join(dir, "agent-xxx.jsonl"), [
    sidechain(user("x1", null, "2026-09-22T10:00:01Z", "X"), "xxx"),
    sidechain(assistant("x2", "x1", "2026-09-22T10:00:02Z", [{ type: "text", text: "Half done." }]), "xxx"),
  ]);
  write(join(dir, "agent-xxx.meta.json"), [{ agentType: "general-purpose", description: "X", toolUseId: "toolu_X" }]);
  const items = await runEffect(readFile(files(), main));
  const x = itemAs(required(items.find((item) => item.id === "task-toolu_X")), "task").task;
  assert.equal(x.status, "cancelled");
  assert.equal(x.summary, "Half done.");
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.