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
terminal.test.ts6 KB
// Ported from the Rust plugin's terminal.rs tests: the command is a
// scripted fake child behind `api.process.spawn`.
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Scope from "effect/Scope";
import * as ManagedRuntime from "effect/ManagedRuntime";
import { liveLayer } from "convergence/effect";
import type { Api } from "convergence";
import { afterEach } from "node:test";
import { test } from "node:test";
import assert from "node:assert/strict";
import { fakeApi, settle } from "../sdk/testing.ts";
import { OutputBuffer, Terminals } from "./terminal.ts";
const cleanups: (() => Promise<void>)[] = [];
afterEach(async () => {
for (const close of cleanups.splice(0)) await close();
});
function terminalsFor(api: Api) {
const scope = Scope.makeUnsafe();
const runtime: ManagedRuntime.ManagedRuntime<import("convergence/effect").PluginServices, never> =
ManagedRuntime.make(
liveLayer(
api,
(effect) => runtime.runPromise(effect),
() => {},
),
);
const terminals = new Terminals(scope);
cleanups.push(async () => {
await runtime.runPromise(terminals.releaseAll());
await runtime.runPromise(Scope.close(scope, Exit.void));
await runtime.dispose();
});
return {
create: (input: Parameters<Terminals["create"]>[0]) => runtime.runPromise(terminals.create(input)),
wait: (id: string) => runtime.runPromise(terminals.wait(id)),
kill: (id: string) => runtime.runPromise(terminals.kill(id)),
release: (id: string) => runtime.runPromise(terminals.release(id)),
releaseSession: (id: string) => runtime.runPromise(terminals.releaseSession(id)),
output: (id: string) => terminals.output(id),
views: (id: string) => terminals.views(id),
};
}
function request(command: string, args: string[], extra = {}) {
return { sessionId: "s1", command, args, ...extra };
}
test("a terminal captures output and its exit code", async () => {
const api = fakeApi({ onSpawn: () => () => {} });
const terminals = terminalsFor(api);
const id = await terminals.create(
request("sh", ["-c", "echo hello; echo oops 1>&2; exit 3"], { env: [{ name: "A", value: "1" }], cwd: "/w" }),
);
const peer = api.peers[0];
assert.equal(peer.program, "sh");
assert.deepEqual(peer.options, { cwd: "/w", env: { A: "1" } });
assert.ok(peer.stdinClosed, "a terminal command gets no input");
peer.stdout.push("hello\n");
peer.stderr.push("oops\n");
await settle();
const running = terminals.output(id);
assert.equal(running.exit, null);
peer.exit(3);
const exit = await terminals.wait(id);
assert.equal(exit.code, 3);
const { output, truncated, exit: status } = terminals.output(id);
assert.match(output, /hello/);
assert.match(output, /oops/);
assert.equal(truncated, false);
assert.ok(status);
assert.equal(status.code, 3);
const views = terminals.views("s1");
const view = views.get(id);
assert.ok(view);
assert.equal(view.command, "sh -c echo hello; echo oops 1>&2; exit 3");
assert.equal(view.exitCode, 3);
assert.equal(terminals.views("other").size, 0);
await terminals.release(id);
assert.throws(() => terminals.output(id), /unknown terminal/);
});
test("killing and releasing a running command stops it, and a wait learns it", async () => {
const api = fakeApi({ onSpawn: () => () => {} });
const terminals = terminalsFor(api);
const id = await terminals.create(request("sleep", ["100"]));
const waiting = terminals.wait(id);
await terminals.kill(id);
assert.deepEqual(api.peers[0].kills, ["SIGKILL"]);
const exit = await waiting;
assert.equal(exit.signal, "SIGKILL");
const other = await terminals.create(request("sleep", ["100"]));
api.peers[1].exit = () => {}; // a command that ignores the kill
const pending = terminals.wait(other);
await terminals.release(other);
await assert.rejects(pending, /released before it exited/);
});
test("a session's terminals are released with it", async () => {
const api = fakeApi({ onSpawn: () => () => {} });
const terminals = terminalsFor(api);
const mine = await terminals.create(request("a", []));
const theirs = await terminals.create({ ...request("b", []), sessionId: "s2" });
await terminals.releaseSession("s1");
assert.throws(() => terminals.output(mine));
assert.doesNotThrow(() => terminals.output(theirs));
});
test("a spawn the host refuses names the command", async () => {
const api = fakeApi({ onSpawn: () => new Error("PermissionNotGranted") });
await assert.rejects(terminalsFor(api).create(request("rm", ["-rf"])), /could not run rm/);
});
test("the buffer keeps the most recent bytes", () => {
const buffer = new OutputBuffer(4);
buffer.push("0123456789");
assert.equal(buffer.text, "6789");
assert.equal(buffer.truncated, true);
// A character is never split: `é` is two bytes.
const accents = new OutputBuffer(3);
accents.push("aéé");
assert.equal(accents.text, "é");
});
test("terminal inputs retain coercion and skip malformed environment entries individually", async () => {
const api = fakeApi({ onSpawn: () => () => {} });
const terminals = terminalsFor(api);
const id = await terminals.create({
sessionId: "s1",
command: "sh",
args: [1, true],
env: [null, { name: "A", value: 2 }, "bad", { name: "B" }],
outputByteLimit: null,
});
assert.deepEqual(api.peers[0].args, ["1", "true"]);
assert.deepEqual(api.peers[0].options.env, { A: "2", B: "" });
api.peers[0].stdout.push("hello");
await settle();
assert.equal(terminals.output(id).output, "hello");
});
test("creating a terminal does not wait for its input to close", { timeout: 1000 }, async () => {
const api = fakeApi({ onSpawn: () => () => {} });
const spawn = api.process.spawn;
api.process.spawn = async (...args) => {
const child = await spawn(...args);
child.stdin.close = () => new Promise<never>(() => {});
return child;
};
const terminals = terminalsFor(api);
const id = await terminals.create(request("sh", []));
assert.equal(terminals.output(id).output, "");
await terminals.release(id);
});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.