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
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
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 94.8 KB | 5 permissions | Listed |
No comments yet.