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
tools.ts10.8 KB
// Classification of Claude Code tool calls and rendering of their results
// (the Rust `tools.rs`). Everything here works from the structured
// `tool_use` input the CLI reports, never from parsing a shell string.
import * as wire from "./wire.ts";
import type { ToolCall, ToolContent, ToolLocation, Plan } from "./types.ts";
import { unifiedDiff } from "../sdk/diff.ts";
import { MCP_SERVER } from "../sdk/mcp.ts";
/// Maps a Claude Code tool name to a renderer category.
export function kindOf(name: string) {
switch (name) {
case "Read":
case "NotebookRead":
return "read";
case "Edit":
case "Write":
case "MultiEdit":
case "NotebookEdit":
return "edit";
case "Bash":
case "BashOutput":
case "KillShell":
return "execute";
case "Grep":
case "Glob":
case "LS":
return "search";
case "WebFetch":
case "WebSearch":
return "fetch";
// `Agent` is the current name of the subagent tool; `Task` is the one
// older CLIs and stored transcripts use.
case "Agent":
case "Task":
return "task";
default:
return "other";
}
}
function string(input: unknown, key: string) {
const value = wire.objectOf(input)[key];
return typeof value === "string" ? value : null;
}
/// The single path a tool call works on, when it has one.
export function pathOf(input: unknown) {
return string(input, "file_path") ?? string(input, "path") ?? string(input, "notebook_path");
}
/// The tool's own name when it is a Convergence plugin tool, which the CLI
/// names `mcp__convergence__<tool>` after the SDK MCP server that serves it.
export function pluginTool(name: unknown) {
const prefix = `mcp__${MCP_SERVER}__`;
return typeof name === "string" && name.startsWith(prefix) ? name.slice(prefix.length) : null;
}
/// The first line with any text on it, trimmed.
export function firstLine(text: unknown) {
if (typeof text !== "string") return null;
for (const line of text.split("\n")) {
const trimmed = line.trim();
if (trimmed) return trimmed;
}
return null;
}
function firstRawLine(text: string) {
return (text.split("\n")[0] ?? "").replace(/\r$/, "");
}
/// A one line label for the call.
export function titleOf(name: string, input: unknown) {
// A plugin tool is titled by its own name, even when it shares one with
// a CLI tool. Its input stays on the call untouched, so code mode's
// `execute` still shows the code it runs.
const tool = pluginTool(name);
if (tool !== null) {
const path = pathOf(input);
return path !== null ? `${tool} ${path}` : tool;
}
switch (name) {
case "Bash": {
const text = string(input, "description") ?? string(input, "command");
return text !== null ? firstRawLine(text) : name;
}
case "Grep":
case "Glob": {
const pattern = string(input, "pattern");
return pattern !== null ? `${name} ${pattern}` : name;
}
case "WebFetch":
case "WebSearch": {
const target = string(input, "url") ?? string(input, "query");
return target !== null ? `${name} ${target}` : name;
}
case "Agent":
case "Task": {
const description = string(input, "description");
if (description !== null && description.trim()) return description;
const prompt = string(input, "prompt");
return (prompt !== null ? firstLine(prompt) : null) ?? name;
}
// A message to a running subagent. `to` is the agent's id, which means
// nothing to a reader, so the summary comes first.
case "SendMessage": {
const text = string(input, "summary") ?? string(input, "message") ?? string(input, "content");
const line = text !== null ? firstLine(text) : null;
if (line !== null) return `Message: ${line}`;
const to = string(input, "to");
return to !== null ? `Message to ${to}` : name;
}
// What a dynamic workflow will do is in its script's `meta`.
case "Workflow":
return workflowTitle(input) ?? name;
default: {
// The renderer labels a row by its kind ("Read", "Edit"), so a known
// tool's title is just the path; an unknown one keeps its name.
const path = pathOf(input);
if (path === null) return name;
return kindOf(name) === "other" ? `${name} ${path}` : path;
}
}
}
/// "Workflow probe: Review the branch" from the script's `meta`, which the
/// CLI requires to be a plain literal, else from the saved workflow or the
/// script file the call names.
function workflowTitle(input: unknown) {
const script = string(input, "script") ?? "";
let name = metaField(script, "name") ?? string(input, "name");
if (name === null) {
const path = string(input, "scriptPath");
if (path !== null) {
const file = path.split("/").pop() ?? "";
const stem = file.includes(".") ? file.slice(0, file.lastIndexOf(".")) : file;
// A saved run's file is `<name>-wf_<run id>.js`.
if (stem) name = stem.split("-wf_")[0] ?? null;
}
}
const description = metaField(script, "description");
if (name && description) return `Workflow ${name}: ${description}`;
if (name) return `Workflow ${name}`;
if (description) return `Workflow: ${description}`;
return null;
}
/// The string value of `key` in the script's `export const meta = { … }`.
export function metaField(script: string, key: string) {
const start = script.indexOf("meta");
if (start < 0) return null;
let meta = script.slice(start);
const close = meta.indexOf("\n}");
if (close >= 0) meta = meta.slice(0, close);
let rest = meta;
for (;;) {
const at = rest.indexOf(key);
if (at < 0) return null;
const before = at > 0 ? rest[at - 1] : null;
const after = rest.slice(at + key.length).trimStart();
rest = rest.slice(at + key.length);
if (!["{", ",", " ", "\n", "\t"].includes(before ?? "")) continue;
if (!after.startsWith(":")) continue;
const value = after.slice(1).trimStart();
const quote = value[0];
if (!["'", '"', "`"].includes(quote ?? "")) return null;
const body = value.slice(1);
const end = body.indexOf(quote ?? "");
if (end < 0) return null;
const text = body.slice(0, end).trim();
return text || null;
}
}
export function locationsOf(raw: unknown): ToolLocation[] {
const input = wire.inputOf(raw);
const path = pathOf(input);
if (path === null) return [];
const location: ToolLocation = { path };
const offset = input?.offset;
if (typeof offset === "number" && Number.isInteger(offset) && offset >= 0) location.line = offset;
return [location];
}
/// The file changes a tool call describes, derived from its input alone.
/// Claude Code reports `Edit` as a fragment replacement, so the result is
/// a unified diff rather than whole-file snapshots; `Write` carries the
/// whole new file, so it gets `newText`.
export function diffsFromInput(name: string, raw: unknown): ToolContent[] {
const input = wire.inputOf(raw);
const path = pathOf(input);
if (path === null) return [];
const text = (value: unknown, key: string) => string(value, key) ?? "";
switch (name) {
case "Write":
return [{ type: "diff", path, newText: text(input, "content") }];
case "Edit":
return [{ type: "diff", path, diff: unifiedDiff(path, text(input, "old_string"), text(input, "new_string")) }];
case "MultiEdit": {
const edits = Array.isArray(input?.edits) ? input.edits : [];
const diff = edits.map((edit) => unifiedDiff(path, text(edit, "old_string"), text(edit, "new_string"))).join("");
return [{ type: "diff", path, diff }];
}
case "NotebookEdit":
return [{ type: "diff", path, diff: unifiedDiff(path, text(input, "old_source"), text(input, "new_source")) }];
default:
return [];
}
}
/// Flattens the `content` of a `tool_result` block into text.
export function resultText(content: unknown): string {
if (typeof content === "string") return content;
if (Array.isArray(content)) {
let out = "";
for (const block of content) {
if (typeof wire.objectOf(block).text === "string") out += wire.objectOf(block).text;
else if (wire.objectOf(block).type === "image") out += "[image]";
}
return out;
}
if (content === null || content === undefined) return "";
return JSON.stringify(content);
}
function exitCode(result: unknown) {
for (const key of ["exitCode", "exit_code", "returnCode", "code"]) {
if (Number.isInteger(wire.objectOf(result)[key])) return Number(wire.objectOf(result)[key]);
}
return null;
}
/// The content a call shows before it has produced anything: a command as
/// the line it will run, a workflow as its script, file edits as a diff.
export function startContent(name: string, input: unknown): ToolContent[] {
const command = string(input, "command");
if (kindOf(name) === "execute" && command !== null) return [{ type: "terminal", command, output: "" }];
if (name === "Workflow") {
const script = string(input, "script");
return script !== null ? [{ type: "text", text: script }] : [];
}
return diffsFromInput(name, input);
}
/// The content of a finished tool call: `input` as the model sent it,
/// `text` the flattened result and `structured` the CLI's
/// `tool_use_result` when present.
export function resultContent(name: string, input: unknown, text: string, structured: unknown): ToolContent[] {
switch (kindOf(name)) {
case "execute": {
const terminal: Extract<ToolContent, { type: "terminal" }> = {
type: "terminal",
command: string(input, "command") ?? "",
output: text,
};
const code = structured ? exitCode(structured) : null;
if (code !== null) terminal.exitCode = code;
return [terminal];
}
case "edit": {
const content = diffsFromInput(name, input);
return content.length ? content : [{ type: "text", text }];
}
default:
return [{ type: "text", text }];
}
}
/// Reads a `TodoWrite` input into a plan.
export function planFromTodos(raw: unknown): Plan | null {
const todos = wire.inputOf(raw).todos;
if (!Array.isArray(todos)) return null;
return {
entries: todos.map((todo) => {
const content =
typeof todo?.content === "string" ? todo.content : typeof todo?.activeForm === "string" ? todo.activeForm : "";
const status =
todo?.status === "in_progress" ? "in_progress" : todo?.status === "completed" ? "completed" : "pending";
return { content, status };
}),
};
}
/// A tool call from the name and input the model produced.
export function toolCall(id: string, name: string, input: unknown, status: ToolCall["status"]): ToolCall {
const call: ToolCall = { id, name, kind: kindOf(name), title: titleOf(name, input), status };
if (input !== null && input !== undefined) call.input = input;
const content = startContent(name, input);
if (content.length) call.content = content;
const locations = locationsOf(input);
if (locations.length) call.locations = locations;
return call;
}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.