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
records.ts5.2 KB
// Stored transcript records (`<session>.jsonl` and every subagent's file)
// mapped to transcript items (the record half of the Rust `history.rs`).
import * as wire from "./wire.ts";
import type { TranscriptItem } from "./types.ts";
import type { TaskInfo } from "convergence/protocol";
import { planFromTodos, resultContent, resultText, toolCall } from "./tools.ts";
/// A record's time as an ISO string, or `null`.
export function timestamp(record: wire.Record | undefined) {
if (typeof record?.timestamp !== "string") return null;
const time = Date.parse(record.timestamp);
return Number.isNaN(time) ? null : new Date(time).toISOString();
}
/// The first text of a message's content: the string itself, or the first
/// text block.
export function firstText(content: unknown): string | null {
if (typeof content === "string") return content;
if (Array.isArray(content)) {
const block = content.map(wire.objectOf).find((block) => block?.type === "text" && typeof block.text === "string");
return block && typeof block.text === "string" ? block.text : null;
}
return null;
}
/// The marker the CLI records when the user stops a turn.
export function isInterruption(content: unknown) {
let text = null;
if (typeof content === "string") text = content;
else if (Array.isArray(content) && content.length === 1 && typeof wire.objectOf(content[0]).text === "string")
text = String(wire.objectOf(content[0]).text);
return text !== null && text.startsWith("[Request interrupted by user");
}
type Body<T> = T extends { id: string } ? Omit<T, "id"> : never;
function item(id: string, createdAt: string | null, body: Body<TranscriptItem>) {
const out: { id: string; createdAt?: string } = { id };
if (createdAt) out.createdAt = createdAt;
return Object.assign(out, body);
}
/// Maps transcript records to items. The main conversation and every
/// subagent's own file share this.
export function recordsToItems(records: wire.Record[]): TranscriptItem[] {
const items: TranscriptItem[] = [];
// Tool calls by id, so a `tool_result` updates the call it answers.
const calls = new Map<string, number>();
for (const record of records) {
const createdAt = timestamp(record);
const uuid = typeof record?.uuid === "string" ? record.uuid : "";
const content = record?.message?.content;
if (content === undefined) continue;
if (record.type === "user") {
// The CLI writes the end of a background task into the conversation
// as a user message. Nobody typed it; the task item shows it.
if (record.origin?.kind === "task-notification") continue;
// Stop, as the CLI records it: shown the way a live stop is.
if (isInterruption(content)) {
items.push(item(uuid, createdAt, { role: "notice", text: "Stopped." }));
continue;
}
const blocks: import("convergence/protocol").ContentBlock[] = [];
for (const block of Array.isArray(content) ? content : []) {
if (block?.type === "text") {
blocks.push({ type: "text", text: typeof block.text === "string" ? block.text : "" });
} else if (block?.type === "tool_result") {
const at = calls.get(block.tool_use_id ?? "");
if (at === undefined) continue;
const entry = items[at];
if (entry?.role !== "tool") continue;
const call = entry.call;
call.status = block.is_error === true ? "failed" : "completed";
const content = resultContent(call.name, call.input ?? null, resultText(block.content), null);
call.content = content;
}
}
if (typeof content === "string") blocks.push({ type: "text", text: content });
if (blocks.length) items.push(item(uuid, createdAt, { role: "user", blocks }));
} else if (record.type === "assistant") {
(Array.isArray(content) ? content : []).forEach((block, index) => {
const id = `${uuid}:${index}`;
if (block?.type === "text") {
const text = typeof block.text === "string" ? block.text : "";
if (text) items.push(item(id, createdAt, { role: "assistant", text }));
} else if (block?.type === "thinking") {
const text = typeof block.thinking === "string" ? block.thinking : "";
if (text) items.push(item(id, createdAt, { role: "reasoning", text }));
} else if (block?.type === "tool_use") {
const callId = typeof block.id === "string" ? block.id : "";
const name = typeof block.name === "string" ? block.name : "";
const input = block.input ?? null;
if (name === "TodoWrite") {
const plan = planFromTodos(input);
if (plan) items.push(item(`${id}:plan`, createdAt, { role: "plan", plan }));
}
calls.set(callId, items.length);
items.push(item(id, createdAt, { role: "tool", call: toolCall(callId, name, input, "pending") }));
}
});
}
}
return items;
}
/// A task item: the subagent's snapshot with its own items inside, under
/// the id the host gives a task it learns about live.
export function taskItem(info: TaskInfo, items: TranscriptItem[]): TranscriptItem {
return item(`task-${info.id}`, info.startedAt ?? null, { role: "task", task: info, items });
}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.