Official
acp
ACP agents from the ACP registry (Gemini, Cursor, Droid, Kilo, pi, ...), Oh My Pi, and your own entries (custom.json in the plugin's data folder). Agents are discovered at runtime.
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/acp@0.2.0
Permissions in 0.2.0
Take care. This plugin asks for permissions that can do anything your account can. The app asks you to hold Enable for two seconds or to type the plugin's name before it turns on.
Files
quirks.test.ts12.1 KB
// Ported from the Rust plugin's quirks.rs tests.
import { test } from "node:test";
import assert from "node:assert/strict";
import { permissionMode } from "../sdk/agent.ts";
import { Dialect, Quirks, answer, catalogState, daemonFormat, proposedPlan, question, todos } from "./quirks.ts";
test("an unknown agent gets the spec behaviour", () => {
const plain = Quirks.forAgent("gemini");
assert.equal(plain.reviewer(), false);
assert.equal(plain.terminal(), true);
assert.deepEqual(plain.spawnArgs(["--acp"], permissionMode.FULL), ["--acp"]);
assert.equal(plain.authMethod({}), null);
assert.equal(plain.extension("cursor/ask_question"), null);
assert.equal(plain.diagnostic("Open the following link to authenticate the ACP server: https://x"), null);
assert.deepEqual(plain.clientCapabilities({}), {});
assert.equal(plain.modelCatalog(), null);
assert.equal(plain.promptMeta("p1"), null);
assert.equal(plain.subagents(), false);
});
test("only Cursor asks for the parameterized model picker", () => {
assert.deepEqual(Quirks.forAgent("cursor").clientCapabilities({ fs: {} }), {
fs: {},
_meta: { parameterizedModelPicker: true },
});
assert.deepEqual(Quirks.forAgent("gemini").clientCapabilities({ fs: {} }), { fs: {} });
});
// Both agents take the flag as an argument of a parent subcommand, so
// appending it makes the process refuse to start.
test("permission flags go where the CLI expects them", () => {
const cursor = Quirks.forAgent("cursor");
assert.deepEqual(cursor.spawnArgs(["acp"], permissionMode.AUTO), ["--auto-review", "acp"]);
assert.deepEqual(cursor.spawnArgs(["acp"], permissionMode.FULL), ["--force", "acp"]);
assert.deepEqual(cursor.spawnArgs(["acp"], permissionMode.SUPERVISED), ["acp"]);
const grok = Quirks.forAgent("grok-build");
assert.deepEqual(grok.spawnArgs(["agent", "stdio"], permissionMode.AUTO_EDITS), [
"--permission-mode",
"acceptEdits",
"agent",
"stdio",
]);
assert.deepEqual(grok.spawnArgs(["agent", "stdio"], permissionMode.FULL), ["agent", "--always-approve", "stdio"]);
// A launch spelled another way gets the flag at the end.
assert.deepEqual(cursor.spawnArgs(["serve"], permissionMode.FULL), ["serve", "--force"]);
});
// Droid's plain `acp` format broadcasts every session's updates to all the
// sessions of its process, so chats would see each other's turns.
test("Factory Droid always runs in the acp-daemon output format", () => {
const droid = Quirks.forAgent("factory-droid");
const registry = ["exec", "--output-format", "acp-daemon"];
assert.deepEqual(droid.spawnArgs(registry, permissionMode.FULL), registry, "the registry's own launch is kept");
assert.deepEqual(droid.spawnArgs(["exec", "--output-format", "acp"], permissionMode.SUPERVISED), registry);
assert.deepEqual(daemonFormat(["exec", "--output-format=acp"]), ["exec", "--output-format=acp-daemon"]);
// No other agent is touched.
assert.deepEqual(Quirks.forAgent("gemini").spawnArgs(["--output-format", "acp"], permissionMode.SUPERVISED), [
"--output-format",
"acp",
]);
});
test("Grok signs in with the key when there is one", () => {
const grok = Quirks.forAgent("grok-build");
assert.deepEqual(grok.authMethod({}), { id: "cached_token", interactive: false });
assert.deepEqual(grok.authMethod({ XAI_API_KEY: "xai-abc" }), { id: "xai.api_key", interactive: false });
// An empty value is not a key.
assert.deepEqual(grok.authMethod({ XAI_API_KEY: " " }), { id: "cached_token", interactive: false });
// A browser login is offered, never started on its own.
assert.deepEqual(Quirks.forAgent("cursor").authMethod({}), { id: "cursor_login", interactive: true });
});
test("Antigravity maps the permission mode onto its own mode id", () => {
const agy = Quirks.forAgent("antigravity-acp");
assert.equal(agy.permissionModeId(permissionMode.FULL), "yolo");
assert.equal(agy.permissionModeId(permissionMode.AUTO_EDITS), "auto_edit");
assert.equal(agy.permissionModeId(permissionMode.SUPERVISED), "default");
assert.equal(Quirks.forAgent("cursor").permissionModeId(permissionMode.FULL), null);
});
test("agents that run their own commands are offered no terminal", () => {
for (const id of ["cursor", "grok-build", "antigravity-acp"]) assert.equal(Quirks.forAgent(id).terminal(), false, id);
assert.equal(Quirks.forAgent("factory-droid").terminal(), true);
});
test("the sign-in line is recognised", () => {
const agy = Quirks.forAgent("antigravity-acp");
const line =
"Open the following link to authenticate the ACP server: https://accounts.google.com/o/oauth2/v2/auth?x=1";
assert.deepEqual(agy.diagnostic(line), { signIn: "https://accounts.google.com/o/oauth2/v2/auth?x=1" });
assert.deepEqual(agy.diagnostic(` ${line} `), { signIn: "https://accounts.google.com/o/oauth2/v2/auth?x=1" });
// Anything that is not a Google sign-in URL is left alone.
assert.equal(agy.diagnostic("Open the following link to authenticate the ACP server: http://evil.example"), null);
assert.equal(agy.diagnostic("starting up"), null);
});
test("Antigravity's shell calls are classified from their input", () => {
const agy = Quirks.forAgent("antigravity-acp");
assert.equal(agy.sessionUpdate({ sessionUpdate: "tool_call", rawInput: { CommandLine: "ls" } }).kind, "execute");
assert.equal(
agy.sessionUpdate({ sessionUpdate: "tool_call", kind: "read", rawInput: { command: "ls" } }).kind,
"read",
"a kind the agent sent wins",
);
assert.equal(agy.sessionUpdate({ sessionUpdate: "tool_call", rawInput: { path: "a" } }).kind, undefined);
assert.equal(Quirks.forAgent("gemini").sessionUpdate({ rawInput: { command: "ls" } }).kind, undefined);
});
test("only an unclassified start_subagent call is a task", () => {
const agy = Quirks.forAgent("antigravity-acp");
const task = agy.subagent({ toolCallId: "t1", title: "Running start_subagent" });
assert.deepEqual(task, { id: "t1", title: "Subagent batch", status: "running", toolCallId: "t1" });
assert.equal(agy.subagent({ toolCallId: "t2", title: "Run command" }), null);
assert.equal(
agy.subagent({ toolCallId: "t3", title: "Running start_subagent", kind: "execute" }),
null,
"a classified call is whatever the kind says",
);
assert.equal(
agy.subagent({ toolCallId: "t4", title: "Running start_subagent", _meta: { is_mcp_tool_call: true } }),
null,
"an MCP call wears the same title",
);
assert.equal(Quirks.forAgent("cursor").subagent({ toolCallId: "t5", title: "Running start_subagent" }), null);
assert.equal(agy.subagents(), true);
});
test("a choice request is a question, not an approval", () => {
const agy = Quirks.forAgent("antigravity-acp");
assert.equal(
agy.permissionIsQuestion({ sessionId: "s", options: [], toolCall: { toolCallId: "interaction_7" } }),
true,
);
assert.equal(agy.permissionIsQuestion({ sessionId: "s", options: [], toolCall: { toolCallId: "call_7" } }), false);
assert.equal(
agy.permissionIsQuestion({ sessionId: "s", options: [], _meta: { isAntigravityUserInputRequest: true } }),
true,
);
assert.equal(Quirks.forAgent("gemini").permissionIsQuestion({ toolCall: { toolCallId: "interaction_7" } }), false);
});
test("Antigravity's namespaced option warning is read", () => {
const agy = Quirks.forAgent("antigravity-acp");
assert.equal(
agy.optionWarning({
optionId: "a",
_meta: { "agy.security.warning": { title: "Careful", message: " Runs as root " } },
}),
"Runs as root",
);
assert.equal(agy.optionWarning({ optionId: "a" }), null);
assert.equal(Quirks.forAgent("cursor").optionWarning({ _meta: { "agy.security.warning": { message: "x" } } }), null);
});
test("extension methods are recognised per agent", () => {
const cursor = Quirks.forAgent("cursor");
assert.deepEqual(cursor.extension("cursor/ask_question"), { kind: "ask_question", dialect: Dialect.CURSOR });
assert.deepEqual(cursor.extension("cursor/create_plan"), { kind: "propose_plan", dialect: Dialect.CURSOR });
assert.deepEqual(cursor.extension("cursor/update_todos"), { kind: "update_todos" });
const grok = Quirks.forAgent("grok-build");
assert.deepEqual(grok.extension("x.ai/ask_user_question"), { kind: "ask_question", dialect: Dialect.XAI });
assert.deepEqual(grok.extension("_x.ai/ask_user_question"), { kind: "ask_question", dialect: Dialect.XAI });
assert.deepEqual(grok.extension("_x.ai/exit_plan_mode"), { kind: "propose_plan", dialect: Dialect.XAI });
assert.deepEqual(grok.extension("_x.ai/session/prompt_complete"), { kind: "prompt_complete" });
assert.equal(grok.extension("cursor/ask_question"), null);
assert.deepEqual(grok.promptMeta("p1"), { promptId: "p1", requestId: "p1" });
assert.equal(cursor.modelCatalog(), "cursor/list_available_models");
assert.equal(cursor.modelIsConfigOption(), true);
});
test("a model catalogue becomes the model state session/new lacked", () => {
const catalog = {
models: [
{ value: " composer-2.5 ", name: "Composer 2.5" },
{ value: "", name: "Blank" },
{ value: "gpt-5", name: "GPT-5" },
],
};
assert.deepEqual(catalogState(catalog, ""), {
currentModelId: "composer-2.5",
availableModels: [
{ modelId: "composer-2.5", name: "Composer 2.5" },
{ modelId: "gpt-5", name: "GPT-5" },
],
});
const selected = catalogState(catalog, "gpt-5");
assert.ok(selected);
assert.equal(selected.currentModelId, "gpt-5");
assert.equal(catalogState({ models: [] }, ""), null);
});
test("both ask_question dialects become one question", () => {
const cursor = question(Dialect.CURSOR, "q1", {
toolCallId: "t1",
title: "Pick a branch",
questions: [{ id: "branch", prompt: "Which branch?", options: [{ id: "main", label: "Main" }] }],
});
assert.ok(cursor);
assert.ok(cursor.question.fields[0]?.options?.[0]);
assert.equal(cursor.question.id, "q1");
assert.equal(cursor.question.message, "Pick a branch");
assert.equal(cursor.question.fields[0].id, "branch");
assert.equal(cursor.question.fields[0].label, "Which branch?");
assert.equal(cursor.question.fields[0].kind, "select");
assert.equal(cursor.question.fields[0].options[0].value, "main");
assert.deepEqual(answer(cursor.shape, { values: { branch: "main" }, cancelled: false }), {
answers: { branch: "main" },
});
const xai = question(Dialect.XAI, "q2", {
sessionId: "s",
toolCallId: "t2",
mode: "default",
questions: [{ question: "Which branch?", multiSelect: true, options: [{ label: "Main" }, { label: "Dev" }] }],
});
assert.ok(xai);
assert.equal(xai.question.fields[0].id, "Which branch?", "xAI keys its answers by the question text");
assert.equal(xai.question.fields[0].kind, "multi_select");
assert.deepEqual(answer(xai.shape, { values: { "Which branch?": ["Main"] }, cancelled: false }), {
outcome: "accepted",
answers: { "Which branch?": ["Main"] },
});
assert.deepEqual(answer(xai.shape, { values: {}, cancelled: true }), { outcome: "cancelled" });
assert.equal(question(Dialect.CURSOR, "q3", { questions: [] }), null);
});
test("a proposed plan becomes plan entries and an immediate reply", () => {
const cursor = proposedPlan(Dialect.CURSOR, {
toolCallId: "t1",
plan: "# Plan",
todos: [{ content: "Read the code", status: "in_progress" }],
phases: [{ name: "Later", todos: [{ title: "Ship it" }] }],
});
assert.equal(cursor.plan.entries.length, 2);
assert.equal(cursor.plan.entries[0].status, "in_progress");
assert.equal(cursor.plan.entries[1].content, "Ship it");
assert.deepEqual(cursor.reply, { accepted: true });
// Prose-only plans keep their markdown rather than showing nothing.
const xai = proposedPlan(Dialect.XAI, { planContent: " # Steps\n1. Do it " });
assert.equal(xai.plan.entries[0].content, "# Steps\n1. Do it");
assert.equal(xai.reply.outcome, "abandoned");
});
test("todos replace the plan", () => {
const replaced = todos({
toolCallId: "t1",
merge: true,
todos: [{ content: " ", title: "Fallback" }, { content: "Done", status: "completed" }, { title: " " }],
});
assert.equal(replaced.entries.length, 2, "an entry with no text at all is dropped");
assert.equal(replaced.entries[0].content, "Fallback");
assert.equal(replaced.entries[1].status, "completed");
});Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 94.9 KB | 6 permissions | Listed |
No comments yet.