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
Files
account.ts7.9 KB
// What the CLI says about the signed in account and about its own
// installation (the Rust `account.rs`).
import * as Effect from "effect/Effect";
import { Process } from "convergence/effect";
import { ProviderError } from "./errors.ts";
import type { AgentInfo } from "./types.ts";
import { objectOf } from "./wire.ts";
import { runProcess } from "../sdk/effect.ts";
import { latestNpmEffect, isNewer } from "../sdk/maintenance.ts";
import { CONFIG_DIR } from "./config.ts";
type Owner = { manager: "npm"; prefix: string } | { manager: "native" | "" };
/// The npm package that ships the CLI.
export const PACKAGE = "@anthropic-ai/claude-code";
/// How long `claude auth status` may take. It reads local credentials, so
/// a slow answer means something is wrong rather than busy.
const PROBE_TIMEOUT = 20_000;
/// How long an upgrade may take before it is reported as stuck.
const UPDATE_TIMEOUT = 300_000;
/// The answer of `claude auth status --json`, or `null` for output that is
/// not one.
export function parseAccount(output: string) {
let status;
try {
const value: unknown = JSON.parse(output);
if (!value || typeof value !== "object") return null;
status = objectOf(value);
} catch {
return null;
}
const text = (key: string) => (typeof status[key] === "string" && status[key] ? status[key] : null);
return {
loggedIn: status.loggedIn === true,
email: text("email"),
plan: text("subscriptionType"),
apiProvider: text("apiProvider"),
authMethod: text("authMethod"),
configDirectory: text("configDirectory"),
orgId: text("orgId"),
orgName: text("orgName"),
};
}
/// Native initialize describes the credentials the session actually uses;
/// auth status adds the organization id and effective configuration directory.
/// Neither an email cached under API-key auth nor a directory alone proves a
/// subscription identity. Unreported or disagreeing evidence stays unknown.
export function recoveryIdentity(raw: unknown, account: ReturnType<typeof parseAccount>, configDir: string | null) {
const native = objectOf(raw);
if (!account?.loggedIn || account.authMethod !== "claude.ai" || account.apiProvider !== "firstParty") return null;
if (!account.email || !account.orgId || !account.orgName || !account.configDirectory || !configDir) return null;
if (native.apiProvider !== "firstParty" || native.email !== account.email || native.organization !== account.orgName)
return null;
if (native.apiKeySource || (native.tokenSource && native.tokenSource !== "claude.ai")) return null;
if (account.configDirectory.replace(/\/+$/, "") !== configDir.replace(/\/+$/, "")) return null;
return JSON.stringify(["claude/oauth", account.configDirectory.replace(/\/+$/, ""), account.email, account.orgId]);
}
/// A one line description of who is signed in, for `AgentInfo`.
export function accountSummary(account: NonNullable<ReturnType<typeof parseAccount>>) {
const plan = account.plan ? account.plan.charAt(0).toUpperCase() + account.plan.slice(1) : null;
if (account.email && plan) return `Signed in as ${account.email} on the ${plan} plan.`;
if (account.email) return `Signed in as ${account.email}.`;
if (plan) return `Signed in on the ${plan} plan.`;
return account.apiProvider ? `Signed in through ${account.apiProvider}.` : null;
}
/// Asks the CLI who is signed in, with the account's own environment and
/// the session's working directory when reconciling runtime credentials.
export const probeAccount = Effect.fn("Claude.probeAccount")(function* (env: Record<string, string>, cwd?: string) {
const result = yield* runProcess("claude", ["auth", "status", "--json"], { env, cwd, timeout: PROBE_TIMEOUT });
if (result.timedOut) return yield* new ProviderError({ message: "claude auth status did not answer in time" });
const account = parseAccount(result.stdout);
if (!account)
return yield* new ProviderError({ message: `claude auth status returned no account: ${result.stderr.trim()}` });
return account;
});
/// A readable failure from a CLI command: its stderr, or its stdout when
/// the command wrote its complaint there instead.
export function messageOf(result: Effect.Success<ReturnType<typeof runProcess>>) {
if (result.stderr.trim()) return result.stderr.trim();
if (result.stdout.trim()) return result.stdout.trim();
return `claude exited with ${result.code ?? result.signal}`;
}
/// The message shown when nobody is signed in. It names the configuration
/// directory, because the login has to be made against the same one.
export function signedOutMessage(chosenDir: string | null) {
return chosenDir
? `Run \`claude auth login\` with ${CONFIG_DIR}=${chosenDir} to sign in.`
: "Run `claude auth login` to sign in.";
}
/// Which installer owns the binary at `realPath` (links followed):
/// `{ manager: "native" }`, `{ manager: "npm", prefix }` or `{ manager: ""
/// }`. Anything unproven stays `""`, so Convergence never runs a package
/// manager against an install it cannot show it created.
export function managerOf(realPath: unknown): Owner {
if (!realPath) return { manager: "" };
const original = String(realPath).replace(/\\/g, "/");
const path = original.toLowerCase();
if (
path.endsWith("/.local/bin/claude") ||
path.endsWith("/.local/bin/claude.exe") ||
path.includes("/.local/share/claude/")
) {
return { manager: "native" };
}
const segment = `/lib/node_modules/${PACKAGE}/`;
const at = path.lastIndexOf(segment);
if (at >= 0) {
const prefix = original.slice(0, at);
// A `node_modules` above the package means a project checkout, not a
// global install, and npm would refuse to upgrade it in place.
if (!prefix.toLowerCase().includes("/node_modules/")) return { manager: "npm", prefix: prefix || "/" };
}
return { manager: "" };
}
/// The installed version: `claude --version`, trimmed.
export const which = Effect.fn("Claude.which")(function* () {
return yield* (yield* Process)
.which("claude")
.pipe(
Effect.catchTag(["HostCallFailed", "PermissionNotGranted", "NeedsReview"], () =>
Effect.succeed({ path: null, realPath: null }),
),
);
});
export const installedVersion = Effect.fn("Claude.installedVersion")(
function* () {
return (yield* runProcess("claude", ["--version"], { timeout: PROBE_TIMEOUT })).stdout.trim() || null;
},
Effect.catchTag(["HostCallFailed", "PermissionNotGranted", "NeedsReview", "TransportFailed"], () =>
Effect.succeed(null),
),
);
export const maintenance = Effect.fn("Claude.maintenance")(function* (version: string | null) {
const { realPath } = yield* which();
const owner = managerOf(realPath);
const latest =
owner.manager === "npm" || owner.manager === "native"
? yield* latestNpmEffect(PACKAGE, owner.manager === "npm" ? owner.prefix : null)
: null;
const out: NonNullable<AgentInfo["maintenance"]> = { canUpdate: owner.manager !== "" };
if (version) out.installedVersion = version;
if (version && latest && isNewer(version, latest)) out.latestVersion = latest;
if (owner.manager) out.manager = owner.manager;
return { out, owner, realPath };
});
export const update = Effect.fn("Claude.update")(function* (env: Record<string, string>) {
const { realPath } = yield* which();
const owner = managerOf(realPath);
let result;
if (owner.manager === "native") result = yield* runProcess("claude", ["update"], { env, timeout: UPDATE_TIMEOUT });
else if (owner.manager === "npm")
result = yield* runProcess("npm", ["install", "-g", "--prefix", owner.prefix, `${PACKAGE}@latest`], {
timeout: UPDATE_TIMEOUT,
});
else
return yield* new ProviderError({
message: `nothing proves which installer owns ${realPath ?? "claude"}, so it must be updated by hand`,
});
if (result.timedOut) return yield* new ProviderError({ message: "the update did not finish in time" });
if (result.code !== 0)
return yield* new ProviderError({ message: `the update failed: ${result.stderr.trim() || result.stdout.trim()}` });
});Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 128.7 KB | 4 permissions | Listed |
No comments yet.