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

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

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

Reviews and comments

0 threads · 0 reviews

No comments yet.