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

legacy.ts3.4 KB
// What the Rust plugin kept in `<data dir>/agents/opencode/`: the servers
// (`instances.json`).
//
// The first time this plugin loads in the Rust plugin's place, the host
// moves that folder into the plugin's data folder as `legacy/`
// (`api.paths.data`, read under the `data` scope of `fs.read`). The plugin
// reads it once into its storage: the entries of `instances.json` whose id
// the stored `instances` lack are added to them. The storage key `legacy`
// then says it was read, so the old file is never read again.
import * as Effect from "effect/Effect";
import * as Result from "effect/Result";
import * as z from "zod";
import { Fs, Host } from "convergence/effect";

const MARK = "legacy";
const entry = z.object({ id: z.unknown().optional() });

/// The legacy folder, or `null` when the runtime names no data folder.
export function legacyDir(api: { paths?: { data?: string | null } }): string | null {
  const data = api.paths?.data;
  return typeof data === "string" && data ? `${data.replace(/\/+$/, "")}/legacy` : null;
}

const idOf = (value: unknown): string => {
  const parsed = entry.safeParse(value);
  return parsed.success && typeof parsed.data.id === "string" ? parsed.data.id.trim() : "";
};

/// The stored servers with the legacy ones whose id they lack after them;
/// `null` when that changes nothing.
export function mergeInstances(stored: unknown, legacy: unknown): unknown[] | null {
  if (!Array.isArray(legacy)) return null;
  const current: unknown[] = Array.isArray(stored) ? stored : [];
  const ids = new Set(current.map(idOf));
  const added = legacy.filter((value: unknown) => {
    const id = idOf(value);
    if (!value || typeof value !== "object" || ids.has(id)) return false;
    ids.add(id);
    return true;
  });
  return added.length ? [...current, ...added] : null;
}

/// Reads the legacy file into storage, once. Resolves to whether the
/// stored servers changed; never fails. A read the plugin may not make
/// leaves the mark unset, so a later load tries again.
export const adoptLegacy = Effect.fn("OpenCode.adoptLegacy")(function* (dir: string | null) {
  if (!dir) return false;
  const host = yield* Host;
  const fs = yield* Fs;
  const result = yield* Effect.result(
    Effect.gen(function* () {
      if ((yield* host.call("host/storage.get", { key: MARK })).value) return false;
      // Missing files were optional; a permission refusal leaves the migration unmarked.
      const text = yield* fs
        .read(`${dir}/instances.json`)
        .pipe(Effect.catchTag("HostCallFailed", () => Effect.succeed(null)));
      let changed = false;
      if (text !== null) {
        let legacy: unknown = null;
        try {
          legacy = JSON.parse(text);
        } catch {
          console.warn(`opencode: ${dir}/instances.json is not JSON; it is left out`);
        }
        const merged = mergeInstances(
          (yield* host.call("host/storage.get", { key: "instances" })).value ?? null,
          legacy,
        );
        if (merged) {
          yield* host.call("host/storage.set", { key: "instances", value: merged });
          changed = true;
          console.info("opencode: took the Rust plugin's servers");
        }
      }
      yield* host.call("host/storage.set", { key: MARK, value: { at: Date.now() } });
      return changed;
    }),
  );
  if (Result.isSuccess(result)) return result.success;
  console.warn(`opencode: the Rust plugin's files could not be read: ${result.failure.message}`);
  return false;
});

Versions

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

Reviews and comments

0 threads · 0 reviews

No comments yet.