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

state.ts3.5 KB
// What the plugin remembers about one host session beyond what the CLI's
// own transcript holds (the Rust `SessionState`): the id the CLI files it
// under, and the user record each of the host's prompts became.
//
// A rewind is resolved with the map, and it lives in this plugin alone:
// the CLI's file does not know the host's ids. The Rust plugin kept it in
// `<data dir>/agents/claude/sessions/<id>.json`; this one keeps it in its
// plugin storage under `session:<id>`, so a restart of the app, or of the
// session after an earlier rewind, can still rewind to a message sent
// before it. The old files are read into it once (`legacy.ts`); `ready`
// is that read, which every load waits for.
import * as Context from "effect/Context";
import * as Layer from "effect/Layer";
import * as Effect from "effect/Effect";
import { Plugin, Host } from "convergence/effect";
import type { PluginServices } from "convergence/effect";
import { adoptLegacy, legacyDir } from "./legacy.ts";
import { objectOf } from "./wire.ts";

export interface SessionState {
  cli: string | null;
  sent: Record<string, number>;
}

const PREFIX = "session:";

export function emptyState(): SessionState {
  return { cli: null, sent: {} };
}

function parse(raw: unknown) {
  const value = objectOf(raw);
  const state = emptyState();
  if (!value || typeof value !== "object") return state;
  if (typeof value.cli === "string" && value.cli) state.cli = value.cli;
  if (value.sent && typeof value.sent === "object") {
    for (const [item, index] of Object.entries(value.sent))
      if (typeof index === "number" && Number.isInteger(index) && index >= 0) state.sent[item] = index;
  }
  return state;
}

/// The session states in the plugin's storage.
export class SessionStore {
  constructor(readonlyReady: Effect.Effect<unknown, never, PluginServices> = Effect.void) {
    this.ready = readonlyReady;
  }
  ready: Effect.Effect<unknown, never, PluginServices>;
  load = Effect.fn("Claude.state.load")(function* (this: SessionStore, id: string) {
    yield* this.ready;
    const host = yield* Host;
    return yield* host.call("host/storage.get", { key: PREFIX + id }).pipe(
      Effect.map((result) => parse(result.value)),
      Effect.catchTag(["HostCallFailed", "PermissionNotGranted", "NeedsReview"], (error) =>
        Effect.sync(() => {
          console.warn(`claude: the state of session ${id} could not be read: ${error.message}`);
          return emptyState();
        }),
      ),
    );
  });
  save = Effect.fn("Claude.state.save")(function* (this: SessionStore, id: string, state: SessionState) {
    const value: { sent: Record<string, number>; cli?: string } = { sent: state.sent };
    if (state.cli) value.cli = state.cli;
    yield* (yield* Host).call("host/storage.set", { key: PREFIX + id, value }).pipe(
      Effect.catchTag(["HostCallFailed", "PermissionNotGranted", "NeedsReview"], (error) =>
        Effect.sync(() => {
          console.warn(`claude: the state of session ${id} could not be kept: ${error.message}`);
        }),
      ),
    );
  });
  cliIdOf = Effect.fn("Claude.state.cliIdOf")(function* (this: SessionStore, id: string) {
    return (yield* this.load(id)).cli ?? id;
  });
}

export class SessionStates extends Context.Service<SessionStates, SessionStore>()("claude/SessionStates") {
  static readonly layer = Layer.effect(
    SessionStates,
    Effect.gen(function* () {
      const plugin = yield* Plugin;
      const legacy = yield* Effect.cached(adoptLegacy(legacyDir(plugin.api)));
      yield* Effect.forkScoped(legacy);
      return new SessionStore(legacy);
    }),
  );
}

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.