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

options.test.ts7.7 KB
// Ported from the Rust plugin's options.rs and models.rs tests.
import assert from "node:assert/strict";
import type { ConfigOption } from "./types.ts";
import { required, test } from "./testing.ts";
import {
  Catalog,
  DEFAULT,
  EFFORT,
  FAST_MODE,
  MODEL,
  PERMISSION_MODE,
  THINKING,
  ULTRACODE,
  choiceFor,
  effortSettings,
  fastModeNote,
  permissionFlag,
  selected,
} from "./options.ts";
import { manifest } from "./models.ts";
import { permissionMode } from "../sdk/agent.ts";

function catalog() {
  return Catalog.fromResponse({
    models: [
      {
        value: "default",
        resolvedModel: "claude-opus-5[1m]",
        displayName: "Default (recommended)",
        description: "Opus 5",
        supportsEffort: true,
        supportedEffortLevels: ["low", "medium", "high"],
        supportsAdaptiveThinking: true,
        supportsFastMode: true,
      },
      { value: "haiku", resolvedModel: "claude-haiku-4-5", displayName: "Haiku", description: "Fast" },
    ],
    commands: [{ name: "review", description: "Review code", argumentHint: "<path>" }],
    current_permission_mode: "auto",
    fast_mode_state: "off",
    fast_mode_disabled_reason: "sdk_opt_in_required",
  });
}

/// The CLI's catalog when the user's settings name `opus`.
function cliCatalog() {
  return Catalog.fromResponse({
    models: [
      { value: "default", resolvedModel: "claude-opus-5-5[1m]", displayName: "Default (recommended)" },
      { value: "opus[1m]", resolvedModel: "claude-opus-5-5[1m]", displayName: "Opus (1M context)" },
      { value: "claude-fable-5-1[1m]", resolvedModel: "claude-fable-5-1", displayName: "Fable" },
      { value: "sonnet", resolvedModel: "claude-sonnet-5", displayName: "Sonnet" },
      { value: "haiku", resolvedModel: "claude-haiku-4-5-20251001", displayName: "Haiku" },
      { value: "opus", resolvedModel: "claude-opus-5-5", displayName: "Opus" },
    ],
  });
}

const option = (options: ConfigOption[], id: string) =>
  options.find((option) => option.id === id) ?? assert.fail(`no ${id} option`);
/// The host's `model_reasoning`: a per-model catalog, or null.
function reasoningOf(option: ConfigOption, model: string) {
  if (!required(option.choices).some((choice) => (choice.reasoningLevels ?? []).length)) return null;
  return required(option.choices).find((choice) => choice.value === model)?.reasoningLevels ?? [];
}

test("the built-in list is valid and names its default", () => {
  const list = manifest();
  assert.ok(list.models.some((model) => model.slug === list.default));
});

test("models come from the list with versions and 1M context", () => {
  const model = option(cliCatalog().options({}), MODEL);
  assert.equal(model.category, "model");
  assert.equal(model.value, "claude-opus-5-5[1m]");
  const named = (value: string) => required(model.choices).find((choice) => choice.value === value)?.name;
  assert.equal(named("claude-opus-5-5[1m]"), "Opus 5.5");
  assert.equal(named("claude-fable-5[1m]"), "Fable 5");
  assert.equal(named("claude-opus-4-8"), "Opus 4.8");
  assert.equal(named("claude-haiku-4-5"), "Haiku 4.5");
  assert.ok(!required(model.choices).some((choice) => choice.value === DEFAULT), "no Default entry");
  assert.ok(required(model.choices).every((choice) => choice.group === undefined));
});

test("models carry their own effort levels", () => {
  const model = option(cliCatalog().options({}), MODEL);
  assert.equal(required(reasoningOf(model, "claude-opus-4-6[1m]")).length, 4);
  assert.equal(required(reasoningOf(model, "claude-haiku-4-5")).length, 0);
});

test("ultracode is offered where the model has xhigh", () => {
  const levels = (catalog: Catalog, model: string) =>
    required(reasoningOf(option(catalog.options({}), MODEL), model)).map((choice) => choice.value);
  const cli = cliCatalog();
  assert.deepEqual(levels(cli, "claude-opus-5-5[1m]"), ["low", "medium", "high", "xhigh", "max", ULTRACODE]);
  assert.ok(!levels(cli, "claude-opus-4-6[1m]").includes(ULTRACODE), "4.6 has no xhigh");
  assert.deepEqual(levels(cli, "claude-haiku-4-5"), []);
  const options = cli.options({});
  const ultracode = required(reasoningOf(option(options, MODEL), "claude-opus-5-5[1m]")).at(-1);
  assert.equal(required(ultracode).name, "Ultracode");
  assert.ok(required(ultracode).icon, "the pill shows its mark");
  assert.equal(required(ultracode).transient, true, "a new chat must not inherit it");
  assert.ok(required(option(options, EFFORT).choices).some((choice) => choice.value === ULTRACODE));
  const off = cliCatalog();
  off.workflowsOff = true;
  assert.ok(!levels(off, "claude-opus-5-5[1m]").includes(ULTRACODE), "workflows are off");
});

test("effort choices become flag settings", () => {
  assert.deepEqual(effortSettings(ULTRACODE), { effortLevel: "xhigh", ultracode: true });
  assert.deepEqual(effortSettings("high"), { effortLevel: "high", ultracode: false });
  assert.deepEqual(effortSettings(DEFAULT), { effortLevel: null, ultracode: false });
});

test("the CLI values select the model they resolve to", () => {
  const model = option(cliCatalog().options({}), MODEL);
  const chosen = (value: string) => choiceFor(model, value)?.name;
  for (const value of ["opus", "opus[1m]", "default", "claude-opus-5-5"])
    assert.equal(chosen(value), "Opus 5.5", value);
  assert.equal(chosen("claude-fable-5-1[1m]"), "Fable 5.1");
  assert.equal(chosen("sonnet"), "Sonnet 5");
  assert.equal(chosen("haiku"), "Haiku 4.5");
});

test("chosen values win over catalog defaults", () => {
  const values = { [MODEL]: "claude-haiku-4-5", [EFFORT]: "high" };
  const options = catalog().options(values);
  assert.equal(option(options, MODEL).value, "claude-haiku-4-5");
  assert.equal(option(options, EFFORT).value, "high");
  assert.equal(selected(values, EFFORT), "high");
  assert.equal(selected({ [EFFORT]: "default" }, EFFORT), null);
});

test("slash commands come from the CLI", () => {
  assert.deepEqual(catalog().slashCommands(), [
    { name: "review", description: "Review code", source: "agent", inputHint: "<path>" },
  ]);
});

test("approvals use the shared four modes", () => {
  const approvals = option(catalog().options({}), PERMISSION_MODE);
  assert.equal(approvals.category, "approval");
  assert.deepEqual(
    required(approvals.choices).map((choice) => choice.value),
    permissionMode.ALL,
    "Claude has its own reviewer, so Auto is offered",
  );
  // The CLI is configured for `auto` here, and a chat still starts
  // Supervised.
  assert.equal(approvals.value, permissionMode.SUPERVISED);
  assert.equal(
    option(catalog().options({ [PERMISSION_MODE]: permissionMode.FULL }), PERMISSION_MODE).value,
    permissionMode.FULL,
  );
  assert.equal(permissionFlag(permissionMode.SUPERVISED), "default");
  assert.equal(permissionFlag(permissionMode.AUTO_EDITS), "acceptEdits");
  assert.equal(permissionFlag(permissionMode.AUTO), "auto");
  assert.equal(permissionFlag(permissionMode.FULL), "bypassPermissions");
});

test("toggles appear only when a model supports them", () => {
  const options = catalog().options({ [FAST_MODE]: true });
  assert.equal(option(options, THINKING).kind, "toggle");
  assert.equal(option(options, THINKING).value, true);
  assert.equal(option(options, FAST_MODE).value, true);
  // The CLI says fast mode waits for a client to opt in, and this toggle
  // is that opt in, so nothing is reported as unavailable.
  assert.equal(option(options, FAST_MODE).description, undefined);
  assert.equal(option(options, FAST_MODE).category, "speed");
  assert.equal(fastModeNote("not_first_party"), "Only an Anthropic account can use fast mode.");
  const plain = Catalog.fromResponse({ models: [{ value: "haiku", displayName: "Haiku", description: "" }] }).options(
    {},
  );
  assert.ok(!plain.some((option) => option.id === THINKING));
  assert.ok(!plain.some((option) => option.id === FAST_MODE));
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.