@cotal-ai/connector-opencode 0.68.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.
@@ -20,7 +20,7 @@
20
20
  * holds no per-part state to drop: a part arrives once, complete, and is mapped in one shot. So the
21
21
  * divergence log is the source's, and there is nothing here for a revert to invalidate.
22
22
  */
23
- import { type AguiEvent, type RecordMapper } from "@cotal-ai/connector-core";
23
+ import { type RecordMapper } from "@cotal-ai/connector-core";
24
24
  import type { OpenCodeRecord } from "./agui-source.js";
25
25
  export interface OpenCodeMapperOptions {
26
26
  /** The native session id. It is the `threadId` on every event and nothing else may claim it. */
@@ -34,25 +34,12 @@ export interface OpenCodeMapperOptions {
34
34
  }
35
35
  export interface OpenCodeMapper {
36
36
  map: RecordMapper<OpenCodeRecord>;
37
- /**
38
- * Close the open run at a boundary the record stream cannot see: `session.idle`, which §3.2 makes
39
- * the flush boundary. Returns `null` when nothing is open, so calling it twice cannot manufacture
40
- * a second `RUN_FINISHED` for the bracket machine to refuse.
41
- */
42
- closeOpenRun: (timestamp: number, stopReason?: string) => {
43
- runId: string;
44
- events: AguiEvent[];
45
- } | null;
46
- /** The run currently open, or `null`. */
47
- openRun: () => string | null;
48
37
  /**
49
38
  * Forget a run the emitter closed out of band. KEYED ON THE ID: the report can arrive after this
50
39
  * mapper has already opened a newer run, and clearing unconditionally would orphan that one, whose
51
40
  * events would then emit under no run at all and halt a session that had done nothing wrong.
52
41
  */
53
42
  forgetOpenRun: (runId: string) => void;
54
- /** Why this session opened no runs, or `null` once one has. A silent refusal is the defect. */
55
- diagnose: () => string | null;
56
43
  }
57
44
  export declare function createOpenCodeMapper(opts: OpenCodeMapperOptions): OpenCodeMapper;
58
45
  //# sourceMappingURL=agui-map.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agui-map.d.ts","sourceRoot":"","sources":["../src/agui-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EACL,KAAK,SAAS,EACd,KAAK,YAAY,EAalB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,MAAM,WAAW,qBAAqB;IACpC,gGAAgG;IAChG,QAAQ,EAAE,MAAM,CAAC;IACjB,oGAAoG;IACpG,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kGAAkG;IAClG,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,YAAY,CAAC,cAAc,CAAC,CAAC;IAClC;;;;OAIG;IACH,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,KAAK;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,SAAS,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC;IACxG,yCAAyC;IACzC,OAAO,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,+FAA+F;IAC/F,QAAQ,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CAC/B;AAkDD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,qBAAqB,GAAG,cAAc,CA4HhF"}
1
+ {"version":3,"file":"agui-map.d.ts","sourceRoot":"","sources":["../src/agui-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAEL,KAAK,YAAY,EAalB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,MAAM,WAAW,qBAAqB;IACpC,gGAAgG;IAChG,QAAQ,EAAE,MAAM,CAAC;IACjB,oGAAoG;IACpG,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,sEAAsE;IACtE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kGAAkG;IAClG,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,YAAY,CAAC,cAAc,CAAC,CAAC;IAClC;;;;OAIG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAkDD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,qBAAqB,GAAG,cAAc,CAuGhF"}
@@ -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
@@ -11,10 +11,13 @@ import { resolve } from "node:path";
11
11
  import { registry as registry3 } from "@cotal-ai/core";
12
12
 
13
13
  // ../connector-core/dist/config.js
14
- import { DEFAULT_SERVER, DEV_OWNER, LAUNCH_MATERIAL_ENV, discardLaunchMaterial, assertLifecycleToken, assertValidChannel, channelInAllow, credsClaims, eventChannel, idFromCreds, isConcreteChannel, loadAgentFile, parseJoinLink, readLaunchMaterial } from "@cotal-ai/core";
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";
@@ -178,14 +181,11 @@ function materialEnv(opts) {
178
181
  // ../connector-core/dist/agui.js
179
182
  import { AGUI_FRAME_KIND, AGUI_EVENT_TYPE, isAguiFramePart } from "@cotal-ai/core";
180
183
  import { AGUI_FRAME_KIND as AGUI_FRAME_KIND2, AGUI_EVENT_TYPE as AGUI_EVENT_TYPE2, isAguiFramePart as isAguiFramePart2 } from "@cotal-ai/core";
181
- import { isCasLoss, principalKey as principalKey2 } from "@cotal-ai/core";
182
184
  var TRUNCATABLE_FIELDS = [
183
185
  { type: AGUI_EVENT_TYPE2.TOOL_CALL_ARGS, field: "delta" },
184
186
  { type: AGUI_EVENT_TYPE2.TOOL_CALL_RESULT, field: "content" },
185
187
  { type: AGUI_EVENT_TYPE2.TEXT_MESSAGE_CONTENT, field: "delta" }
186
188
  ];
187
- var SIZING_ID = "S".repeat(64);
188
- var SIZING_EXPECTATION = Number.MAX_SAFE_INTEGER;
189
189
  var EGRESS_FORBIDDEN_TYPES = /* @__PURE__ */ new Set([
190
190
  AGUI_EVENT_TYPE2.TOOL_CALL_ARGS,
191
191
  AGUI_EVENT_TYPE2.TOOL_CALL_RESULT
@@ -208,6 +208,11 @@ var KNOWN_EVENT_KEYS_BY_TYPE = /* @__PURE__ */ new Map([
208
208
  [AGUI_EVENT_TYPE2.CUSTOM, /* @__PURE__ */ new Set(["type", "timestamp", "rawEvent", "name", "value", "cotal"])]
209
209
  ]);
210
210
 
211
+ // ../connector-core/dist/agui-emitter.js
212
+ import { isAguiFramePart as isAguiFramePart3, isCasLoss, principalKey as principalKey2 } from "@cotal-ai/core";
213
+ var SIZING_ID = "S".repeat(64);
214
+ var SIZING_EXPECTATION = Number.MAX_SAFE_INTEGER;
215
+
211
216
  // ../connector-core/dist/event-wal.js
212
217
  import { assertIdToken } from "@cotal-ai/core";
213
218
 
@@ -215,7 +220,7 @@ import { assertIdToken } from "@cotal-ai/core";
215
220
  import { ensureDirNoSymlink } from "@cotal-ai/core";
216
221
 
217
222
  // ../connector-core/dist/agui-render.js
218
- import { AGUI_EVENT_TYPE as AGUI_EVENT_TYPE3, AGUI_FRAME_KIND as AGUI_FRAME_KIND3, isAguiFramePart as isAguiFramePart3, registry } from "@cotal-ai/core";
223
+ import { AGUI_EVENT_TYPE as AGUI_EVENT_TYPE3, AGUI_FRAME_KIND as AGUI_FRAME_KIND3, isAguiFramePart as isAguiFramePart4, registry } from "@cotal-ai/core";
219
224
  var str = (v) => typeof v === "string" ? v : void 0;
220
225
  var CONT = " \xB7 ";
221
226
  var TEXT_PREFIX = "\xBB ";
@@ -322,7 +327,7 @@ var aguiFramePartRenderer = Object.freeze({
322
327
  kind: "part-renderer",
323
328
  name: AGUI_FRAME_KIND3,
324
329
  render(part) {
325
- if (!isAguiFramePart3(part))
330
+ if (!isAguiFramePart4(part))
326
331
  return `[not an AG-UI frame]`;
327
332
  const events = part.events;
328
333
  if (!Array.isArray(events) || events.length === 0)
@@ -14853,7 +14858,10 @@ function date4(params) {
14853
14858
  config(en_default());
14854
14859
 
14855
14860
  // ../connector-core/dist/tool-specs.js
14856
- import { isConcreteChannel as isConcreteChannel3, channelInAllow as channelInAllow3, AmbiguousPeerError, assertLifecycleToken as assertLifecycleToken4, isPermissionDenied as isPermissionDenied2, renderLifecycleBlocked as renderLifecycleBlocked2, LANG_PROBLEM_DETAIL_KIND } 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";
14857
14865
 
14858
14866
  // ../connector-core/dist/docs.js
14859
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