@cotal-ai/connector-opencode 0.11.3 → 0.11.5

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 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,MAAM,EAAS,MAAM,qBAAqB,CAAC;AAiBzD,eAAO,MAAM,KAAK,EAAE,MAkbnB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,MAAM,EAAS,MAAM,qBAAqB,CAAC;AAiBzD,eAAO,MAAM,KAAK,EAAE,MA6anB,CAAC"}
package/dist/serve.js CHANGED
@@ -1,233 +1,170 @@
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 through this exact server 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
- */
1
+ // src/serve.ts
20
2
  import { execFileSync, spawn } from "node:child_process";
21
3
  import { createServer } from "node:net";
22
4
  import { once } from "node:events";
23
5
  import { randomBytes } from "node:crypto";
24
6
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
25
7
  import { delimiter, join } from "node:path";
26
- /** The opencode binary to spawn. `COTAL_OPENCODE_BIN` overrides. On Windows, `opencode` on PATH is
27
- * an npm `.cmd` shim that child_process can't spawn, and the real `opencode.exe` it wraps
28
- * (`<bindir>/node_modules/opencode-ai/bin/opencode.exe`) isn't itself on PATH — resolve and spawn
29
- * it directly, so there's no `cmd.exe` wrapper to orphan the server on kill. POSIX spawns the name
30
- * as-is. Unresolved on Windows → returns the bare name so spawn fails with a clear ENOENT. */
31
8
  function resolveOpencodeBin() {
32
- const override = process.env.COTAL_OPENCODE_BIN?.trim();
33
- if (override)
34
- return override;
35
- if (process.platform !== "win32")
36
- return "opencode";
37
- for (const dir of (process.env.PATH ?? "").split(delimiter)) {
38
- if (!dir)
39
- continue;
40
- for (const exe of [join(dir, "opencode.exe"), join(dir, "node_modules", "opencode-ai", "bin", "opencode.exe")]) {
41
- if (existsSync(exe))
42
- return exe;
43
- }
9
+ const override = process.env.COTAL_OPENCODE_BIN?.trim();
10
+ if (override) return override;
11
+ if (process.platform !== "win32") return "opencode";
12
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
13
+ if (!dir) continue;
14
+ for (const exe of [join(dir, "opencode.exe"), join(dir, "node_modules", "opencode-ai", "bin", "opencode.exe")]) {
15
+ if (existsSync(exe)) return exe;
44
16
  }
45
- return "opencode";
17
+ }
18
+ return "opencode";
46
19
  }
47
- const BIN = resolveOpencodeBin();
48
- const USERNAME = "opencode";
49
- /** Per-launch secret gating the spawned server's HTTP API (see SECURITY above). */
50
- const SECRET = randomBytes(24).toString("hex");
51
- /** Ask the OS for a free port (bind :0, read it, release) so co-located peers don't collide. */
20
+ var BIN = resolveOpencodeBin();
21
+ var USERNAME = "opencode";
22
+ var SECRET = randomBytes(24).toString("hex");
52
23
  async function freePort() {
53
- const srv = createServer();
54
- srv.listen(0, "127.0.0.1");
55
- await once(srv, "listening");
56
- const port = srv.address().port;
57
- await new Promise((r) => srv.close(() => r()));
58
- return port;
24
+ const srv = createServer();
25
+ srv.listen(0, "127.0.0.1");
26
+ await once(srv, "listening");
27
+ const port = srv.address().port;
28
+ await new Promise((r) => srv.close(() => r()));
29
+ return port;
59
30
  }
60
- /** SIGTERM the serve, then SIGKILL if it's still alive 3s later — a lingering serve keeps the
61
- * agent's data dir (SQLite) open and wedges every later same-name spawn. Resolves once it's dead. */
62
31
  async function killServe(serve) {
63
- if (serve.exitCode !== null || serve.signalCode !== null)
64
- return;
65
- serve.kill("SIGTERM");
66
- const dead = await Promise.race([
67
- once(serve, "exit").then(() => true),
68
- new Promise((r) => setTimeout(() => r(false), 3000)),
69
- ]);
70
- if (!dead) {
71
- serve.kill("SIGKILL");
72
- await once(serve, "exit");
73
- }
32
+ if (serve.exitCode !== null || serve.signalCode !== null) return;
33
+ serve.kill("SIGTERM");
34
+ const dead = await Promise.race([
35
+ once(serve, "exit").then(() => true),
36
+ new Promise((r) => setTimeout(() => r(false), 3e3))
37
+ ]);
38
+ if (!dead) {
39
+ serve.kill("SIGKILL");
40
+ await once(serve, "exit");
41
+ }
74
42
  }
75
43
  function processCommand(pid) {
76
- try {
77
- if (process.platform === "win32") {
78
- return execFileSync("powershell.exe", [
79
- "-NoProfile",
80
- "-NonInteractive",
81
- "-Command",
82
- `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`,
83
- ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
84
- }
85
- return execFileSync("ps", ["-p", String(pid), "-o", "command="], {
86
- encoding: "utf8",
87
- stdio: ["ignore", "pipe", "ignore"],
88
- }).trim();
89
- }
90
- catch {
91
- return undefined;
44
+ try {
45
+ if (process.platform === "win32") {
46
+ return execFileSync("powershell.exe", [
47
+ "-NoProfile",
48
+ "-NonInteractive",
49
+ "-Command",
50
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${pid}').CommandLine`
51
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
92
52
  }
53
+ return execFileSync("ps", ["-p", String(pid), "-o", "command="], {
54
+ encoding: "utf8",
55
+ stdio: ["ignore", "pipe", "ignore"]
56
+ }).trim();
57
+ } catch {
58
+ return void 0;
59
+ }
93
60
  }
94
61
  function isLiveOpencodeServe(pid) {
95
- if (!Number.isInteger(pid) || pid <= 0)
96
- return false;
97
- try {
98
- process.kill(pid, 0);
99
- }
100
- catch {
101
- return false;
102
- }
103
- const cmd = processCommand(pid);
104
- if (!cmd)
105
- return true; // If the platform cannot inspect it, keep the old fail-closed behavior.
106
- return /\bserve\b/.test(cmd) && (/(?:^|[\\/\s])opencode(?:\.exe)?(?:\s|$)/i.test(cmd) ||
107
- (/--hostname\s+127\.0\.0\.1\b/.test(cmd) && /--port\s+\d+\b/.test(cmd)));
62
+ if (!Number.isInteger(pid) || pid <= 0) return false;
63
+ try {
64
+ process.kill(pid, 0);
65
+ } catch {
66
+ return false;
67
+ }
68
+ const cmd = processCommand(pid);
69
+ if (!cmd) return true;
70
+ 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));
108
71
  }
109
72
  async function main() {
110
- const port = process.env.COTAL_OPENCODE_PORT?.trim() || String(await freePort());
111
- const url = `http://127.0.0.1:${port}`;
112
- // Own SQLite DB per agent: opencode auth/config stay on the operator's normal HOME/XDG roots,
113
- // while sessions avoid the global DB write lock that stalls concurrent `opencode serve`s.
114
- const name = process.env.COTAL_NAME?.trim() || "agent";
115
- // Data root the connector pins (the manager's workspace, or the launch dir for standalone spawn)
116
- // NOT process.cwd(), which the manager can point at any repo per-agent. Fail loud if it's missing
117
- // rather than silently scattering the DB/pidfile into the launch cwd.
118
- const dataRoot = process.env.COTAL_OPENCODE_HOME?.trim();
119
- if (!dataRoot)
120
- throw new Error("COTAL_OPENCODE_HOME is not set — the connector must pin the agent's data root");
121
- const agentHome = join(dataRoot, ".cotal", "opencode", name);
122
- const dbPath = join(agentHome, "opencode.db");
123
- // Two serves on one agent DB share the SQLite file and stall each other — refuse up front.
124
- const pidFile = join(agentHome, "serve.pid");
125
- if (existsSync(pidFile)) {
126
- const pid = Number(readFileSync(pidFile, "utf8"));
127
- if (isLiveOpencodeServe(pid))
128
- throw new Error(`agent "${name}" is already running (opencode serve pid ${pid}) — kill it first`);
129
- rmSync(pidFile);
73
+ const port = process.env.COTAL_OPENCODE_PORT?.trim() || String(await freePort());
74
+ const url = `http://127.0.0.1:${port}`;
75
+ const name = process.env.COTAL_NAME?.trim() || "agent";
76
+ const dataRoot = process.env.COTAL_OPENCODE_HOME?.trim();
77
+ if (!dataRoot) throw new Error("COTAL_OPENCODE_HOME is not set \u2014 the connector must pin the agent's data root");
78
+ const agentHome = join(dataRoot, ".cotal", "opencode", name);
79
+ const dbPath = join(agentHome, "opencode.db");
80
+ const pidFile = join(agentHome, "serve.pid");
81
+ if (existsSync(pidFile)) {
82
+ const pid = Number(readFileSync(pidFile, "utf8"));
83
+ if (isLiveOpencodeServe(pid))
84
+ throw new Error(`agent "${name}" is already running (opencode serve pid ${pid}) \u2014 kill it first`);
85
+ rmSync(pidFile);
86
+ }
87
+ mkdirSync(agentHome, { recursive: true });
88
+ const serve = spawn(BIN, ["serve", "--hostname", "127.0.0.1", "--port", port], {
89
+ env: {
90
+ ...process.env,
91
+ COTAL_OPENCODE_SERVER_URL: url,
92
+ OPENCODE_SERVER_USERNAME: USERNAME,
93
+ OPENCODE_SERVER_PASSWORD: SECRET,
94
+ OPENCODE_DB: dbPath
95
+ },
96
+ stdio: ["ignore", "pipe", "pipe"]
97
+ });
98
+ writeFileSync(pidFile, String(serve.pid));
99
+ serve.on("exit", () => rmSync(pidFile, { force: true }));
100
+ let sessionId;
101
+ let attached = false;
102
+ let onSession;
103
+ const scan = (d) => {
104
+ if (!attached) process.stderr.write(d);
105
+ if (!sessionId) {
106
+ const m = d.toString().match(/\[cotal-session\] (\S+)/);
107
+ if (m) {
108
+ sessionId = m[1];
109
+ onSession?.(sessionId);
110
+ }
130
111
  }
131
- mkdirSync(agentHome, { recursive: true });
132
- const serve = spawn(BIN, ["serve", "--hostname", "127.0.0.1", "--port", port], {
133
- env: {
134
- ...process.env,
135
- COTAL_OPENCODE_SERVER_URL: url,
136
- OPENCODE_SERVER_USERNAME: USERNAME,
137
- OPENCODE_SERVER_PASSWORD: SECRET,
138
- OPENCODE_DB: dbPath,
139
- },
140
- stdio: ["ignore", "pipe", "pipe"],
141
- });
142
- writeFileSync(pidFile, String(serve.pid));
143
- serve.on("exit", () => rmSync(pidFile, { force: true }));
144
- // Scan the server's output for the plugin's session handshake; forward boot logs to our stderr
145
- // until the TUI takes over the terminal (after that, drop them so they can't corrupt its display).
146
- let sessionId;
147
- let attached = false;
148
- let onSession;
149
- const scan = (d) => {
150
- if (!attached)
151
- process.stderr.write(d);
152
- if (!sessionId) {
153
- const m = d.toString().match(/\[cotal-session\] (\S+)/);
154
- if (m) {
155
- sessionId = m[1];
156
- onSession?.(sessionId);
157
- }
158
- }
159
- };
160
- serve.stdout?.on("data", scan);
161
- serve.stderr?.on("data", scan);
162
- serve.on("exit", (code, signal) => {
163
- if (!attached)
164
- process.exit(code ?? (signal ? 1 : 0)); // died before the TUI came up
165
- });
166
- // Poke the server until the plugin's session handshake lands (lazy plugin load → mesh join +
167
- // session create). Poking must NOT stop at the first 2xx: early in boot the server can answer
168
- // /session before the project instance has bootstrapped, and only a later request triggers the
169
- // bootstrap that loads the plugin. Each poke carries its own abort timeout: a request that
170
- // lands in the early-boot window can hang with no response, and an un-timed fetch would pin
171
- // the loop on it forever (undici queues later requests behind it on the pooled connection).
172
- const auth = `Basic ${Buffer.from(`${USERNAME}:${SECRET}`).toString("base64")}`;
173
- void (async () => {
174
- for (let i = 0; i < 300 && !sessionId; i++) {
175
- try {
176
- await fetch(`${url}/session`, { headers: { authorization: auth }, signal: AbortSignal.timeout(1500) });
177
- }
178
- catch {
179
- /* not up yet (or a hung early request, aborted) — retry on a fresh connection */
180
- }
181
- await new Promise((r) => setTimeout(r, 200));
182
- }
183
- })();
184
- // Wait for the agent's session, then attach a foreground TUI to it.
185
- const id = await new Promise((resolve) => {
186
- if (sessionId)
187
- return resolve(sessionId);
188
- onSession = resolve;
189
- setTimeout(() => resolve(sessionId), 60_000);
190
- });
191
- if (!id) {
192
- process.stderr.write(`[cotal-connector] serve: agent session never came up (~60s) — aborting. Check the boot log above for plugin/mesh errors (OPENCODE_DB=${dbPath})\n`);
193
- await killServe(serve);
194
- process.exit(1);
112
+ };
113
+ serve.stdout?.on("data", scan);
114
+ serve.stderr?.on("data", scan);
115
+ serve.on("exit", (code, signal) => {
116
+ if (!attached) process.exit(code ?? (signal ? 1 : 0));
117
+ });
118
+ const auth = `Basic ${Buffer.from(`${USERNAME}:${SECRET}`).toString("base64")}`;
119
+ void (async () => {
120
+ for (let i = 0; i < 300 && !sessionId; i++) {
121
+ try {
122
+ await fetch(`${url}/session`, { headers: { authorization: auth }, signal: AbortSignal.timeout(1500) });
123
+ } catch {
124
+ }
125
+ await new Promise((r) => setTimeout(r, 200));
195
126
  }
196
- // Headless mode (COTAL_SERVE_HEADLESS=1): no foreground TUI. Hand the running server back to a
197
- // non-terminal host (a web studio, an automated harness) via one machine-readable handshake line
198
- // on stdout, then keep the serve alive. The host drives the session over HTTP (basic auth with the
199
- // password below) and tails its event stream. `attached` stays false, so the `serve.on("exit")`
200
- // handler above still exits us if the server dies; SIGTERM tears the server down for real.
201
- if (process.env.COTAL_SERVE_HEADLESS?.trim() === "1") {
202
- process.stdout.write(`[cotal-serve] ${JSON.stringify({ port: Number(port), session: id, password: SECRET })}\n`);
203
- for (const sig of ["SIGINT", "SIGTERM"])
204
- process.on(sig, () => void killServe(serve).then(() => process.exit(0)));
205
- return;
206
- }
207
- const tuiEnv = {
208
- ...process.env,
209
- OPENCODE_SERVER_USERNAME: USERNAME,
210
- OPENCODE_SERVER_PASSWORD: SECRET,
211
- OPENCODE_DB: dbPath,
212
- };
213
- delete tuiEnv.OPENCODE_CONFIG_CONTENT; // a viewer, not a peer — must NOT load the plugin again
214
- for (const k of Object.keys(tuiEnv))
215
- if (k.startsWith("COTAL_"))
216
- delete tuiEnv[k];
217
- attached = true;
218
- const tui = spawn(BIN, ["attach", url, "--session", id, "--password", SECRET], {
219
- env: tuiEnv,
220
- stdio: "inherit",
221
- });
127
+ })();
128
+ const id = await new Promise((resolve) => {
129
+ if (sessionId) return resolve(sessionId);
130
+ onSession = resolve;
131
+ setTimeout(() => resolve(sessionId), 6e4);
132
+ });
133
+ if (!id) {
134
+ process.stderr.write(
135
+ `[cotal-connector] serve: agent session never came up (~60s) \u2014 aborting. Check the boot log above for plugin/mesh errors (OPENCODE_DB=${dbPath})
136
+ `
137
+ );
138
+ await killServe(serve);
139
+ process.exit(1);
140
+ }
141
+ if (process.env.COTAL_SERVE_HEADLESS?.trim() === "1") {
142
+ process.stdout.write(`[cotal-serve] ${JSON.stringify({ port: Number(port), session: id, password: SECRET })}
143
+ `);
222
144
  for (const sig of ["SIGINT", "SIGTERM"])
223
- process.on(sig, () => {
224
- tui.kill(sig);
225
- serve.kill(sig);
226
- });
227
- tui.on("exit", (code, signal) => {
228
- // TUI closed → tear down the server, for real (SIGKILL fallback), before exiting.
229
- void killServe(serve).then(() => process.exit(code ?? (signal ? 1 : 0)));
145
+ process.on(sig, () => void killServe(serve).then(() => process.exit(0)));
146
+ return;
147
+ }
148
+ const tuiEnv = {
149
+ ...process.env,
150
+ OPENCODE_SERVER_USERNAME: USERNAME,
151
+ OPENCODE_SERVER_PASSWORD: SECRET,
152
+ OPENCODE_DB: dbPath
153
+ };
154
+ delete tuiEnv.OPENCODE_CONFIG_CONTENT;
155
+ for (const k of Object.keys(tuiEnv)) if (k.startsWith("COTAL_")) delete tuiEnv[k];
156
+ attached = true;
157
+ const tui = spawn(BIN, ["attach", url, "--session", id, "--password", SECRET], {
158
+ env: tuiEnv,
159
+ stdio: "inherit"
160
+ });
161
+ for (const sig of ["SIGINT", "SIGTERM"])
162
+ process.on(sig, () => {
163
+ tui.kill(sig);
164
+ serve.kill(sig);
230
165
  });
166
+ tui.on("exit", (code, signal) => {
167
+ void killServe(serve).then(() => process.exit(code ?? (signal ? 1 : 0)));
168
+ });
231
169
  }
232
170
  void main();
233
- //# sourceMappingURL=serve.js.map
package/dist/tools.d.ts CHANGED
@@ -4,9 +4,8 @@
4
4
  * tools (the `tool()` helper). One source of truth → the cotal_* surface can't drift across
5
5
  * adapters: an OpenCode peer gets the same tools (incl. channels / join / leave / channel_info).
6
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.
7
+ * The one OpenCode-specific tool is `cotal_inbox`: automatic traffic remains owned by the driver,
8
+ * while the tool destructively pulls only quiet ambient (plus read-only focus recall).
10
9
  */
11
10
  import { type ToolDefinition } from "@opencode-ai/plugin";
12
11
  import { type MeshAgent, type AgentConfig } from "@cotal-ai/connector-core";
@@ -1 +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"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;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,CA0BrG"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cotal-ai/connector-opencode",
3
3
  "description": "Cotal connector for OpenCode: a native in-process plugin that joins a session to the mesh.",
4
- "version": "0.11.3",
4
+ "version": "0.11.5",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -17,9 +17,6 @@
17
17
  "import": "./dist/index.js"
18
18
  }
19
19
  },
20
- "dependencies": {
21
- "@cotal-ai/connector-core": "0.11.3"
22
- },
23
20
  "peerDependencies": {
24
21
  "@cotal-ai/core": ">=0.1.0",
25
22
  "@opencode-ai/plugin": "^1.16.2",
@@ -30,7 +27,8 @@
30
27
  "@opencode-ai/sdk": "^1.16.2",
31
28
  "esbuild": "^0.28.0",
32
29
  "tsx": "^4.22.4",
33
- "@cotal-ai/core": "0.11.3"
30
+ "@cotal-ai/core": "0.11.5",
31
+ "@cotal-ai/connector-core": "0.11.5"
34
32
  },
35
33
  "files": [
36
34
  "dist"
@@ -40,7 +38,7 @@
40
38
  },
41
39
  "scripts": {
42
40
  "typecheck": "tsc -p tsconfig.json --noEmit",
43
- "build": "tsc -p tsconfig.json && pnpm run bundle",
44
- "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"
41
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json --emitDeclarationOnly && pnpm run bundle",
42
+ "bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:@cotal-ai/core --external:@opencode-ai/plugin --external:@opencode-ai/sdk && esbuild src/plugin.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/plugin.bundle.js --external:@opencode-ai/plugin --external:@opencode-ai/sdk && esbuild src/serve.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/serve.js --external:@opencode-ai/plugin --external:@opencode-ai/sdk"
45
43
  }
46
44
  }