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

  • Read files fs.readMediumReads files in the listed places.Read, once, the servers the previous OpenCode provider keptPlaces: its own data folder
  • Provide agents agents.provideMediumAdds agents to the app.Provide the OpenCode agent, and serve plugin tools to it through the host's loopback MCP server
  • Run named programs processMediumStarts the listed programs.Run the OpenCode server (`opencode serve`, or the binary you choose in Settings), read its catalog from the command line when the server cannot answer, upgrade it (`opencode upgrade`), and ask or tell the npm installation that owns it about a newer versionPrograms: opencodenpm${settings.binaryPath}
  • Network access netMediumConnects to the listed hosts.Talk to the OpenCode server it starts on this computer, at the port it picks for each launch, and to the external server you choose in SettingsHosts: localhost:*${settings.serverUrl}
  • Environment variables envMediumReads the listed environment variables.Expand ~ in a configured binary, and read the OpenCode settings you set in the environment: the binary to run, an external server's address, and the server's user name and passwordVariables: HOMEOPENCODE_PATHOPENCODE_SERVER_URLOPENCODE_SERVER_USERNAMEOPENCODE_SERVER_PASSWORDCONVERGENCE_OPENCODE_SERVER_PASSWORD

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

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <394.8 KB5 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.