Official
opencode-v2
OpenCode 2 agent provider: runs opencode serve (2.x) and talks to its /api 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-v2@0.1.0
Permissions in 0.1.0
Files
prompt.ts3.9 KB
// What the plugin sends OpenCode for a prompt: one text with the file and
// skill attachments it mentions (`POST /api/session/{id}/prompt`), or the
// slash command the text invokes (`POST /api/session/{id}/command`).
import type { ContentBlock } from "convergence/protocol";
export interface Mention {
start: number;
end: number;
text: string;
}
export interface FileAttachment {
uri: string;
name?: string;
mention?: Mention;
}
export interface SkillAttachment {
id: string;
mention?: Mention;
}
export interface PromptBody {
text: string;
files?: FileAttachment[];
skills?: SkillAttachment[];
}
/// The largest attachment a model behind OpenCode accepts.
export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
/// The decoded size of base64 data, without decoding it.
export function base64Size(data: string): number {
let padding = 0;
for (let at = data.length - 1; at >= 0 && data[at] === "=" && padding < 2; at -= 1) padding += 1;
return Math.floor(data.length / 4) * 3 - padding;
}
/// An inline attachment as a `data:` URI. One the model would refuse is
/// reported here rather than in a turn that dies on the far side.
function inline(what: string, mimeType: string, data: string): FileAttachment {
const size = base64Size(data);
if (size > MAX_ATTACHMENT_BYTES)
throw new Error(
`OpenCode cannot attach ${what}: it is ${Math.floor(size / 1_000_000)} MB, over the ${MAX_ATTACHMENT_BYTES / 1_000_000} MB limit`,
);
return { uri: `data:${mimeType};base64,${data}`, name: what };
}
/// `file:///a%20b/c.ts` for an absolute path.
export function fileUri(path: string): string {
return `file://${path.split("/").map(encodeURIComponent).join("/")}`;
}
/// The prompt body for the host's blocks. A mention (`@path`, `$skill`)
/// stays in the text where the user wrote it, and its attachment names that
/// span, which is how OpenCode links the two.
export function promptBody(blocks: readonly ContentBlock[]): PromptBody {
let text = "";
const files: FileAttachment[] = [];
const skills: SkillAttachment[] = [];
const mention = (written: string): Mention => {
const start = text.length;
text += written;
return { start, end: text.length, text: written };
};
for (const block of blocks) {
switch (block.type) {
case "text":
text += block.text;
break;
case "image":
files.push(inline("image", block.mimeType, block.data));
break;
case "audio":
files.push(inline("audio", block.mimeType, block.data));
break;
// The server reads the file itself: this plugin has no grant to.
case "file_ref":
files.push({
uri: fileUri(block.path),
name: block.path.split("/").pop() || block.path,
mention: mention(`@${block.path}`),
});
break;
case "resource":
text += `${block.path}:\n${block.text}`;
break;
case "skill": {
skills.push({ id: block.name, mention: mention(`$${block.name}`) });
if (block.input) text += ` ${block.input}`;
break;
}
default:
break;
}
}
return { text, ...(files.length ? { files } : {}), ...(skills.length ? { skills } : {}) };
}
/// The command a prompt invokes, when its text starts with `/name` and
/// `name` is one the server lists: `{ name, text }` with the rest as the
/// command's input.
export function slashCommand(
body: PromptBody,
commands: readonly { name: string }[],
): { name: string; text: string } | null {
const match = /^\/(\S+)(?:\s+([\s\S]*))?$/.exec(body.text);
const name = match?.[1];
if (!name || !commands.some((command) => command.name === name)) return null;
return { name, text: match[2] ?? "" };
}
/// The OpenCode message id a host item becomes: the server accepts an id
/// that starts with `msg_`, so the host's own id needs no table.
export const messageIdFor = (hostItemId: string) => (hostItemId.startsWith("msg_") ? hostItemId : `msg_${hostItemId}`);Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.1.0latest | Oct 5, 2026 | >=2 <3 | 45.1 KB | 4 permissions | Listed |
No comments yet.