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

maintenance.ts4.6 KB
// Which installer owns the `opencode` binary, and how to upgrade it (the
// Rust `maintenance.rs`).
//
// Ownership by evidence: a manager is named only when the resolved path
// proves it installed this binary. Guessing would make Convergence run a
// package manager against somebody else's install, which is worse than
// telling the user to upgrade it themselves.
import * as Effect from "effect/Effect";
import { Process } from "convergence/effect";
import { isNewer, latestNpmEffect } from "../sdk/maintenance.ts";
import type { AgentMaintenance } from "convergence/protocol";

/// The npm package that ships `opencode`.
export const NPM_PACKAGE = "opencode-ai";

export interface Owner {
  manager: string;
  prefix?: string;
}

export interface Located {
  path: string | null;
  realPath: string | null;
  owner: Owner;
}

function normalize(path: unknown): string {
  return String(path ?? "").replace(/\\/g, "/");
}

/// Whether a path is the layout the OpenCode install script creates.
function isNative(path: unknown): boolean {
  const text = normalize(path).toLowerCase();
  return text.endsWith("/.opencode/bin/opencode") || text.endsWith("/.opencode/bin/opencode.exe");
}

/// The global npm prefix that owns `path`, when it owns it: the package's
/// own folder inside a `lib/node_modules` tree. A second `node_modules`
/// before it is a project's dependency, never a global install.
export function npmPrefix(path: unknown): string | null {
  if (!path) return null;
  const slashed = normalize(path);
  const lower = slashed.toLowerCase();
  const segment = `/lib/node_modules/${NPM_PACKAGE}/`;
  const at = lower.lastIndexOf(segment);
  if (at < 0) return null;
  if (lower.slice(0, at).includes("/node_modules/")) return null;
  return at === 0 ? "/" : slashed.slice(0, at);
}

/// The owner of a binary, from the path `PATH` finds and the file it
/// really is: `{ manager: "native" }`, `{ manager: "npm", prefix }` or
/// `{ manager: "" }`. A shim proves as much as its target.
export function ownerOf(path: unknown, realPath: unknown = path): Owner {
  for (const candidate of [path, realPath]) if (candidate && isNative(candidate)) return { manager: "native" };
  const prefix = npmPrefix(realPath ?? path);
  if (prefix) return { manager: "npm", prefix };
  return { manager: "" };
}

/// The program and arguments that upgrade an install, or `null` when
/// nothing owns it provably. A native install upgrades itself; the plugin
/// runs it as `opencode`, the name its grant covers.
export function updateCommand(owner: Owner): { program: string; args: string[] } | null {
  switch (owner.manager) {
    case "native":
      return { program: "opencode", args: ["upgrade"] };
    case "npm":
      return {
        program: "npm",
        args: [
          "install",
          "-g",
          "--prefix",
          (owner as { prefix: string }).prefix,
          // npm no longer runs install scripts by default and still exits
          // 0, so the upgrade would silently do nothing.
          `--allow-scripts=${NPM_PACKAGE}`,
          `${NPM_PACKAGE}@latest`,
        ],
      };
    default:
      return null;
  }
}

/// Where the login PATH finds `opencode` and whose it is.
export const locate = Effect.fn("OpenCode.locate")(function* () {
  const process = yield* Process;
  const found = yield* process
    .which("opencode")
    .pipe(
      Effect.catchTag(["HostCallFailed", "NeedsReview", "PermissionNotGranted"], () =>
        Effect.succeed({ path: null, realPath: null }),
      ),
    );
  const located: Located = {
    path: found.path,
    realPath: found.realPath,
    owner: found.path ? ownerOf(found.path, found.realPath) : { manager: "" },
  };
  return located;
});

/// The maintenance block of `AgentInfo`. Only the installer that owns the
/// binary is asked for the newest version: another one would report an
/// update it cannot deliver.
export const describe = Effect.fn("OpenCode.describe")(function* (installedVersion: string | null) {
  const { owner } = yield* locate();
  let latest: string | null = null;
  if (owner.manager === "npm") {
    const prefix = typeof owner.prefix === "string" ? owner.prefix : "";
    latest = yield* latestNpmEffect(NPM_PACKAGE, prefix).pipe(Effect.scoped);
  } else if (owner.manager === "native") latest = yield* latestNpmEffect(NPM_PACKAGE, null).pipe(Effect.scoped);
  const out: AgentMaintenance = {
    manager: owner.manager,
    canUpdate: owner.manager !== "",
  };
  if (installedVersion) out.installedVersion = installedVersion;
  if (installedVersion && latest && isNewer(installedVersion, latest)) out.latestVersion = latest;
  return out;
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.