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
prompt.test.ts3 KB
import { test } from "node:test";
import assert from "node:assert/strict";
import {
MAX_ATTACHMENT_BYTES,
isSupportedAttachment,
messageIdFor,
plainText,
promptParts,
skillSource,
slashCommand,
} from "./prompt.ts";
test("prompt blocks become text and file parts", () => {
const parts = promptParts([
{ type: "text", text: "look at " },
{ type: "file_ref", path: "/w/src/main.rs" },
{ type: "image", mimeType: "image/png", data: "AAA" },
{ type: "skill", name: "review", input: "the parser" },
{ type: "resource", path: "notes.md", text: "hello" },
]);
assert.deepEqual(parts[0], { type: "text", text: "look at " });
assert.equal(parts[1].url, "file:///w/src/main.rs");
assert.equal(parts[1].filename, "main.rs");
assert.equal(parts[1].mime, "text/plain");
assert.equal(parts[2].url, "data:image/png;base64,AAA");
assert.deepEqual(
parts[3],
{ type: "text", text: "/review the parser" },
"OpenCode runs a skill through its command form",
);
assert.deepEqual(parts[4], { type: "text", text: "notes.md:\nhello" });
});
test("attachments the server would refuse are reported here", () => {
assert.throws(() => promptParts([{ type: "image", mimeType: "application/zip", data: "AAAA" }]), /application\/zip/);
// Just over 20 MB of base64 is just over the limit.
const huge = "A".repeat(Math.floor(MAX_ATTACHMENT_BYTES / 3) * 4 + 8);
assert.throws(() => promptParts([{ type: "image", mimeType: "image/png", data: huge }]), /limit/);
for (const mime of [
"image/png",
"image/jpeg",
"image/gif",
"image/webp",
"application/pdf",
"text/markdown",
"TEXT/Plain",
]) {
assert.ok(isSupportedAttachment(mime), mime);
}
for (const mime of ["application/zip", "image/svg+xml", "application/octet-stream"])
assert.ok(!isSupportedAttachment(mime), mime);
});
test("a host message id names the OpenCode message", () => {
assert.equal(messageIdFor("014be328-8762-4c18"), "msg_014be328-8762-4c18");
assert.equal(messageIdFor("msg_0b294cb240010oW1"), "msg_0b294cb240010oW1");
});
test("a prompt that starts with a known command is that command", () => {
const commands = [{ name: "review" }, { name: "init" }];
assert.deepEqual(slashCommand("/review the parser please", commands), {
name: "review",
arguments: "the parser please",
});
assert.deepEqual(slashCommand("/init", commands), { name: "init", arguments: "" });
assert.equal(slashCommand("/unknown x", commands), null);
assert.equal(slashCommand("review", commands), null);
assert.equal(slashCommand("/", commands), null);
assert.equal(
plainText([
{ type: "text", text: "/review " },
{ type: "file_ref", path: "a.rs" },
]),
"/review @a.rs",
);
});
test("a skill is sourced from where it lives", () => {
assert.equal(skillSource("<built-in>", "/w"), "built-in");
assert.equal(skillSource("/w/.opencode/skills/review/SKILL.md", "/w"), "project");
assert.equal(skillSource("/Users/dev/.config/opencode/skills/tdd/SKILL.md", "/w"), "user");
});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.