@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.
- package/LICENSE +202 -0
- package/dist/extension.d.ts +17 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +66 -0
- package/dist/extension.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.bundle.js +32984 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +278 -0
- package/dist/plugin.js.map +1 -0
- package/dist/serve.d.ts +2 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +115 -0
- package/dist/serve.js.map +1 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +42 -0
- package/dist/tools.js.map +1 -0
- package/package.json +45 -0
package/dist/plugin.d.ts
ADDED
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,aAAa,EAAuB,MAAM,gBAAgB,CAAC;AACpE,OAAO,EACL,aAAa,EACb,WAAW,EACX,SAAS,EACT,eAAe,EACf,OAAO,GAER,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,SAAS,GAAG,CAAC,GAAW;IACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;AACrD,CAAC;AAED;;;0DAG0D;AAC1D,MAAM,KAAK,GAAG,UAA8C,CAAC;AAE7D,MAAM,CAAC,MAAM,KAAK,GAAW,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;IAChD,8EAA8E;IAC9E,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;QACnB,GAAG,CAAC,6DAA6D,CAAC,CAAC;QACnE,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,KAAK,CAAC,oBAAoB;QAAE,OAAO,KAAK,CAAC,oBAAoB,CAAC,CAAC,6BAA6B;IAChG,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;IAC/B,MAAM,KAAK,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC;IACpC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,uDAAuD;IAEtE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAClH,MAAM,OAAO,GAAG,GAAG,EAAE,OAAO,CAAC;IAE7B,iGAAiG;IACjG,6FAA6F;IAC7F,uGAAuG;IACvG,IAAI,SAA6B,CAAC;IAClC,IAAI,IAAI,GAAG,KAAK,CAAC,CAAC,gFAAgF;IAClG,IAAI,OAAO,GAAG,KAAK,CAAC,CAAC,oDAAoD;IACzE,IAAI,MAAM,GAAG,KAAK,CAAC,CAAC,qDAAqD;IACzE,IAAI,OAAO,GAAG,KAAK,CAAC,CAAC,iEAAiE;IACtF,IAAI,QAAQ,GAAa,EAAE,CAAC,CAAC,6EAA6E;IAC1G,IAAI,eAAe,GAAG,KAAK,CAAC,CAAC,mEAAmE;IAEhG,MAAM,UAAU,GAAG,KAAK,EAAE,MAAsB,EAAE,QAAiB,EAAiB,EAAE;QACpF,IAAI,CAAC;YACH,IAAI,KAAK,CAAC,SAAS;gBAAE,MAAM,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAC/D,CAAC;QAAC,MAAM,CAAC;YACP,yDAAyD;QAC3D,CAAC;IACH,CAAC,CAAC;IAEF;;;4EAGwE;IACxE,MAAM,YAAY,GAAgC,CAAC,KAAK,IAAI,EAAE;QAC5D,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;YACrG,SAAS,GAAG,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;YACzB,IAAI,SAAS;gBAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,SAAS,IAAI,CAAC,CAAC;;gBACjE,GAAG,CAAC,+BAA+B,CAAC,CAAC;QAC5C,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,GAAG,CAAC,0BAA2B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC,EAAE,CAAC;IAEL,4EAA4E;IAC5E,KAAK,UAAU,aAAa;QAC1B,OAAO,SAAS,IAAI,CAAC,MAAM,YAAY,CAAC,CAAC;IAC3C,CAAC;IAED;;;;;+FAK2F;IAC3F,KAAK,UAAU,KAAK,CAAC,QAAiB;QACpC,IAAI,OAAO,IAAI,IAAI;YAAE,OAAO;QAC5B,OAAO,GAAG,IAAI,CAAC;QACf,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,MAAM,aAAa,EAAE,CAAC;YACjC,IAAI,CAAC,EAAE;gBAAE,OAAO,CAAC,wDAAwD;YACzE,MAAM,KAAK,GAAqC,EAAE,CAAC;YACnD,IAAI,GAAG,GAAa,EAAE,CAAC;YACvB,IAAI,QAAQ,EAAE,CAAC;gBACb,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC/C,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;gBAChC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;oBAAE,OAAO;gBAC/B,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;gBAC7B,MAAM,GAAG,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;gBACnC,IAAI,GAAG;oBAAE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;YACnD,CAAC;YACD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,GAAG,IAAI,CAAC;gBACf,MAAM,KAAK,GAAG,KAAK,CAAC,eAAe,EAAE,CAAC;gBACtC,IAAI,KAAK;oBAAE,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YAC1D,CAAC;YACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO;YAC/B,MAAM,IAAI,GAA6C,EAAE,KAAK,EAAE,CAAC;YACjE,IAAI,CAAC,MAAM,IAAI,OAAO;gBAAE,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,CAAC,sDAAsD;YACrG,IAAI,GAAG,IAAI,CAAC;YACZ,QAAQ,GAAG,GAAG,CAAC;YACf,oFAAoF;YACpF,oFAAoF;YACpF,eAAe,GAAG,IAAI,CAAC;YACvB,MAAM,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,GAAG,KAAK,CAAC;YACb,QAAQ,GAAG,EAAE,CAAC;YACd,eAAe,GAAG,KAAK,CAAC;YACxB,GAAG,CAAC,iBAAkB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QAC/C,CAAC;gBAAS,CAAC;YACT,OAAO,GAAG,KAAK,CAAC;QAClB,CAAC;IACH,CAAC;IAED;;;;gFAI4E;IAC5E,SAAS,WAAW;QAClB,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAClC,MAAM,KAAK,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;QAChC,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC;YAAE,CAAC,EAAE,CAAC;QACnF,IAAI,CAAC,GAAG,CAAC;YAAE,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC/B,QAAQ,GAAG,EAAE,CAAC;IAChB,CAAC;IAED;;sFAEkF;IAClF,SAAS,YAAY;QACnB,IAAI,CAAC,eAAe;YAAE,OAAO;QAC7B,eAAe,GAAG,KAAK,CAAC;QACxB,IAAI,GAAG,KAAK,CAAC;QACb,WAAW,EAAE,CAAC;QACd,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,oBAAoB,EAAE,CAAC;QAC/F,IAAI,OAAO,GAAG,CAAC;YAAE,KAAK,KAAK,EAAE,CAAC;IAChC,CAAC;IAED,6FAA6F;IAC7F,kGAAkG;IAClG,mGAAmG;IACnG,6FAA6F;IAC7F,KAAK,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,IAAe,EAAE,EAAE;QACvC,IAAI,IAAI;YAAE,OAAO,CAAC,0CAA0C;QAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,UAAU,CAAC;QAC5D,IAAI,QAAQ,IAAI,KAAK,CAAC,SAAS,KAAK,MAAM;YAAE,KAAK,KAAK,EAAE,CAAC;IAC3D,CAAC,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,CAAC,cAAc,EAAE,CAAC,IAAe,EAAE,EAAE;QAC3C,8FAA8F;QAC9F,IAAI,CAAC,IAAI;YAAE,KAAK,KAAK,CAAC,4BAA4B,OAAO,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,OAAO,IAAI,GAAG,8BAA8B,CAAC,CAAC;IAC5H,CAAC,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;QACpB,IAAI,CAAC,IAAI;YAAE,KAAK,KAAK,EAAE,CAAC;IAC1B,CAAC,CAAC,CAAC;IAEH;oGACgG;IAChG,MAAM,IAAI,GAAG,CAAC,EAAW,EAAW,EAAE;QACpC,IAAI,CAAC,EAAE;YAAE,OAAO,CAAC,SAAS,CAAC,CAAC,oEAAoE;QAChG,IAAI,CAAC,SAAS;YAAE,SAAS,GAAG,EAAE,CAAC;QAC/B,OAAO,EAAE,KAAK,SAAS,CAAC;IAC1B,CAAC,CAAC;IAEF,MAAM,KAAK,GAAU;QACnB,IAAI,EAAE,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC;QAEpC,KAAK,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;YACzB,2FAA2F;YAC3F,4FAA4F;YAC5F,0EAA0E;YAC1E,IAAK,KAAK,CAAC,IAAe,KAAK,kBAAkB,EAAE,CAAC;gBAClD,MAAM,CAAC,GAAG,KAAK,CAAC,UAAoD,CAAC;gBACrE,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;oBAAE,MAAM,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;gBAC5E,OAAO;YACT,CAAC;YACD,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;gBACnB,KAAK,iBAAiB;oBACpB,wFAAwF;oBACxF,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ;wBAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBACpE,MAAM;gBACR,KAAK,cAAc;oBACjB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC;wBAAE,OAAO;oBAC9C,MAAM,UAAU,CAAC,MAAM,CAAC,CAAC;oBACzB,YAAY,EAAE,CAAC,CAAC,gEAAgE;oBAChF,MAAM;gBACR,KAAK,gBAAgB,CAAC,CAAC,CAAC;oBACtB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC;wBAAE,OAAO;oBAC9C,MAAM,CAAC,GAAG,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC;oBAClC,qFAAqF;oBACrF,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;wBACtB,IAAI,GAAG,IAAI,CAAC;wBACZ,MAAM,UAAU,CAAC,SAAS,CAAC,CAAC;oBAC9B,CAAC;yBAAM,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;wBAC7B,MAAM,UAAU,CAAC,MAAM,CAAC,CAAC;oBAC3B,CAAC;yBAAM,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;wBAC9B,MAAM,UAAU,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;oBACxD,CAAC;oBACD,MAAM;gBACR,CAAC;gBACD,KAAK,eAAe;oBAClB,mFAAmF;oBACnF,yFAAyF;oBACzF,4DAA4D;oBAC5D,IAAI,KAAK,CAAC,UAAU,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC;wBAAE,OAAO;oBAC5E,IAAI,CAAC,eAAe;wBAAE,OAAO,CAAC,4BAA4B;oBAC1D,eAAe,GAAG,KAAK,CAAC;oBACxB,IAAI,GAAG,KAAK,CAAC;oBACb,WAAW,EAAE,CAAC,CAAC,0EAA0E;oBACzF,MAAM,UAAU,CAAC,MAAM,CAAC,CAAC;oBACzB,KAAK,KAAK,EAAE,CAAC;oBACb,MAAM;gBACR,KAAK,iBAAiB;oBACpB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;wBAAE,OAAO;oBAC5C,MAAM,UAAU,CAAC,SAAS,CAAC,CAAC;oBAC5B,MAAM;YACV,CAAC;QACH,CAAC;QAED,mFAAmF;QACnF,qBAAqB,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;YACrC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC;gBAAE,OAAO;YACnC,MAAM,UAAU,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC;QAED,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,UAAU,CAAC,SAAS,CAAC,CAAC;YAC5B,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;QACrB,CAAC;KACF,CAAC;IAEF,KAAK,CAAC,oBAAoB,GAAG,KAAK,CAAC;IACnC,GAAG,CAAC,kCAAkC,MAAM,CAAC,KAAK,WAAW,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC3H,OAAO,KAAK,CAAC;AACf,CAAC,CAAC"}
|
package/dist/serve.d.ts
ADDED
|
@@ -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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"}
|
package/dist/tools.d.ts
ADDED
|
@@ -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
|
+
}
|