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
instances.ts4.8 KB
// OpenCode servers, each served as its own Convergence agent.
//
// One machine can reach several OpenCode servers: the local one this
// plugin starts, and any number the user runs elsewhere. They are the
// plugin's stored value `instances` (plugin storage, key `instances`):
//
// [{ "id": "team", "name": "OpenCode (team server)", "serverUrl": "https://opencode.example:4096", "password": "…" }]
//
// Each entry becomes the agent id `opencode:<id>`, beside the default
// `opencode`. An entry without an id configures the local agent itself
// (its `name`, or the `command` it starts). Sessions live on the server,
// so `continuationKey` is the server URL: two agents pointing at one server
// can continue each other's chats, and two pointing at different servers
// cannot.
//
// A local password is never sent to a server the user runs elsewhere: an
// instance carries its own, which is the only credential used for it.
import * as z from "zod";
export interface Instance {
id: string;
name: string | null;
serverUrl: string | null;
password: string | null;
command: string | null;
}
const entrySchema = z.object({
id: z.unknown().optional(),
name: z.unknown().optional(),
serverUrl: z.unknown().optional(),
password: z.unknown().optional(),
command: z.unknown().optional(),
});
/// The local server's agent, which starts its own `opencode serve`.
export function defaultInstance(): Instance {
return { id: "", name: null, serverUrl: null, password: null, command: null };
}
export function agentId(instance: Pick<Instance, "id">): string {
return instance.id ? `opencode:${instance.id}` : "opencode";
}
export function displayName(instance: Pick<Instance, "id" | "name">): string {
if (typeof instance.name === "string" && instance.name.trim()) return instance.name;
return instance.id ? `OpenCode (${instance.id})` : "OpenCode";
}
const text = (value: unknown): string | null => (typeof value === "string" && value.trim() ? value.trim() : null);
/// The server this agent talks to, without a trailing slash; `null` means
/// it starts its own. The default agent still honours
/// `OPENCODE_SERVER_URL`, which is how one external server was configured
/// before instances existed; a named instance never does.
export function serverUrl(
instance: Pick<Instance, "id" | "serverUrl">,
env: Record<string, string> = {},
): string | null {
const own = text(instance.serverUrl);
if (own) return own.replace(/\/+$/, "");
if (instance.id) return null;
const ambient = text(env["OPENCODE_SERVER_URL"]);
return ambient ? ambient.replace(/\/+$/, "") || null : null;
}
/// The binary a local server is started from, when the user chose one:
/// the instance's `command` (`~` is the home folder), else
/// `OPENCODE_PATH` for the default agent. `null` means the `opencode` the
/// login PATH finds.
export function command(instance: Pick<Instance, "id" | "command">, env: Record<string, string> = {}): string | null {
const own = text(instance.command);
if (own) return expand(own, env["HOME"]);
if (instance.id) return null;
return text(env["OPENCODE_PATH"]);
}
function expand(path: string, home?: string): string {
if (path.startsWith("~/") && home) return `${home.replace(/\/+$/, "")}/${path.slice(2)}`;
return path;
}
/// Every configured server, the local one first. Entries with a repeated
/// id are left out (and reported); the entry without an id is merged into
/// the local agent rather than dropped.
export function parseInstances(value: unknown): Instance[] {
const all = [defaultInstance()];
if (value === null || value === undefined) return all;
if (!Array.isArray(value)) {
console.warn("the stored opencode instances are not a list; only the local server is served");
return all;
}
for (const raw of value) {
const parsed = entrySchema.safeParse(raw);
const entry = parsed.success ? parsed.data : {};
const id = typeof entry?.id === "string" ? entry.id.trim() : "";
const described: Instance = {
id,
name: typeof entry.name === "string" ? entry.name : null,
serverUrl: typeof entry.serverUrl === "string" ? entry.serverUrl : null,
password: typeof entry.password === "string" && entry.password ? entry.password : null,
command: typeof entry.command === "string" ? entry.command : null,
};
if (!id) {
// The local agent is defined by starting its own server, so the
// unnamed entry may name its binary or its name, never a URL.
const local = all[0];
if (local) {
local.command = described.command ?? local.command;
local.name = described.name ?? local.name;
}
continue;
}
if (all.some((known) => known.id === id)) {
console.warn(`ignoring an opencode instance with a repeated id: ${id}`);
continue;
}
all.push(described);
}
return all;
}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.