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
main.ts5.3 KB
// The OpenCode provider plugin: one agent per OpenCode server, the local
// one this plugin starts (`opencode serve`) and any the user runs
// elsewhere (see agent.ts and NOTES.md).
import * as Effect from "effect/Effect";
import * as Result from "effect/Result";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import * as z from "zod";
import { Agents, Env, Host, Plugin, Settings, runPlugin } from "convergence/effect";
import { errorMessage } from "../sdk/errors.ts";
import { OpenCodeAgent } from "./agent.ts";
import { defaultInstance, displayName, parseInstances } from "./instances.ts";
import type { Instance } from "./instances.ts";
import { adoptLegacy, legacyDir } from "./legacy.ts";
/// The login environment the agents read: `HOME` (to expand `~`), the
/// binary override, the external server's URL, and the server
/// credentials.
export const ENV_VARS = [
"HOME",
"OPENCODE_PATH",
"OPENCODE_SERVER_URL",
"OPENCODE_SERVER_USERNAME",
"OPENCODE_SERVER_PASSWORD",
"CONVERGENCE_OPENCODE_SERVER_PASSWORD",
];
const loginEnv = Effect.fn("OpenCode.loginEnv")(function* () {
const env = yield* Env;
const values: Record<string, string> = {};
for (const name of ENV_VARS) {
const value = yield* env.get(name).pipe(
Effect.catchTag(["HostCallFailed", "PermissionNotGranted", "NeedsReview"], (error) =>
Effect.sync(() => {
console.warn(`opencode: reading ${name} failed: ${error.message}`);
return null;
}),
),
);
if (typeof value === "string" && value) values[name] = value;
}
return values;
});
// The optional collection exposes the servers to lifecycle tests; the entry
// point uses exactly the same program without retaining another copy of state.
export const registerServers = Effect.fn("OpenCode.registerServers")(function* (servers: OpenCodeAgent[] = []) {
const registry = yield* Agents;
const scope = yield* Scope.Scope;
const plugin = yield* Plugin;
const host = yield* Host;
const settings = yield* Settings;
const env = yield* Effect.cached(loginEnv());
const register = Effect.fn("OpenCode.register")(function* (instance: Instance) {
const agent = new OpenCodeAgent({ scope, instance });
const handle = yield* registry.register(agent.definition(env));
agent.emit = handle.emit;
servers.push(agent);
return agent;
});
// The local server's agent, at once: the host lists the agents as soon
// as the plugin has loaded.
const local = yield* register(defaultInstance());
const setup = Effect.gen(function* () {
yield* adoptLegacy(legacyDir(plugin.api));
// The stored entry without an id (its binary or its name) applies to
// the local agent before its first call.
const stored = yield* host.call("host/storage.get", { key: "instances" }).pipe(
Effect.catchTag(["HostCallFailed", "PermissionNotGranted", "NeedsReview"], (error) =>
Effect.sync(() => {
console.warn(`opencode: the stored servers could not be read: ${error.message}`);
return { value: null };
}),
),
);
const [own, ...extra] = parseInstances(stored.value);
const localOwn = own ?? defaultInstance();
// The binary chosen in Settings (the `binaryPath` setting, granted
// through its card) comes before a stored one; without either, the
// `opencode` the login PATH finds, or OPENCODE_PATH.
const binaryPath = yield* settings
.get("binaryPath", z.string().nullish())
.pipe(Effect.catchTag("ParseFailed", () => Effect.succeed(null)));
// An external server chosen in Settings (the `serverUrl` setting,
// whose card grants the connection) comes before a stored one and
// before OPENCODE_SERVER_URL.
const serverUrl = yield* settings
.get("serverUrl", z.string().nullish())
.pipe(Effect.catchTag("ParseFailed", () => Effect.succeed(null)));
local.instance.command = binaryPath ?? localOwn.command;
local.instance.serverUrl = serverUrl ?? localOwn.serverUrl;
if (localOwn.name) {
local.instance.name = localOwn.name;
local.name = displayName(localOwn);
}
// A binary that needs a grant reloads the plugin once the user allows
// it on its card; one the grants cover already applies here, to the
// next launch of the server.
yield* settings.changes.pipe(
Stream.runForEach((change) =>
Effect.sync(() => {
if (change.key === "binaryPath")
local.instance.command = typeof change.value === "string" ? change.value : localOwn.command;
else if (change.key === "serverUrl")
local.instance.serverUrl = typeof change.value === "string" ? change.value : localOwn.serverUrl;
}),
),
Effect.forkIn(scope),
);
// The other servers come from the plugin's storage, which answers only
// once the plugin is connected. The runtime tells the host of the agents
// registered after the load.
for (const instance of extra) yield* register(instance);
});
const outcome = yield* setup.pipe(Effect.result);
if (Result.isFailure(outcome))
yield* Effect.sync(() =>
console.error(`opencode: registering the servers failed: ${errorMessage(outcome.failure)}`),
);
yield* Effect.addFinalizer(() => Effect.forEach(servers, (agent) => agent.shutdown(), { discard: true }));
});
export const activate = runPlugin(registerServers());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.