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.ts4.5 KB
// What the Rust plugin kept in `<data dir>/agents/claude/`: the accounts
// (`instances.json`) and, for each host session, the id the CLI files it
// under and the records its prompts became (`sessions/<id>.json`).
//
// The first time this plugin loads in the Rust plugin's place, the host
// moves that folder into the plugin's data folder as `legacy/`
// (`api.paths.data`, read under the `data` scope of `fs.read`). The plugin
// reads it once into its storage: the accounts into `instances` (entries
// whose id storage lacks), each session into `session:<id>` (unless
// storage has one). The storage key `legacy` then says it was read, so the
// old files are never read again.
import * as Effect from "effect/Effect";
import * as Result from "effect/Result";
import { Fs, Host } from "convergence/effect";
import type { Api } from "convergence";
import { objectOf } from "./wire.ts";
import { join, mapLimit } from "./files.ts";

const MARK = "legacy";

/// The legacy folder, or `null` when the runtime names no data folder.
export function legacyDir(api: Api) {
  const data = api.paths?.data;
  return typeof data === "string" && data ? join(data, "legacy") : null;
}

const idOf = (entry: unknown) => {
  const id = objectOf(entry).id;
  return typeof id === "string" ? id.trim() : "";
};

/// The stored accounts with the legacy ones whose id they lack after them;
/// `null` when that changes nothing.
export function mergeInstances(stored: unknown, legacy: unknown) {
  if (!Array.isArray(legacy)) return null;
  const current = Array.isArray(stored) ? stored : [];
  const ids = new Set(current.map(idOf));
  const added = legacy.filter((entry) => {
    const id = idOf(entry);
    if (!entry || typeof entry !== "object" || ids.has(id)) return false;
    ids.add(id);
    return true;
  });
  return added.length ? [...current, ...added] : null;
}

/// The legacy file's JSON value; `undefined` when it is not there or is
/// not JSON. A read the plugin may not make throws.
const readJson = Effect.fn("Claude.legacy.readJson")(function* (path: string) {
  const result = yield* Effect.result((yield* Fs).read(path));
  if (Result.isFailure(result)) {
    if (result.failure._tag === "PermissionNotGranted" || result.failure._tag === "NeedsReview")
      return yield* result.failure;
    return undefined;
  }
  try {
    const value: unknown = JSON.parse(result.success);
    return value;
  } catch {
    console.warn(`claude: ${path} is not JSON; it is left out`);
    return undefined;
  }
});
export const adoptLegacy = Effect.fn("Claude.adoptLegacy")(
  function* (dir: string | null) {
    if (!dir) return false;
    const host = yield* Host;
    const fs = yield* Fs;
    if ((yield* host.call("host/storage.get", { key: MARK })).value) return false;
    let changed = false;
    const legacy = yield* readJson(join(dir, "instances.json"));
    if (legacy !== undefined) {
      const merged = mergeInstances((yield* host.call("host/storage.get", { key: "instances" })).value ?? null, legacy);
      if (merged) {
        yield* host.call("host/storage.set", { key: "instances", value: merged });
        changed = true;
      }
    }
    const listing = yield* Effect.result(fs.list(join(dir, "sessions")));
    if (
      Result.isFailure(listing) &&
      (listing.failure._tag === "PermissionNotGranted" || listing.failure._tag === "NeedsReview")
    )
      return yield* listing.failure;
    const files = (Result.isSuccess(listing) ? listing.success : []).filter(
      (entry) => entry.kind === "file" && entry.name.endsWith(".json"),
    );
    let sessions = 0;
    yield* mapLimit(
      files,
      8,
      Effect.fn(function* (entry) {
        const id = entry.name.slice(0, -".json".length);
        const state = yield* readJson(join(dir, "sessions", entry.name));
        if (!id || !state || typeof state !== "object") return;
        const key = `session:${id}`;
        if ((yield* host.call("host/storage.get", { key })).value) return;
        yield* host.call("host/storage.set", { key, value: state });
        sessions += 1;
      }),
    );
    yield* host.call("host/storage.set", { key: MARK, value: { at: Date.now(), sessions } });
    if (changed || sessions)
      console.info(`claude: took the Rust plugin's ${changed ? "accounts and " : ""}${sessions} session states`);
    return changed;
  },
  Effect.catchTag(["HostCallFailed", "PermissionNotGranted", "NeedsReview"], (error) =>
    Effect.sync(() => {
      console.warn(`claude: the Rust plugin's files could not be read: ${error.message}`);
      return false;
    }),
  ),
);

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.