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

tools.test.ts5.5 KB
// Ported from the Rust plugin's tools.rs tests.
import assert from "node:assert/strict";
import { contentAs, required, test } from "./testing.ts";
import {
  diffsFromInput,
  kindOf,
  pluginTool,
  resultContent,
  resultText,
  startContent,
  titleOf,
  toolCall,
  planFromTodos,
  locationsOf,
} from "./tools.ts";

test("a workflow call is titled by its meta and shows its script", () => {
  const script =
    "export const meta = {\n  name: 'review',\n  description: \"Check the auth changes\",\n  phases: [{ title: 'Find' }],\n}\nphase('Find')\n";
  const input = { script };
  assert.equal(titleOf("Workflow", input), "Workflow review: Check the auth changes");
  assert.deepEqual(startContent("Workflow", input), [{ type: "text", text: script }]);
  const resumed = { scriptPath: "/c/workflows/scripts/probe-wf_47a42fba-b24.js", resumeFromRunId: "wf_47a42fba-b24" };
  assert.equal(titleOf("Workflow", resumed), "Workflow probe");
  // `name` inside the description is not the name.
  assert.equal(
    titleOf("Workflow", { script: "export const meta = { description: 'rename: x', name: \"real\" }" }),
    "Workflow real: rename: x",
  );
  assert.equal(titleOf("Workflow", {}), "Workflow");
});

test("a path tool is titled by its path alone", () => {
  assert.equal(titleOf("Read", { file_path: "/w/a.rs" }), "/w/a.rs");
  assert.equal(titleOf("Edit", { file_path: "/w/a.rs", old_string: "x" }), "/w/a.rs");
  assert.equal(titleOf("Unknown", { path: "/w/a.rs" }), "Unknown /w/a.rs");
  assert.equal(titleOf("Read", {}), "Read");
});

test("tools are classified by name", () => {
  assert.equal(kindOf("Read"), "read");
  assert.equal(kindOf("MultiEdit"), "edit");
  assert.equal(kindOf("Bash"), "execute");
  assert.equal(kindOf("Glob"), "search");
  assert.equal(kindOf("WebSearch"), "fetch");
  assert.equal(kindOf("Task"), "task");
  assert.equal(kindOf("Agent"), "task", "the current CLI calls the subagent tool Agent");
  assert.equal(kindOf("mcp__pencil__get_screenshot"), "other");
});

test("a subagent call is titled by its description", () => {
  const input = { description: "Search the tests", prompt: "find them", subagent_type: "Explore" };
  assert.equal(titleOf("Agent", input), "Search the tests");
  assert.equal(titleOf("Task", input), "Search the tests");
  assert.equal(titleOf("Agent", { prompt: "\n  look for the parser\nthen stop" }), "look for the parser");
  assert.equal(titleOf("Agent", {}), "Agent");
  assert.equal(titleOf("SendMessage", { to: "a12", message: "keep going\nplease" }), "Message: keep going");
  assert.equal(titleOf("SendMessage", { to: "a12", summary: "Resume", message: "long" }), "Message: Resume");
  assert.equal(titleOf("SendMessage", { to: "a12" }), "Message to a12");
  assert.equal(titleOf("SendMessage", {}), "SendMessage");
});

test("a plugin tool is titled by its own name", () => {
  assert.equal(titleOf("mcp__convergence__now", {}), "now");
  assert.equal(titleOf("mcp__convergence__execute", { code: "return 1" }), "execute");
  assert.equal(titleOf("mcp__convergence__open", { path: "/w/a.rs" }), "open /w/a.rs");
  assert.equal(titleOf("mcp__pencil__get_screenshot", {}), "mcp__pencil__get_screenshot");
  assert.equal(pluginTool("mcp__convergence__now"), "now");
  assert.equal(pluginTool("mcp__convergencex__now"), null);
  const call = toolCall("toolu_1", "mcp__convergence__execute", { code: "return 1" }, "pending");
  assert.equal(call.title, "execute");
  assert.deepEqual(call.input, { code: "return 1" }, "the code stays visible");
});

test("edit input becomes a unified diff", () => {
  const content = diffsFromInput("Edit", { file_path: "/tmp/a.txt", old_string: "one\n", new_string: "two\n" });
  assert.equal(content[0].type, "diff");
  assert.equal(content[0].path, "/tmp/a.txt");
  assert.equal(content[0].newText, undefined);
  assert.match(required(content[0].diff), /-one/);
  assert.match(required(content[0].diff), /\+two/);
  const multi = diffsFromInput("MultiEdit", {
    file_path: "/a",
    edits: [
      { old_string: "a\n", new_string: "b\n" },
      { old_string: "c\n", new_string: "d\n" },
    ],
  });
  assert.equal(required(required(contentAs(multi[0], "diff").diff).match(/^@@/gm)).length, 2);
});

test("write input becomes a whole file snapshot", () => {
  const content = diffsFromInput("Write", { file_path: "/tmp/a.txt", content: "hello\n" });
  assert.equal(contentAs(content[0], "diff").newText, "hello\n");
  assert.equal(contentAs(content[0], "diff").diff, undefined);
});

test("a bash result becomes a terminal", () => {
  const content = resultContent("Bash", { command: "ls -l" }, "a\n", { stdout: "a\n", exitCode: 2 });
  assert.deepEqual(content, [{ type: "terminal", command: "ls -l", output: "a\n", exitCode: 2 }]);
  assert.deepEqual(startContent("Bash", { command: "ls" }), [{ type: "terminal", command: "ls", output: "" }]);
});

test("tool result content flattens blocks", () => {
  assert.equal(resultText("plain"), "plain");
  assert.equal(
    resultText([
      { type: "text", text: "a" },
      { type: "text", text: "b" },
    ]),
    "ab",
  );
  assert.equal(resultText([{ type: "image" }]), "[image]");
  assert.equal(resultText(null), "");
});

test("todos become a plan, and a path with an offset a located line", () => {
  assert.deepEqual(planFromTodos({ todos: [{ content: "one", status: "in_progress" }, { activeForm: "two" }] }), {
    entries: [
      { content: "one", status: "in_progress" },
      { content: "two", status: "pending" },
    ],
  });
  assert.deepEqual(locationsOf({ file_path: "/a", offset: 12 }), [{ path: "/a", line: 12 }]);
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.