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

testing.ts6.6 KB
// Node fixtures for Claude's Effect programs. Each test owns its runtime,
// queues and scope; shutdown finishes before the next fixture is released.
import * as Exit from "effect/Exit";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as ManagedRuntime from "effect/ManagedRuntime";
import * as Scope from "effect/Scope";
import * as Queue from "effect/Queue";
import * as Stream from "effect/Stream";
import * as z from "zod";
import assert from "node:assert/strict";
import { test as nodeTest } from "node:test";
import type { TestContext } from "node:test";
import { AsyncLocalStorage } from "node:async_hooks";
import { mkdtempSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { liveLayer, runPlugin } from "convergence/effect";
import type { PluginServices } from "convergence/effect";
import type { Api } from "convergence";
import { apiFiles } from "./files.ts";
import { diskFs, fakeApi, FakePeer } from "../sdk/testing.ts";
import { LineProcess } from "../sdk/process.ts";
import { SessionStates } from "./state.ts";
import { registerAccounts } from "./main.ts";
import type { ClaudeAgent } from "./agent.ts";
import type { Job } from "./session.ts";
import type { AgentEventKind, TranscriptItem, ToolContent } from "./types.ts";

interface Fixture {
  api: Api;
  resources: Map<Api, Resources>;
  disposers: (() => Promise<void>)[];
}
interface Resources {
  scope: Scope.Closeable;
  jobs: Queue.Queue<Job>;
  runtime: ManagedRuntime.ManagedRuntime<PluginServices | Scope.Scope, never>;
}
const current = new AsyncLocalStorage<Fixture>();
function fixtureState() {
  const state = current.getStore();
  if (!state) throw new Error("use a Claude test fixture");
  return state;
}
export function test(name: string, body: (context: TestContext) => unknown) {
  return nodeTest(name, async (context) => {
    const state: Fixture = { api: fakeApi({ fs: diskFs() }), resources: new Map(), disposers: [] };
    await current.run(state, async () => {
      try {
        await body(context);
      } finally {
        for (const dispose of state.disposers.reverse()) await dispose();
        for (const resource of state.resources.values()) {
          await Effect.runPromise(Scope.close(resource.scope, Exit.void));
          await resource.runtime.dispose();
        }
      }
    });
  });
}
export function useApi(api: Api) {
  fixtureState().api = api;
  return api;
}
export function resources(api: Api = fixtureState().api): Resources {
  const state = fixtureState();
  const existing = state.resources.get(api);
  if (existing) return existing;
  const scope = Effect.runSync(Scope.make());
  const jobs = Effect.runSync(Queue.make<Job>());
  const runtime: Resources["runtime"] = ManagedRuntime.make(
    Layer.mergeAll(
      liveLayer(
        api,
        (effect, signal) => runtime.runPromise(effect, { signal }),
        (effect) => {
          void runtime.runPromise(effect);
        },
      ),
      Layer.succeed(Scope.Scope, scope),
    ),
  );
  const resource = { scope, jobs, runtime };
  state.resources.set(api, resource);
  void runtime.runPromise(
    Stream.runForEach(Stream.fromQueue(jobs), (job) => job.pipe(Effect.forkIn(scope))).pipe(Effect.forkIn(scope)),
  );
  return resource;
}
export function run<A, E>(effect: Effect.Effect<A, E, PluginServices | Scope.Scope>, api?: Api) {
  return resources(api).runtime.runPromise(effect);
}
export function activateAccounts(api: Api) {
  useApi(api);
  const agents: ClaudeAgent[] = [];
  fixtureState().disposers.push(runPlugin(SessionStates.layer, registerAccounts(agents))(api));
  return { agents };
}

export const WF = "toolu_01VtcZ7X6iVcHJxYYKo3ShgL";
export const RUN = "wf_47a42fba-b24";
const here = new URL(".", import.meta.url).pathname;
export const fixture = (name: string) => readFileSync(join(here, "testdata", name), "utf8");
export function streamLines(dir: string, output: string) {
  return fixture("workflow_stream.jsonl")
    .split("\n")
    .filter((line) => line.trim())
    .map((line) => line.replaceAll("$DIR", dir).replaceAll("$OUTPUT", output));
}
export const tempDir = () => mkdtempSync(join(tmpdir(), "claude-plugin-"));
export const files = () => apiFiles();

const text = z.string().optional();
const mcpResult = z.looseObject({
  tools: z.array(z.looseObject({ name: z.string() })).optional(),
  content: z.array(z.looseObject({ text })).optional(),
});
export const writtenSchema = z.looseObject({
  type: text,
  uuid: text,
  priority: text,
  request_id: text,
  answered: z.boolean().optional(),
  request: z
    .looseObject({
      subtype: text,
      mode: text,
      sdkMcpServers: z.array(z.string()).optional(),
      sdkMcpServerConfigs: z.record(z.string(), z.object({ timeout: z.number() })).optional(),
      appendSystemPrompt: text,
      settings: z.record(z.string(), z.unknown()).optional(),
    })
    .optional(),
  response: z
    .looseObject({
      subtype: text,
      request_id: text,
      error: text,
      response: z
        .looseObject({ behavior: text, mcp_response: z.looseObject({ result: mcpResult.optional() }).optional() })
        .optional(),
    })
    .optional(),
});
export type Written = z.infer<typeof writtenSchema>;
export class RecordingProcess extends LineProcess {
  constructor(readonlyWritten: Written[]) {
    super(new FakePeer({ program: "claude", args: [], options: {}, pid: 1 }).child, { name: "claude" });
    this.written = readonlyWritten;
  }
  written: Written[];
  override send(message: unknown) {
    this.written.push(writtenSchema.parse(typeof message === "string" ? JSON.parse(message) : message));
  }
}

// Assertions narrow protocol unions while keeping fixture expectations explicit.

export function required<A>(value: A | null | undefined): A {
  assert.ok(value !== null && value !== undefined);
  return value;
}
export function eventAs<K extends AgentEventKind["event"]>(
  value: AgentEventKind,
  kind: K,
): Extract<AgentEventKind, { event: K }>;
export function eventAs(value: AgentEventKind, kind: AgentEventKind["event"]) {
  assert.equal(value.event, kind);
  return value;
}
export function itemAs<K extends TranscriptItem["role"]>(
  value: TranscriptItem,
  role: K,
): Extract<TranscriptItem, { role: K }>;
export function itemAs(value: TranscriptItem, role: TranscriptItem["role"]) {
  assert.equal(value.role, role);
  return value;
}
export function contentAs<K extends ToolContent["type"]>(
  value: ToolContent,
  type: K,
): Extract<ToolContent, { type: K }>;
export function contentAs(value: ToolContent, type: ToolContent["type"]) {
  assert.equal(value.type, type);
  return value;
}

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.