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