@cotal-ai/connector-opencode 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export declare const cotal: Plugin;
3
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,MAAM,EAAS,MAAM,qBAAqB,CAAC;AAazD,eAAO,MAAM,KAAK,EAAE,MA+NnB,CAAC"}
package/dist/plugin.js ADDED
@@ -0,0 +1,278 @@
1
+ /**
2
+ * The Cotal OpenCode plugin — loaded in-process by `opencode serve` (via the inline config the
3
+ * connector sets). The serve shim attaches a foreground `opencode` TUI to the session this plugin
4
+ * owns, so the human watches (and can type into) the exact session the agent drives. It turns the
5
+ * session into a first-class mesh peer, at parity with the Claude Code connector:
6
+ *
7
+ * • holds the {@link MeshAgent} (NATS endpoint, inbox, presence) for the server's lifetime;
8
+ * • registers the cotal_* tools natively, rendered from the SHARED {@link cotalToolSpecs}
9
+ * (`./tools.ts`) — same surface as Claude/Codex, incl. channels / join / leave / channel_info;
10
+ * • maps OpenCode bus events to presence (idle | working | waiting | offline);
11
+ * • owns ONE session (created at boot) and drives it: it injects each inbox batch as a turn via the
12
+ * prompt API (`session.promptAsync` — server-side, so it can't race the TUI input box; the
13
+ * attached TUI renders it live), acking ON TURN COMPLETION (so a crash/error redelivers).
14
+ * Delivery is **attention-aware** (open/dnd/focus) and never interrupts a running turn — a
15
+ * message that arrives mid-turn waits for the turn to end (matching Claude's no-interrupt
16
+ * behavior), then drives.
17
+ *
18
+ * Identity comes from COTAL_* env (the plugin runs in the opencode process and inherits it).
19
+ * No identity → inert, so an operator's own `opencode` never joins as a stray peer.
20
+ */
21
+ import { loadAgentFile } from "@cotal-ai/core";
22
+ import { configFromEnv, hasIdentity, MeshAgent, formatInjection, fmtFrom, } from "@cotal-ai/connector-core";
23
+ import { buildCotalTools } from "./tools.js";
24
+ function log(msg) {
25
+ process.stderr.write(`[cotal-connector] ${msg}\n`);
26
+ }
27
+ /** Process-global guard: opencode loads the plugin once per app/worktree scope, so the function
28
+ * can run more than once in a single process. We want exactly one mesh endpoint — so the first
29
+ * call wires up the agent, and every call returns the *same* hooks (the same tools, bound to that
30
+ * one agent), whichever scope opencode ends up using. */
31
+ const guard = globalThis;
32
+ export const cotal = async ({ client }) => {
33
+ // No identity → a plain `opencode`, not a launcher-spawned agent. Stay inert.
34
+ if (!hasIdentity()) {
35
+ log("no COTAL_NAME — not a managed session; staying off the mesh");
36
+ return {};
37
+ }
38
+ if (guard.__cotalOpencodeHooks)
39
+ return guard.__cotalOpencodeHooks; // one agent; reuse the hooks
40
+ const config = configFromEnv();
41
+ const agent = new MeshAgent(config);
42
+ agent.start(); // background connect with retry — never blocks startup
43
+ const def = process.env.COTAL_AGENT_FILE?.trim() ? loadAgentFile(process.env.COTAL_AGENT_FILE.trim()) : undefined;
44
+ const persona = def?.persona;
45
+ // This agent OWNS one session, created at boot. The serve shim attaches the foreground TUI to it
46
+ // (`opencode attach --session <id>`), so what the human watches IS the session we drive — no
47
+ // phantom home-screen, no stale-session guessing. Used to match our turn-end (idle) vs subagent idles.
48
+ let sessionID;
49
+ let busy = false; // a turn is running → don't prompt: opencode would COALESCE onto it (no reject)
50
+ let driving = false; // re-entrancy guard around an in-flight promptAsync
51
+ let primed = false; // persona is prepended to the first turn's text once
52
+ let briefed = false; // the boot channel briefing is prepended once, on the first turn
53
+ let surfaced = []; // ids surfaced into the current turn, acked on completion (by id, not count)
54
+ let awaitingTurnEnd = false; // a turn is in flight → ignore a duplicate idle that isn't its end
55
+ const safeStatus = async (status, activity) => {
56
+ try {
57
+ if (agent.connected)
58
+ await agent.setStatus(status, activity);
59
+ }
60
+ catch {
61
+ /* presence is best-effort — never throw into opencode */
62
+ }
63
+ };
64
+ /** Create the session this agent owns and announce its id to the serve shim, which attaches the
65
+ * foreground TUI to it. The handshake line on stderr (`[cotal-session] <id>`) is how the shim
66
+ * learns *which* session to open — by exact id, so a stale same-titled session from a prior run
67
+ * can't be picked. Awaited by ensureSession before the first drive. */
68
+ const sessionReady = (async () => {
69
+ try {
70
+ const res = await client.session.create({ body: { title: `cotal:${config.space}:${config.name}` } });
71
+ sessionID = res.data?.id;
72
+ if (sessionID)
73
+ process.stderr.write(`[cotal-session] ${sessionID}\n`);
74
+ else
75
+ log("session.create returned no id");
76
+ }
77
+ catch (e) {
78
+ log(`session.create failed: ${e.message}`);
79
+ }
80
+ return sessionID;
81
+ })();
82
+ /** The session to drive — the one we created and the TUI is attached to. */
83
+ async function ensureSession() {
84
+ return sessionID ?? (await sessionReady);
85
+ }
86
+ /** Drive a turn carrying the current inbox batch (and the boot briefing once) into the visible
87
+ * session via the prompt API — server-side, so it can't race like the TUI input box, and the TUI
88
+ * renders it live (it subscribes to that session's events). Surfaces the items but does NOT ack
89
+ * them — ackSurfaced runs on turn completion, so a crash/error redelivers. `override` replaces
90
+ * the body (a bare nudge, e.g. a focus @mention pull) and surfaces nothing to ack. Self-guards
91
+ * re-entrancy and never prompts into a running turn (opencode would COALESCE onto it). */
92
+ async function drive(override) {
93
+ if (driving || busy)
94
+ return;
95
+ driving = true;
96
+ try {
97
+ const id = await ensureSession();
98
+ if (!id)
99
+ return; // no visible session yet — retry on the next event/wake
100
+ const parts = [];
101
+ let ids = [];
102
+ if (override) {
103
+ parts.push({ type: "text", text: override });
104
+ }
105
+ else {
106
+ const items = agent.peekInbox();
107
+ if (items.length === 0)
108
+ return;
109
+ ids = items.map((i) => i.id);
110
+ const inj = formatInjection(items);
111
+ if (inj)
112
+ parts.push({ type: "text", text: inj });
113
+ }
114
+ if (!briefed) {
115
+ briefed = true;
116
+ const brief = agent.channelBriefing();
117
+ if (brief)
118
+ parts.unshift({ type: "text", text: brief });
119
+ }
120
+ if (parts.length === 0)
121
+ return;
122
+ const body = { parts };
123
+ if (!primed && persona)
124
+ body.system = persona; // persona once, as system (no --append-system-prompt)
125
+ busy = true;
126
+ surfaced = ids;
127
+ // Arm BEFORE the await: a turn-end signal can land before promptAsync resolves, and
128
+ // completeTurn bails unless armed — arming after would drop it and wedge the agent.
129
+ awaitingTurnEnd = true;
130
+ await client.session.promptAsync({ path: { id }, body });
131
+ primed = true;
132
+ }
133
+ catch (e) {
134
+ busy = false;
135
+ surfaced = [];
136
+ awaitingTurnEnd = false;
137
+ log(`drive failed: ${e.message}`);
138
+ }
139
+ finally {
140
+ driving = false;
141
+ }
142
+ }
143
+ /** Ack the surfaced batch — but only the leading run STILL at the front of the inbox, matched by
144
+ * id. MeshAgent evicts from the FRONT at MAX_INBOX, so a long turn on a chatty channel can shift
145
+ * our surfaced prefix out; matching by id (not a raw count) means we never ack the wrong, newer
146
+ * messages. Evicted items were already acked by the overflow; any surfaced survivor that no
147
+ * longer leads is left unacked → redelivered (re-answered, never lost). */
148
+ function ackSurfaced() {
149
+ if (surfaced.length === 0)
150
+ return;
151
+ const front = agent.peekInbox();
152
+ let n = 0;
153
+ while (n < surfaced.length && n < front.length && front[n].id === surfaced[n])
154
+ n++;
155
+ if (n > 0)
156
+ agent.drainInbox(n);
157
+ surfaced = [];
158
+ }
159
+ /** A turn ended (the sole ack site). Ignore a stray/duplicate idle that isn't our turn's end. Ack
160
+ * what the turn consumed, then drive the next batch — mode-aware, so bare ambient (dnd/focus)
161
+ * doesn't self-wake a turn (it rides the next directed turn or a human turn). */
162
+ function completeTurn() {
163
+ if (!awaitingTurnEnd)
164
+ return;
165
+ awaitingTurnEnd = false;
166
+ busy = false;
167
+ ackSurfaced();
168
+ const pending = agent.attention === "open" ? agent.inboxCount() : agent.directedPendingCount();
169
+ if (pending > 0)
170
+ void drive();
171
+ }
172
+ // Inbound mesh → drive (never interrupt a running turn — matches Claude). A directed message
173
+ // (DM / anycast / @mention) drives when idle; ambient channel chatter drives only in `open` while
174
+ // idle (dnd/focus hold it for the next turn). In `focus`, ambient/@mentions never reach "incoming"
175
+ // (acked-and-dropped at ingest); a focus @mention wakes us to PULL via "mention-wake" below.
176
+ agent.on("incoming", (item) => {
177
+ if (busy)
178
+ return; // buffer; completeTurn drives at turn end
179
+ const directed = item.kind !== "channel" || item.mentionsMe;
180
+ if (directed || agent.attention === "open")
181
+ void drive();
182
+ });
183
+ agent.on("mention-wake", (item) => {
184
+ // Focus: the @mention body was acked-and-dropped at ingest — wake a turn to PULL it (recall).
185
+ if (!busy)
186
+ void drive(`📨 You were mentioned by ${fmtFrom(item)} on #${item.channel ?? "?"} — read it with cotal_inbox.`);
187
+ });
188
+ agent.on("wake", () => {
189
+ if (!busy)
190
+ void drive();
191
+ });
192
+ /** Match an event's session against the one we drive. Adopt the first session id we see (a fresh
193
+ * spawn has exactly one — the visible session our first submit creates), then filter to it. */
194
+ const ours = (id) => {
195
+ if (!id)
196
+ return !sessionID; // a session-less event counts as ours only before we've adopted one
197
+ if (!sessionID)
198
+ sessionID = id;
199
+ return id === sessionID;
200
+ };
201
+ const hooks = {
202
+ tool: buildCotalTools(agent, config),
203
+ event: async ({ event }) => {
204
+ // The server emits `permission.asked` (the SDK's `permission.updated` type ships but never
205
+ // fires — #11616), so match the real runtime name out of band. With permission:"allow" this
206
+ // rarely triggers, but it keeps presence correct if the posture tightens.
207
+ if (event.type === "permission.asked") {
208
+ const p = event.properties;
209
+ if (!p.sessionID || ours(p.sessionID))
210
+ await safeStatus("waiting", p.title);
211
+ return;
212
+ }
213
+ switch (event.type) {
214
+ case "session.created":
215
+ // Adopt the visible (top-level) session as soon as it's born; ignore subagent children.
216
+ if (!event.properties.info.parentID)
217
+ ours(event.properties.info.id);
218
+ break;
219
+ case "session.idle":
220
+ if (!ours(event.properties.sessionID))
221
+ return;
222
+ await safeStatus("idle");
223
+ completeTurn(); // the sole turn-end site: ack-on-surface + drive the next batch
224
+ break;
225
+ case "session.status": {
226
+ if (!ours(event.properties.sessionID))
227
+ return;
228
+ const s = event.properties.status;
229
+ // Presence only — session.idle owns ack + drive (so a duplicate idle can't mis-ack).
230
+ if (s.type === "busy") {
231
+ busy = true;
232
+ await safeStatus("working");
233
+ }
234
+ else if (s.type === "idle") {
235
+ await safeStatus("idle");
236
+ }
237
+ else if (s.type === "retry") {
238
+ await safeStatus("working", `retrying: ${s.message}`);
239
+ }
240
+ break;
241
+ }
242
+ case "session.error":
243
+ // session.error's sessionID is OPTIONAL; skip only a DIFFERENT session's error — a
244
+ // session-less one (id undefined) during our in-flight turn must still complete it, else
245
+ // the surfaced batch is never acked and `busy` stays stuck.
246
+ if (event.properties.sessionID && !ours(event.properties.sessionID))
247
+ return;
248
+ if (!awaitingTurnEnd)
249
+ return; // no in-flight turn to fail
250
+ awaitingTurnEnd = false;
251
+ busy = false;
252
+ ackSurfaced(); // turn surfaced the batch but failed — ack (don't retry-loop) and move on
253
+ await safeStatus("idle");
254
+ void drive();
255
+ break;
256
+ case "session.deleted":
257
+ if (!ours(event.properties.info.id))
258
+ return;
259
+ await safeStatus("offline");
260
+ break;
261
+ }
262
+ },
263
+ // Surface the running tool as presence activity (parity with Claude's PreToolUse).
264
+ "tool.execute.before": async (input) => {
265
+ if (!ours(input.sessionID))
266
+ return;
267
+ await safeStatus("working", input.tool);
268
+ },
269
+ dispose: async () => {
270
+ await safeStatus("offline");
271
+ await agent.stop();
272
+ },
273
+ };
274
+ guard.__cotalOpencodeHooks = hooks;
275
+ log(`opencode plugin ready — space="${config.space}" name="${config.name}"${config.role ? ` role="${config.role}"` : ""}`);
276
+ return hooks;
277
+ };
278
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=serve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":""}
package/dist/serve.js ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Launcher shim for the OpenCode connector — gives a spawned agent a *watchable* TUI bound to the
3
+ * exact session it drives, using OpenCode's own client/server split:
4
+ *
5
+ * 1. start `opencode serve` (headless) on a free port, with the Cotal plugin loaded inline;
6
+ * 2. poke it once so the lazily-loaded plugin initializes (joins the mesh, creates ONE session);
7
+ * 3. the plugin announces that session's id on stderr (`[cotal-session] <id>`);
8
+ * 4. launch a foreground `opencode attach <url> --session <id>` — the TUI opens straight onto the
9
+ * agent's session, and every turn the plugin drives (via `session.promptAsync`) renders live.
10
+ *
11
+ * The attach TUI is a pure viewer (it connects to the running server); its env strips the plugin
12
+ * config + COTAL_* so it never loads a *second* mesh endpoint.
13
+ *
14
+ * SECURITY: `opencode serve` is UNAUTHENTICATED by default — the CVE-2026-22812 surface (any local
15
+ * process, or a malicious site via DNS-rebind, can drive the session: arbitrary code execution as
16
+ * this user + full mesh-identity takeover via the ungated cotal_* tools). So we set a random
17
+ * per-launch `OPENCODE_SERVER_PASSWORD` in the child env; the poke and the attach TUI present it as
18
+ * HTTP basic auth. Bind stays loopback; no CORS / mDNS.
19
+ */
20
+ import { spawn } from "node:child_process";
21
+ import { createServer } from "node:net";
22
+ import { once } from "node:events";
23
+ import { randomBytes } from "node:crypto";
24
+ const BIN = process.env.COTAL_OPENCODE_BIN?.trim() || "opencode";
25
+ /** Per-launch secret gating the spawned server's HTTP API (see SECURITY above). */
26
+ const SECRET = randomBytes(24).toString("hex");
27
+ /** Ask the OS for a free port (bind :0, read it, release) so co-located peers don't collide. */
28
+ async function freePort() {
29
+ const srv = createServer();
30
+ srv.listen(0, "127.0.0.1");
31
+ await once(srv, "listening");
32
+ const port = srv.address().port;
33
+ await new Promise((r) => srv.close(() => r()));
34
+ return port;
35
+ }
36
+ async function main() {
37
+ const port = process.env.COTAL_OPENCODE_PORT?.trim() || String(await freePort());
38
+ const url = `http://127.0.0.1:${port}`;
39
+ const serve = spawn(BIN, ["serve", "--hostname", "127.0.0.1", "--port", port], {
40
+ env: { ...process.env, OPENCODE_SERVER_PASSWORD: SECRET },
41
+ stdio: ["ignore", "pipe", "pipe"],
42
+ });
43
+ // Scan the server's output for the plugin's session handshake; forward boot logs to our stderr
44
+ // until the TUI takes over the terminal (after that, drop them so they can't corrupt its display).
45
+ let sessionId;
46
+ let attached = false;
47
+ let onSession;
48
+ const scan = (d) => {
49
+ if (!attached)
50
+ process.stderr.write(d);
51
+ if (!sessionId) {
52
+ const m = d.toString().match(/\[cotal-session\] (\S+)/);
53
+ if (m) {
54
+ sessionId = m[1];
55
+ onSession?.(sessionId);
56
+ }
57
+ }
58
+ };
59
+ serve.stdout?.on("data", scan);
60
+ serve.stderr?.on("data", scan);
61
+ serve.on("exit", (code, signal) => {
62
+ if (!attached)
63
+ process.exit(code ?? (signal ? 1 : 0)); // died before the TUI came up
64
+ });
65
+ // Poke the server (lazy plugin load → mesh join + session create). Polling — not a one-shot keyed
66
+ // off a log banner — means a slow start can't leave the agent silently un-joined.
67
+ const auth = `Basic ${Buffer.from(`opencode:${SECRET}`).toString("base64")}`;
68
+ void (async () => {
69
+ for (let i = 0; i < 50; i++) {
70
+ try {
71
+ const res = await fetch(`${url}/session`, { headers: { authorization: auth } });
72
+ if (res.ok)
73
+ return;
74
+ }
75
+ catch {
76
+ /* not up yet — retry */
77
+ }
78
+ await new Promise((r) => setTimeout(r, 200));
79
+ }
80
+ process.stderr.write("[cotal-connector] serve: no 2xx from the poke after ~10s — plugin may not have loaded\n");
81
+ })();
82
+ // Wait for the agent's session, then attach a foreground TUI to it.
83
+ const id = await new Promise((resolve) => {
84
+ if (sessionId)
85
+ return resolve(sessionId);
86
+ onSession = resolve;
87
+ setTimeout(() => resolve(sessionId), 20_000);
88
+ });
89
+ if (!id) {
90
+ process.stderr.write("[cotal-connector] serve: agent session never came up (~20s) — aborting\n");
91
+ serve.kill("SIGTERM");
92
+ process.exit(1);
93
+ }
94
+ const tuiEnv = { ...process.env, OPENCODE_SERVER_PASSWORD: SECRET };
95
+ delete tuiEnv.OPENCODE_CONFIG_CONTENT; // a viewer, not a peer — must NOT load the plugin again
96
+ for (const k of Object.keys(tuiEnv))
97
+ if (k.startsWith("COTAL_"))
98
+ delete tuiEnv[k];
99
+ attached = true;
100
+ const tui = spawn(BIN, ["attach", url, "--session", id, "--password", SECRET], {
101
+ env: tuiEnv,
102
+ stdio: "inherit",
103
+ });
104
+ for (const sig of ["SIGINT", "SIGTERM"])
105
+ process.on(sig, () => {
106
+ tui.kill(sig);
107
+ serve.kill(sig);
108
+ });
109
+ tui.on("exit", (code, signal) => {
110
+ serve.kill("SIGTERM"); // TUI closed → tear down the server
111
+ process.exit(code ?? (signal ? 1 : 0));
112
+ });
113
+ }
114
+ void main();
115
+ //# sourceMappingURL=serve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve.js","sourceRoot":"","sources":["../src/serve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The Cotal tool surface for OpenCode, rendered from the **shared** {@link cotalToolSpecs}
3
+ * (the same source the Claude Code / Codex MCP connectors render) as OpenCode-native plugin
4
+ * tools (the `tool()` helper). One source of truth → the cotal_* surface can't drift across
5
+ * adapters: an OpenCode peer gets the same tools (incl. channels / join / leave / channel_info).
6
+ *
7
+ * The one OpenCode-specific tool is `cotal_inbox`: this connector DRIVES delivery (it surfaces
8
+ * each batch into a turn and acks on completion), so the agent's inbox tool is READ-ONLY — it
9
+ * peeks (never drains), or it would race the connector's ack. It still honors focus-mode recall.
10
+ */
11
+ import { type ToolDefinition } from "@opencode-ai/plugin";
12
+ import { type MeshAgent, type AgentConfig } from "@cotal-ai/connector-core";
13
+ /** Build the cotal_* tool map wired to one mesh agent, rendered from the shared specs. */
14
+ export declare function buildCotalTools(agent: MeshAgent, config: AgentConfig): Record<string, ToolDefinition>;
15
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAkB,KAAK,SAAS,EAAE,KAAK,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5F,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA4BrG"}
package/dist/tools.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The Cotal tool surface for OpenCode, rendered from the **shared** {@link cotalToolSpecs}
3
+ * (the same source the Claude Code / Codex MCP connectors render) as OpenCode-native plugin
4
+ * tools (the `tool()` helper). One source of truth → the cotal_* surface can't drift across
5
+ * adapters: an OpenCode peer gets the same tools (incl. channels / join / leave / channel_info).
6
+ *
7
+ * The one OpenCode-specific tool is `cotal_inbox`: this connector DRIVES delivery (it surfaces
8
+ * each batch into a turn and acks on completion), so the agent's inbox tool is READ-ONLY — it
9
+ * peeks (never drains), or it would race the connector's ack. It still honors focus-mode recall.
10
+ */
11
+ import { tool } from "@opencode-ai/plugin";
12
+ import { cotalToolSpecs } from "@cotal-ai/connector-core";
13
+ /** Build the cotal_* tool map wired to one mesh agent, rendered from the shared specs. */
14
+ export function buildCotalTools(agent, config) {
15
+ const tools = {};
16
+ for (const spec of cotalToolSpecs(config)) {
17
+ if (spec.name === "cotal_inbox") {
18
+ // Read-only: this connector delivers + acks each turn, so the tool must never drain. Force
19
+ // peek (still surfaces focus-mode recall), and reframe push-primary / pull-secondary. (norman)
20
+ tools.cotal_inbox = tool({
21
+ description: "Show the peer messages currently waiting for you (incl. focus-mode recall). You don't normally need this — the connector delivers peer messages into your turns automatically; use it to re-check what's pending mid-task. Read-only: it never consumes them.",
22
+ args: {},
23
+ async execute() {
24
+ const r = await spec.run(agent, config, { peek: true });
25
+ return r.isError ? `⚠ ${r.text}` : r.text;
26
+ },
27
+ });
28
+ continue;
29
+ }
30
+ tools[spec.name] = tool({
31
+ description: spec.description,
32
+ // The shared spec carries a Zod raw shape; OpenCode's tool() takes the same (zod via tool.schema).
33
+ args: (spec.schema ?? {}),
34
+ async execute(args) {
35
+ const r = await spec.run(agent, config, (args ?? {}));
36
+ return r.isError ? `⚠ ${r.text}` : r.text;
37
+ },
38
+ });
39
+ }
40
+ return tools;
41
+ }
42
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,IAAI,EAAuB,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,cAAc,EAAoC,MAAM,0BAA0B,CAAC;AAE5F,0FAA0F;AAC1F,MAAM,UAAU,eAAe,CAAC,KAAgB,EAAE,MAAmB;IACnE,MAAM,KAAK,GAAmC,EAAE,CAAC;IACjD,KAAK,MAAM,IAAI,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1C,IAAI,IAAI,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YAChC,2FAA2F;YAC3F,+FAA+F;YAC/F,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC;gBACvB,WAAW,EACT,+PAA+P;gBACjQ,IAAI,EAAE,EAAE;gBACR,KAAK,CAAC,OAAO;oBACX,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;oBACxD,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC5C,CAAC;aACF,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;YACtB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,mGAAmG;YACnG,IAAI,EAAE,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAA0B;YAClD,KAAK,CAAC,OAAO,CAAC,IAAa;gBACzB,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,IAAI,IAAI,EAAE,CAAQ,CAAC,CAAC;gBAC7D,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC5C,CAAC;SACF,CAAC,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@cotal-ai/connector-opencode",
3
+ "version": "0.1.1",
4
+ "license": "Apache-2.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Cotal-AI/Cotal.git",
8
+ "directory": "extensions/connector-opencode"
9
+ },
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "dependencies": {
20
+ "@cotal-ai/connector-core": "0.1.3"
21
+ },
22
+ "peerDependencies": {
23
+ "@opencode-ai/plugin": "^1.16.2",
24
+ "@opencode-ai/sdk": "^1.16.2",
25
+ "@cotal-ai/core": "0.1.2"
26
+ },
27
+ "devDependencies": {
28
+ "@opencode-ai/plugin": "^1.16.2",
29
+ "@opencode-ai/sdk": "^1.16.2",
30
+ "esbuild": "^0.28.0",
31
+ "tsx": "^4.22.4",
32
+ "@cotal-ai/core": "0.1.2"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "scripts": {
41
+ "typecheck": "tsc -p tsconfig.json --noEmit",
42
+ "build": "tsc -p tsconfig.json && pnpm run bundle",
43
+ "bundle": "esbuild src/plugin.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/plugin.bundle.js --external:@opencode-ai/plugin --external:@opencode-ai/sdk"
44
+ }
45
+ }