Official
opencode
OpenCode agent provider: runs opencode serve and talks to it over HTTP and server-sent events.
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/opencode@0.2.0
Permissions in 0.2.0
Files
cli.test.ts5.8 KB
import { test } from "node:test";
import assert from "node:assert/strict";
import { inventory, parseAgents, parseModels, parseSkills } from "./cli.ts";
import { fakeApi } from "../sdk/testing.ts";
import { testRuntime } from "./testing.test.ts";
import { obj } from "./map.ts";
/// Captured from `opencode models --verbose` on 1.18.29, shortened to the
/// fields the plugin reads.
const MODELS = `opencode/big-pickle
{
"id": "big-pickle",
"providerID": "opencode",
"name": "Big Pickle",
"limit": {
"context": 200000,
"input": 160000
},
"release_date": "2025-10-17",
"variants": {}
}
openai/gpt-5.1-codex
{
"id": "gpt-5.1-codex",
"providerID": "openai",
"name": "GPT-5.1 Codex",
"limit": {
"context": 400000
},
"release_date": "2026-01-04",
"variants": {
"high": {},
"low": {},
"medium": {}
}
}
openrouter/qwen/qwen3-coder
{
"id": "qwen/qwen3-coder",
"providerID": "openrouter",
"name": "Qwen3 Coder",
"variants": {}
}
`;
test("models are grouped by the provider of their header", () => {
const catalog = parseModels(MODELS);
assert.deepEqual(
catalog.providers.map((provider) => provider.id),
["opencode", "openai", "openrouter"],
);
const openai = catalog.providers[1];
assert.ok(openai);
const raw = openai.models["gpt-5.1-codex"];
assert.ok(raw);
const model = obj(raw);
assert.equal(model["name"], "GPT-5.1 Codex");
assert.equal(obj(model["limit"])["context"], 400000);
assert.equal(model["release_date"], "2026-01-04");
const variants = model["variants"];
assert.deepEqual(Object.keys(variants && typeof variants === "object" ? variants : {}).sort(), [
"high",
"low",
"medium",
]);
// The CLI has no display name for a provider, so the id stands in.
assert.equal(openai.name, "openai");
});
test("a model id with a slash survives, and so does its JSON", () => {
const openrouter = parseModels(MODELS).providers.find((provider) => provider.id === "openrouter");
assert.deepEqual(Object.keys(openrouter?.models ?? {}), ["qwen/qwen3-coder"]);
});
test("single-line model bodies parse too; empty or unreadable output yields no models", () => {
const catalog = parseModels('anthropic/claude\n{"id":"claude","providerID":"anthropic","name":"Claude"}\n');
assert.equal(catalog.providers.length, 1);
assert.ok(catalog.providers[0]?.models["claude"]);
assert.deepEqual(parseModels("").providers, []);
assert.deepEqual(parseModels("opencode/broken\n{not json}\n").providers, []);
});
/// Captured from `opencode agent list` on 1.18.29: a header line, then the
/// agent's permission rules, whose closing bracket sits in column 0.
const AGENTS = `build (primary)
[
{
"permission": "*",
"action": "allow",
"pattern": "*"
}
]
explore (subagent)
[
{
"permission": "read",
"action": "allow",
"pattern": "*"
}
]
code reviewer (subagent)
[]
`;
test("agents come from the header lines only", () => {
const agents = parseAgents(AGENTS);
// A name may contain a space, and the rule bodies are not agents.
assert.deepEqual(
agents.map((agent) => [agent.name, agent.mode]),
[
["build", "primary"],
["explore", "subagent"],
["code reviewer", "subagent"],
],
);
assert.ok(
agents.every((agent) => !agent.hidden),
"the command line does not report hidden agents",
);
});
test("skill entries drop the body, and output cut at a pipe buffer is named as such", () => {
const skills = parseSkills(
JSON.stringify([
{
name: "review",
description: "Review a change.",
location: "/w/.opencode/skills/review/SKILL.md",
content: "---\nname: review\n---\n",
},
{
name: "customize-opencode",
description: "Edit opencode configuration.",
location: "<built-in>",
content: "...",
},
]),
);
assert.deepEqual(skills, [
{ name: "review", description: "Review a change.", location: "/w/.opencode/skills/review/SKILL.md" },
{ name: "customize-opencode", description: "Edit opencode configuration.", location: "<built-in>" },
]);
// Bun flushes exactly one 64 KiB buffer to a pipe: `debug skill` arrives
// as 65536 bytes of a longer document.
const cut = `[${JSON.stringify({ name: "x", content: "y".repeat(70_000) })}]`.slice(0, 65_536);
assert.throws(() => parseSkills(cut), /cut at 65536 bytes/);
assert.throws(() => parseSkills("[{"), /decoding the output/);
});
test("the inventory runs the commands one at a time, in the workspace, and reports an empty catalog", async () => {
const runs: [string, string, string | undefined][] = [];
const api = fakeApi({
onSpawn: (program, args, options) => {
runs.push([program, args.join(" "), options.cwd]);
return undefined;
},
});
const spawn = api.process.spawn;
api.process.spawn = async (program, args = [], options = {}) => {
const child = await spawn(program, args, options);
const peer = api.peers.at(-1);
assert.ok(peer);
queueMicrotask(() => {
peer.stdout.push(args[0] === "models" ? MODELS : AGENTS);
peer.exit(0);
});
return child;
};
const { run } = testRuntime(api);
const found = await run(inventory("opencode", "/w"));
assert.deepEqual(runs, [
["opencode", "models --verbose", "/w"],
["opencode", "agent list", "/w"],
]);
assert.equal(found.providers.providers.length, 3);
assert.equal(found.agents[0]?.name, "build");
api.process.spawn = async (program, args = [], options = {}) => {
const child = await spawn(program, args, options);
const peer = api.peers.at(-1);
assert.ok(peer);
queueMicrotask(() => {
peer.stdout.push("no models here\n");
peer.exit(0);
});
return child;
};
await assert.rejects(run(inventory("opencode", "/w")), /listed no models; run `opencode auth login` first/);
});Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 94.8 KB | 5 permissions | Listed |
No comments yet.