@cotal-ai/connector-opencode 0.69.0 → 0.70.0

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.
@@ -1,7 +1,7 @@
1
1
  import { type Connector } from "@cotal-ai/core";
2
2
  /**
3
3
  * The OpenCode connector: launches a watchable `opencode` TUI bound to the agent's session, using
4
- * OpenCode's client/server split (see serve.ts). The Cotal mesh bridge runs as an in-process plugin
4
+ * OpenCode's client/server split (see launch.ts). The Cotal mesh bridge runs as an in-process plugin
5
5
  * inside a headless `opencode serve`: it holds the {@link MeshAgent}, registers the cotal_* tools
6
6
  * natively (from the shared specs, at parity with Claude Code), reports presence off the event bus,
7
7
  * and owns ONE session it drives — injecting each incoming peer batch through the authenticated
package/dist/index.js CHANGED
@@ -14,7 +14,10 @@ import { registry as registry3 } from "@cotal-ai/core";
14
14
  import { DEFAULT_SERVER, DEV_OWNER, LAUNCH_MATERIAL_ENV, discardLaunchMaterial, assertLifecycleToken, assertValidChannel, channelInAllow, credsClaims, eventChannel, idFromCreds, isConcreteChannel, loadAgentFile, parseJoinLink, readLaunchMaterial, resolveReadAcl } from "@cotal-ai/core";
15
15
 
16
16
  // ../connector-core/dist/agent.js
17
- import { normalizeMentions, RUN_LAUNCH_DEADLINE_MS, subjectMatches, isConcreteChannel as isConcreteChannel2, assertValidChannel as assertValidChannel2, channelInAllow as channelInAllow2, resolvePeer as resolvePeerInRoster, CotalEndpoint, BASELINE_LIFECYCLE_ENDPOINT as BASELINE_LIFECYCLE_ENDPOINT2, BIND_SPLIT_REISSUES, assertLifecycleToken as assertLifecycleToken3, EpEnvelopeError, isPublishPermissionDenied, unansweredRequest, renderLifecycleBlocked, bearerCommandFailure, partsToText, routeToken } from "@cotal-ai/core";
17
+ import { normalizeMentions, RUN_LAUNCH_DEADLINE_MS, subjectMatches, isConcreteChannel as isConcreteChannel2, assertValidChannel as assertValidChannel2, channelInAllow as channelInAllow2, resolvePeer as resolvePeerInRoster, peerLabel as peerLabel2, CotalEndpoint, BASELINE_LIFECYCLE_ENDPOINT as BASELINE_LIFECYCLE_ENDPOINT2, BIND_SPLIT_REISSUES, assertLifecycleToken as assertLifecycleToken3, EpEnvelopeError, isPublishPermissionDenied, unansweredRequest, renderLifecycleBlocked, controlReplyFrom, bearerCommandFailure, partsToText, routeToken } from "@cotal-ai/core";
18
+
19
+ // ../connector-core/dist/framing.js
20
+ import { peerLabel } from "@cotal-ai/core";
18
21
 
19
22
  // ../connector-core/dist/manager-call.js
20
23
  import { BASELINE_LIFECYCLE_ENDPOINT, assertLifecycleToken as assertLifecycleToken2, dialerFor, invokeCommand, isPermissionDenied, issuedUserCaller, replyRefusedBeforeEffect, resolveService, standaloneConnectOpts } from "@cotal-ai/core";
@@ -14855,7 +14858,10 @@ function date4(params) {
14855
14858
  config(en_default());
14856
14859
 
14857
14860
  // ../connector-core/dist/tool-specs.js
14858
- import { isConcreteChannel as isConcreteChannel3, channelInAllow as channelInAllow3, AmbiguousPeerError, assertLifecycleToken as assertLifecycleToken4, isPermissionDenied as isPermissionDenied2, renderLifecycleBlocked as renderLifecycleBlocked2, LANG_PROBLEM_DETAIL_KIND, formatAge, presenceAges } from "@cotal-ai/core";
14861
+ import { isConcreteChannel as isConcreteChannel3, channelInAllow as channelInAllow3, AmbiguousPeerError, peerLabel as peerLabel4, assertLifecycleToken as assertLifecycleToken4, isPermissionDenied as isPermissionDenied2, renderLifecycleBlocked as renderLifecycleBlocked2, LANG_PROBLEM_DETAIL_KIND, formatAge, presenceAges } from "@cotal-ai/core";
14862
+
14863
+ // ../connector-core/dist/orientation.js
14864
+ import { peerLabel as peerLabel3 } from "@cotal-ai/core";
14859
14865
 
14860
14866
  // ../connector-core/dist/docs.js
14861
14867
  var STOP = new Set("a an and are as at be by do for from has how in is it of on or that the this to use using was what when where which who with you your".split(" "));
@@ -0,0 +1,14 @@
1
+ export declare const BIN: string;
2
+ /** The foreground TUI's argv, built from the server it attaches to. Its env carries the server's
3
+ * basic-auth credentials, so the argv never needs the secret. */
4
+ export type TuiArgv = (server: {
5
+ url: string;
6
+ session: string;
7
+ line: 1 | 2;
8
+ }) => [command: string, ...args: string[]];
9
+ /** Extra env for the server, built from inputs the caller writes into the agent's home. It runs only
10
+ * once no seat of this name is running, so a refused launch never rewrites files a running seat's
11
+ * server still reads. */
12
+ export type ServeEnv = (agentHome: string) => NodeJS.ProcessEnv;
13
+ export declare function launch(tuiArgv: TuiArgv, serveEnv?: ServeEnv): Promise<void>;
14
+ //# sourceMappingURL=launch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch.d.ts","sourceRoot":"","sources":["../src/launch.ts"],"names":[],"mappings":"AAgDA,eAAO,MAAM,GAAG,QAAuB,CAAC;AAqExC;kEACkE;AAClE,MAAM,MAAM,OAAO,GAAG,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAA;CAAE,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;AAEtH;;0BAE0B;AAC1B,MAAM,MAAM,QAAQ,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,UAAU,CAAC;AAEhE,wBAAsB,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CA+IjF"}
package/dist/launch.js ADDED
@@ -0,0 +1,195 @@
1
+ // src/launch.ts
2
+ import { execFileSync, spawn } from "node:child_process";
3
+ import { createServer } from "node:net";
4
+ import { once } from "node:events";
5
+ import { randomBytes } from "node:crypto";
6
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
+ import { delimiter, join } from "node:path";
8
+
9
+ // src/opencode-line.ts
10
+ function opencodeLine(raw, bin) {
11
+ const m = raw.match(/(?:^|v)(\d+)\.\d+/);
12
+ const major = m ? Number(m[1]) : NaN;
13
+ if (major === 1 || major === 2) return major;
14
+ throw new Error(
15
+ `opencode connector: unsupported OpenCode version "${raw}" from ${bin}; this connector supports the 1.x (opencode-ai) and 2.x (@opencode/cli) lines`
16
+ );
17
+ }
18
+
19
+ // src/launch.ts
20
+ function resolveOpencodeBin() {
21
+ const override = process.env.COTAL_OPENCODE_BIN?.trim() || process.env.COTAL_OPENCODE_RESOLVED_BIN?.trim();
22
+ if (override) return override;
23
+ if (process.platform !== "win32") return "opencode";
24
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
25
+ if (!dir) continue;
26
+ for (const exe of [join(dir, "opencode.exe"), join(dir, "node_modules", "opencode-ai", "bin", "opencode.exe")]) {
27
+ if (existsSync(exe)) return exe;
28
+ }
29
+ }
30
+ return "opencode";
31
+ }
32
+ var BIN = resolveOpencodeBin();
33
+ var USERNAME = "opencode";
34
+ var SECRET = process.env.OPENCODE_SERVER_PASSWORD || randomBytes(24).toString("hex");
35
+ async function freePort() {
36
+ const srv = createServer();
37
+ srv.listen(0, "127.0.0.1");
38
+ await once(srv, "listening");
39
+ const port = srv.address().port;
40
+ await new Promise((r) => srv.close(() => r()));
41
+ return port;
42
+ }
43
+ async function terminate(child) {
44
+ if (child.exitCode !== null || child.signalCode !== null) return;
45
+ child.kill("SIGTERM");
46
+ const dead = await Promise.race([
47
+ once(child, "exit").then(() => true),
48
+ new Promise((r) => setTimeout(() => r(false), 3e3))
49
+ ]);
50
+ if (!dead) {
51
+ child.kill("SIGKILL");
52
+ await once(child, "exit");
53
+ }
54
+ }
55
+ function processCommand(pid) {
56
+ try {
57
+ if (process.platform === "win32") {
58
+ return execFileSync("powershell.exe", [
59
+ "-NoProfile",
60
+ "-NonInteractive",
61
+ "-Command",
62
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`
63
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
64
+ }
65
+ return execFileSync("ps", ["-p", String(pid), "-o", "command="], {
66
+ encoding: "utf8",
67
+ stdio: ["ignore", "pipe", "ignore"]
68
+ }).trim();
69
+ } catch {
70
+ return void 0;
71
+ }
72
+ }
73
+ function isLiveOpencodeServe(pid) {
74
+ if (!Number.isInteger(pid) || pid <= 0) return false;
75
+ try {
76
+ process.kill(pid, 0);
77
+ } catch {
78
+ return false;
79
+ }
80
+ const cmd = processCommand(pid);
81
+ if (!cmd) return true;
82
+ return /\bserve\b/.test(cmd) && (/(?:^|[\\/\s])opencode(?:\.exe)?(?:\s|$)/i.test(cmd) || /--hostname\s+127\.0\.0\.1\b/.test(cmd) && /--port\s+\d+\b/.test(cmd));
83
+ }
84
+ async function launch(tuiArgv, serveEnv) {
85
+ const port = process.env.COTAL_OPENCODE_PORT?.trim() || String(await freePort());
86
+ const url = `http://127.0.0.1:${port}`;
87
+ const name = process.env.COTAL_NAME?.trim() || "agent";
88
+ const dataRoot = process.env.COTAL_OPENCODE_HOME?.trim();
89
+ if (!dataRoot) throw new Error("COTAL_OPENCODE_HOME is not set \u2014 the connector must pin the agent's data root");
90
+ const agentHome = join(dataRoot, ".cotal", "opencode", name);
91
+ const dbPath = join(agentHome, "opencode.db");
92
+ const pidFile = join(agentHome, "serve.pid");
93
+ let recorded;
94
+ try {
95
+ recorded = readFileSync(pidFile, "utf8");
96
+ } catch (e) {
97
+ if (e.code !== "ENOENT") throw e;
98
+ }
99
+ if (recorded !== void 0) {
100
+ const pid = Number(recorded);
101
+ if (isLiveOpencodeServe(pid))
102
+ throw new Error(`agent "${name}" is already running (opencode serve pid ${pid}) \u2014 kill it first`);
103
+ rmSync(pidFile, { force: true });
104
+ }
105
+ const versionRaw = execFileSync(BIN, ["--version"], { encoding: "utf8" }).trim();
106
+ const line = opencodeLine(versionRaw, BIN);
107
+ mkdirSync(agentHome, { recursive: true });
108
+ const serve = spawn(BIN, ["serve", "--hostname", "127.0.0.1", "--port", port], {
109
+ env: {
110
+ ...process.env,
111
+ ...serveEnv?.(agentHome),
112
+ COTAL_OPENCODE_SERVER_URL: url,
113
+ COTAL_OPENCODE_LINE: String(line),
114
+ OPENCODE_SERVER_USERNAME: USERNAME,
115
+ OPENCODE_SERVER_PASSWORD: SECRET,
116
+ OPENCODE_DB: dbPath
117
+ },
118
+ stdio: ["ignore", "pipe", "pipe"]
119
+ });
120
+ writeFileSync(pidFile, String(serve.pid));
121
+ serve.on("exit", () => rmSync(pidFile, { force: true }));
122
+ let sessionId;
123
+ let attached = false;
124
+ let onSession;
125
+ const scan = (d) => {
126
+ if (!attached) process.stderr.write(d);
127
+ if (!sessionId) {
128
+ const m = d.toString().match(/\[cotal-session\] (\S+)/);
129
+ if (m) {
130
+ sessionId = m[1];
131
+ onSession?.(sessionId);
132
+ }
133
+ }
134
+ };
135
+ serve.stdout?.on("data", scan);
136
+ serve.stderr?.on("data", scan);
137
+ serve.on("exit", (code, signal) => {
138
+ if (!attached) process.exit(code ?? (signal ? 1 : 0));
139
+ });
140
+ const auth = `Basic ${Buffer.from(`${USERNAME}:${SECRET}`).toString("base64")}`;
141
+ const pokePath = line === 2 ? "/api/config" : "/session";
142
+ void (async () => {
143
+ for (let i = 0; i < 300 && !sessionId; i++) {
144
+ try {
145
+ await fetch(`${url}${pokePath}`, { headers: { authorization: auth }, signal: AbortSignal.timeout(1500) });
146
+ } catch {
147
+ }
148
+ await new Promise((r) => setTimeout(r, 200));
149
+ }
150
+ })();
151
+ const id = await new Promise((resolve) => {
152
+ if (sessionId) return resolve(sessionId);
153
+ onSession = resolve;
154
+ setTimeout(() => resolve(sessionId), 6e4);
155
+ });
156
+ if (!id) {
157
+ process.stderr.write(
158
+ `[cotal-connector] serve: agent session never came up (~60s) \u2014 aborting. Check the boot log above for plugin/mesh errors (OPENCODE_DB=${dbPath})
159
+ `
160
+ );
161
+ await terminate(serve);
162
+ process.exit(1);
163
+ }
164
+ if (process.env.COTAL_SERVE_HEADLESS?.trim() === "1") {
165
+ process.stdout.write(`[cotal-serve] ${JSON.stringify({ port: Number(port), session: id })}
166
+ `);
167
+ for (const sig of ["SIGINT", "SIGTERM"])
168
+ process.on(sig, () => void terminate(serve).then(() => process.exit(0)));
169
+ return;
170
+ }
171
+ const tuiEnv = {
172
+ ...process.env,
173
+ OPENCODE_SERVER_USERNAME: USERNAME,
174
+ OPENCODE_SERVER_PASSWORD: SECRET,
175
+ OPENCODE_DB: dbPath
176
+ };
177
+ delete tuiEnv.OPENCODE_CONFIG_CONTENT;
178
+ for (const k of Object.keys(tuiEnv)) if (k.startsWith("COTAL_")) delete tuiEnv[k];
179
+ attached = true;
180
+ const [command, ...args] = tuiArgv({ url, session: id, line });
181
+ const tui = spawn(command, args, { env: tuiEnv, stdio: "inherit" });
182
+ for (const sig of ["SIGINT", "SIGTERM"])
183
+ process.on(sig, () => {
184
+ tui.kill(sig);
185
+ serve.kill(sig);
186
+ });
187
+ serve.on("exit", () => void terminate(tui));
188
+ tui.on("exit", (code, signal) => {
189
+ void terminate(serve).then(() => process.exit(code ?? (signal ? 1 : 0)));
190
+ });
191
+ }
192
+ export {
193
+ BIN,
194
+ launch
195
+ };
@@ -1,5 +1,5 @@
1
1
  /** Parses `opencode --version` output into the major protocol line: 1 (opencode-ai) or 2
2
- * (@opencode/cli). Pure so both serve.ts (the launcher) and extension.ts (the model catalog) can
2
+ * (@opencode/cli). Pure so both launch.ts (the launcher) and extension.ts (the model catalog) can
3
3
  * detect the line from the same rule with no fallback for anything else. */
4
4
  export declare function opencodeLine(raw: string, bin: string): 1 | 2;
5
5
  //# sourceMappingURL=opencode-line.d.ts.map