acp-bridge 0.3.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.
package/dist/daemon.js ADDED
@@ -0,0 +1,1438 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const node_child_process_1 = require("node:child_process");
38
+ const node_crypto_1 = require("node:crypto");
39
+ const node_fs_1 = require("node:fs");
40
+ const node_http_1 = require("node:http");
41
+ const node_https_1 = require("node:https");
42
+ const node_os_1 = require("node:os");
43
+ const node_path_1 = require("node:path");
44
+ const node_stream_1 = require("node:stream");
45
+ const acp = __importStar(require("@agentclientprotocol/sdk"));
46
+ class HttpError extends Error {
47
+ statusCode;
48
+ details;
49
+ constructor(statusCode, message, details) {
50
+ super(message);
51
+ this.statusCode = statusCode;
52
+ this.details = details;
53
+ this.name = "HttpError";
54
+ }
55
+ }
56
+ class BridgeClient {
57
+ getRecord;
58
+ constructor(getRecord) {
59
+ this.getRecord = getRecord;
60
+ }
61
+ async requestPermission(params) {
62
+ const record = this.getRecord();
63
+ if (!record) {
64
+ return { outcome: { outcome: "cancelled" } };
65
+ }
66
+ record.updatedAt = nowIso();
67
+ record.state = "working";
68
+ return new Promise((resolve) => {
69
+ record.pendingPermissions.push({
70
+ requestId: nextPermissionRequestId++,
71
+ params,
72
+ requestedAt: nowIso(),
73
+ resolve,
74
+ });
75
+ });
76
+ }
77
+ async sessionUpdate(params) {
78
+ const record = this.getRecord();
79
+ if (!record) {
80
+ return;
81
+ }
82
+ const update = params.update;
83
+ record.updatedAt = new Date().toISOString();
84
+ if (update.sessionUpdate === "agent_message_chunk") {
85
+ const text = update.content?.type === "text" ? update.content.text : "";
86
+ if (text) {
87
+ record.currentText += text;
88
+ record.lastText = record.currentText;
89
+ publishChunk(record.name, text);
90
+ }
91
+ return;
92
+ }
93
+ if (update.sessionUpdate === "tool_call") {
94
+ record.state = "working";
95
+ return;
96
+ }
97
+ }
98
+ }
99
+ const agents = new Map();
100
+ const tasks = new Map();
101
+ const bridgeConfig = loadConfig();
102
+ const chunkSubscribers = new Map();
103
+ let nextPermissionRequestId = 1;
104
+ const MAX_COMPLETED_TASKS = parsePositiveIntegerEnv("ACP_BRIDGE_MAX_TASKS", 100);
105
+ const TASK_TTL_MS = parsePositiveIntegerEnv("ACP_BRIDGE_TASK_TTL_MS", 3600000);
106
+ const MAX_STDERR_LINES = 50;
107
+ const ENDPOINT_TIMEOUT_MS = 5000;
108
+ function nowIso() {
109
+ return new Date().toISOString();
110
+ }
111
+ function parsePositiveIntegerEnv(name, fallback) {
112
+ const raw = Number(process.env[name] ?? String(fallback));
113
+ if (!Number.isFinite(raw) || raw <= 0) {
114
+ return fallback;
115
+ }
116
+ return Math.floor(raw);
117
+ }
118
+ function expandHomePath(input) {
119
+ if (input.startsWith("~/")) {
120
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), input.slice(2));
121
+ }
122
+ return input;
123
+ }
124
+ function loadConfig() {
125
+ const configPath = (0, node_path_1.join)((0, node_os_1.homedir)(), ".config", "acp-bridge", "config.json");
126
+ if (!(0, node_fs_1.existsSync)(configPath)) {
127
+ return {};
128
+ }
129
+ try {
130
+ const raw = (0, node_fs_1.readFileSync)(configPath, "utf8");
131
+ const parsed = JSON.parse(raw);
132
+ if (!parsed || typeof parsed !== "object") {
133
+ return {};
134
+ }
135
+ return parsed;
136
+ }
137
+ catch (error) {
138
+ process.stderr.write(JSON.stringify({
139
+ ok: false,
140
+ event: "config_error",
141
+ path: configPath,
142
+ error: error instanceof Error ? error.message : String(error),
143
+ }) + "\n");
144
+ return {};
145
+ }
146
+ }
147
+ function pushStderrLine(buffer, line) {
148
+ const normalized = line.trim();
149
+ if (!normalized) {
150
+ return;
151
+ }
152
+ buffer.push(normalized);
153
+ if (buffer.length > MAX_STDERR_LINES) {
154
+ buffer.splice(0, buffer.length - MAX_STDERR_LINES);
155
+ }
156
+ }
157
+ function commandExists(command, env) {
158
+ const expanded = expandHomePath(command);
159
+ if (expanded.includes("/")) {
160
+ return (0, node_fs_1.existsSync)(expanded);
161
+ }
162
+ try {
163
+ (0, node_child_process_1.execSync)(`which ${expanded}`, {
164
+ stdio: "ignore",
165
+ env: env,
166
+ });
167
+ return true;
168
+ }
169
+ catch {
170
+ return false;
171
+ }
172
+ }
173
+ function getTypeBaseUrl(type, env) {
174
+ if (type === "codex") {
175
+ return env.OPENAI_BASE_URL || "https://api.openai.com/v1";
176
+ }
177
+ if (type === "claude") {
178
+ return env.ANTHROPIC_BASE_URL || "https://api.anthropic.com";
179
+ }
180
+ if (type === "gemini") {
181
+ return env.GOOGLE_GEMINI_BASE_URL || "https://generativelanguage.googleapis.com";
182
+ }
183
+ return null;
184
+ }
185
+ function getApiKeyValue(type, env) {
186
+ if (type === "codex") {
187
+ return env.OPENAI_API_KEY?.trim() || null;
188
+ }
189
+ if (type === "claude") {
190
+ return env.ANTHROPIC_API_KEY?.trim() || env.ANTHROPIC_AUTH_TOKEN?.trim() || null;
191
+ }
192
+ if (type === "gemini") {
193
+ return env.GEMINI_API_KEY?.trim() || null;
194
+ }
195
+ return null;
196
+ }
197
+ function getApiKeyRequirement(type) {
198
+ if (type === "codex") {
199
+ return {
200
+ required: true,
201
+ message: "OPENAI_API_KEY is not set. Set it in environment or config.",
202
+ };
203
+ }
204
+ if (type === "claude") {
205
+ return {
206
+ required: true,
207
+ message: "ANTHROPIC_API_KEY is not set. Set it in environment or config.",
208
+ };
209
+ }
210
+ if (type === "gemini") {
211
+ return {
212
+ required: true,
213
+ message: "GEMINI_API_KEY is not set. Set it in environment or config.",
214
+ };
215
+ }
216
+ return { required: false, message: null };
217
+ }
218
+ function apiKeyFormatStatus(type, env) {
219
+ const value = getApiKeyValue(type, env);
220
+ if (!value) {
221
+ const required = getApiKeyRequirement(type).required;
222
+ return required ? "missing" : "not_required";
223
+ }
224
+ if (type === "codex") {
225
+ return value.startsWith("sk-") ? "valid" : "invalid";
226
+ }
227
+ if (type === "claude") {
228
+ return value.startsWith("cr_") || value.startsWith("sk-ant-") ? "valid" : "invalid";
229
+ }
230
+ if (type === "gemini") {
231
+ return value.startsWith("AIza") ? "valid" : "invalid";
232
+ }
233
+ return "unknown";
234
+ }
235
+ function classifyAskError(error) {
236
+ const message = error instanceof Error ? error.message : JSON.stringify(error) ?? String(error);
237
+ const statusMatch = message.match(/\b(401|403|429|500|502|503|504)\b/);
238
+ if (statusMatch) {
239
+ const code = Number(statusMatch[1]);
240
+ if (code === 401 || code === 403) {
241
+ return `[${code}] API key invalid or expired. Check your key. (${message})`;
242
+ }
243
+ if (code === 429) {
244
+ return `[429] Rate limited. Check proxy quota. (${message})`;
245
+ }
246
+ if (code === 500) {
247
+ return `[500] Upstream server error. Likely a proxy or model issue, not local config. (${message})`;
248
+ }
249
+ if (code === 502) {
250
+ return `[502] Bad gateway. Proxy failed to reach upstream. (${message})`;
251
+ }
252
+ if (code === 503) {
253
+ return `[503] Service unavailable. Check proxy status. (${message})`;
254
+ }
255
+ if (code === 504) {
256
+ return `[504] Gateway timeout. Proxy or upstream too slow. (${message})`;
257
+ }
258
+ }
259
+ if (message.includes("ECONNREFUSED")) {
260
+ return `[ECONNREFUSED] Connection refused. Check base URL. (${message})`;
261
+ }
262
+ if (message.includes("ENOTFOUND")) {
263
+ return `[ENOTFOUND] DNS resolution failed. Check network. (${message})`;
264
+ }
265
+ if (message.includes("ETIMEDOUT") || message.includes("timeout")) {
266
+ return `[TIMEOUT] Request timed out. Check network or proxy. (${message})`;
267
+ }
268
+ return message;
269
+ }
270
+ function endpointCheck(urlString) {
271
+ return new Promise((resolve) => {
272
+ let url;
273
+ try {
274
+ url = new URL(urlString);
275
+ }
276
+ catch {
277
+ resolve({
278
+ reachable: false,
279
+ statusCode: null,
280
+ latencyMs: null,
281
+ errorCode: "EINVAL",
282
+ });
283
+ return;
284
+ }
285
+ const start = Date.now();
286
+ const requestImpl = url.protocol === "https:" ? node_https_1.request : node_http_1.request;
287
+ const req = requestImpl({
288
+ method: "HEAD",
289
+ hostname: url.hostname,
290
+ port: url.port || undefined,
291
+ path: `${url.pathname}${url.search}`,
292
+ }, (res) => {
293
+ res.resume();
294
+ res.once("end", () => {
295
+ const latencyMs = Date.now() - start;
296
+ const statusCode = res.statusCode ?? null;
297
+ resolve({
298
+ reachable: statusCode !== null,
299
+ statusCode,
300
+ latencyMs,
301
+ errorCode: null,
302
+ });
303
+ });
304
+ });
305
+ req.setTimeout(ENDPOINT_TIMEOUT_MS, () => {
306
+ req.destroy(Object.assign(new Error("timeout"), { code: "ETIMEDOUT" }));
307
+ });
308
+ req.once("error", (error) => {
309
+ resolve({
310
+ reachable: false,
311
+ statusCode: null,
312
+ latencyMs: null,
313
+ errorCode: error.code || "UNKNOWN",
314
+ });
315
+ });
316
+ req.end();
317
+ });
318
+ }
319
+ async function preflightCheck(type, env) {
320
+ const configuredCommand = env.ACP_BRIDGE_AGENT_COMMAND?.trim();
321
+ if (configuredCommand) {
322
+ if (!commandExists(configuredCommand, env)) {
323
+ throw new HttpError(400, `${configuredCommand} binary not found on PATH.`);
324
+ }
325
+ }
326
+ else if (type === "codex") {
327
+ if (!commandExists("codex-acp", env) && !commandExists("codex", env)) {
328
+ throw new HttpError(400, "codex-acp binary not found on PATH. Install with: cargo install codex-acp");
329
+ }
330
+ }
331
+ else if (type === "claude") {
332
+ if (!commandExists("claude-agent-acp", env)) {
333
+ throw new HttpError(400, "claude-agent-acp binary not found on PATH. Install it globally first.");
334
+ }
335
+ }
336
+ else if (type === "gemini") {
337
+ if (!commandExists("gemini", env)) {
338
+ throw new HttpError(400, "gemini binary not found on PATH. Install @google/gemini-cli.");
339
+ }
340
+ }
341
+ else if (type === "opencode") {
342
+ if (!commandExists("opencode", env)) {
343
+ throw new HttpError(400, "opencode binary not found on PATH. Install OpenCode first.");
344
+ }
345
+ }
346
+ else if (!commandExists(type, env)) {
347
+ throw new HttpError(400, `${type} binary not found on PATH.`);
348
+ }
349
+ const keyRequirement = getApiKeyRequirement(type);
350
+ if (keyRequirement.required && !getApiKeyValue(type, env)) {
351
+ throw new HttpError(400, keyRequirement.message || "required API key is missing");
352
+ }
353
+ const baseUrl = getTypeBaseUrl(type, env);
354
+ if (baseUrl) {
355
+ const endpoint = await endpointCheck(baseUrl);
356
+ if (!endpoint.reachable) {
357
+ const code = endpoint.errorCode || "UNKNOWN";
358
+ throw new HttpError(400, `Proxy ${baseUrl} is unreachable (${code}). Check the URL.`);
359
+ }
360
+ }
361
+ }
362
+ function writeJson(res, status, body) {
363
+ res.statusCode = status;
364
+ res.setHeader("content-type", "application/json; charset=utf-8");
365
+ res.end(JSON.stringify(body));
366
+ }
367
+ function writeSse(res, event, data) {
368
+ res.write(`event: ${event}\n`);
369
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
370
+ }
371
+ function requestUrl(req) {
372
+ return new URL(req.url ?? "/", "http://127.0.0.1");
373
+ }
374
+ function pathParts(req) {
375
+ const pathname = requestUrl(req).pathname;
376
+ return pathname.split("/").filter(Boolean);
377
+ }
378
+ async function readJson(req) {
379
+ const chunks = [];
380
+ for await (const chunk of req) {
381
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
382
+ }
383
+ if (chunks.length === 0) {
384
+ return {};
385
+ }
386
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
387
+ }
388
+ function toStatus(record) {
389
+ return {
390
+ name: record.name,
391
+ type: record.type,
392
+ cwd: record.cwd,
393
+ state: record.state,
394
+ sessionId: record.sessionId,
395
+ protocolVersion: record.protocolVersion,
396
+ lastError: record.lastError,
397
+ recentStderr: [...record.stderrBuffer],
398
+ lastText: record.lastText,
399
+ stopReason: record.stopReason,
400
+ pendingPermissions: record.pendingPermissions.map((item) => ({
401
+ requestId: item.requestId,
402
+ requestedAt: item.requestedAt,
403
+ sessionId: item.params.sessionId,
404
+ options: item.params.options.map((option) => ({
405
+ optionId: option.optionId,
406
+ kind: option.kind,
407
+ name: option.name,
408
+ })),
409
+ })),
410
+ createdAt: record.createdAt,
411
+ updatedAt: record.updatedAt,
412
+ };
413
+ }
414
+ function findPermissionOptionId(params, mode, explicitOptionId) {
415
+ if (explicitOptionId && params.options.some((option) => option.optionId === explicitOptionId)) {
416
+ return explicitOptionId;
417
+ }
418
+ if (mode === "approve") {
419
+ const preferred = params.options.find((option) => option.kind.startsWith("allow"));
420
+ if (preferred) {
421
+ return preferred.optionId;
422
+ }
423
+ }
424
+ else {
425
+ const preferred = params.options.find((option) => option.kind.startsWith("reject"));
426
+ if (preferred) {
427
+ return preferred.optionId;
428
+ }
429
+ }
430
+ return params.options[0]?.optionId ?? null;
431
+ }
432
+ function resolvePendingPermission(record, decision, explicitOptionId) {
433
+ const pending = record.pendingPermissions.shift();
434
+ if (!pending) {
435
+ return null;
436
+ }
437
+ if (decision === "cancel") {
438
+ pending.resolve({ outcome: { outcome: "cancelled" } });
439
+ }
440
+ else {
441
+ const optionId = findPermissionOptionId(pending.params, decision, explicitOptionId);
442
+ if (optionId) {
443
+ pending.resolve({
444
+ outcome: {
445
+ outcome: "selected",
446
+ optionId,
447
+ },
448
+ });
449
+ }
450
+ else {
451
+ pending.resolve({ outcome: { outcome: "cancelled" } });
452
+ }
453
+ }
454
+ record.updatedAt = nowIso();
455
+ return pending;
456
+ }
457
+ function cancelAllPendingPermissions(record) {
458
+ let count = 0;
459
+ while (resolvePendingPermission(record, "cancel")) {
460
+ count += 1;
461
+ }
462
+ return count;
463
+ }
464
+ function isSubtaskTerminal(state) {
465
+ return state === "done" || state === "error" || state === "cancelled";
466
+ }
467
+ function findTaskSubtask(task, subtaskId) {
468
+ return task.subtasks.find((item) => item.id === subtaskId);
469
+ }
470
+ function toTaskStatus(task) {
471
+ return {
472
+ id: task.id,
473
+ name: task.name,
474
+ state: task.state,
475
+ createdAt: task.createdAt,
476
+ updatedAt: task.updatedAt,
477
+ subtasks: task.subtasks.map((subtask) => ({
478
+ id: subtask.id,
479
+ agent: subtask.agent,
480
+ prompt: subtask.prompt,
481
+ dependsOn: subtask.dependsOn,
482
+ state: subtask.state,
483
+ result: subtask.result,
484
+ error: subtask.error,
485
+ createdAt: subtask.createdAt,
486
+ updatedAt: subtask.updatedAt,
487
+ startedAt: subtask.startedAt,
488
+ completedAt: subtask.completedAt,
489
+ })),
490
+ };
491
+ }
492
+ function refreshTaskState(task) {
493
+ if (task.state === "cancelled") {
494
+ task.updatedAt = nowIso();
495
+ return;
496
+ }
497
+ if (task.subtasks.some((item) => item.state === "pending" || item.state === "running")) {
498
+ task.state = "running";
499
+ task.updatedAt = nowIso();
500
+ return;
501
+ }
502
+ if (task.subtasks.length > 0 && task.subtasks.every((item) => item.state === "done")) {
503
+ task.state = "done";
504
+ }
505
+ else if (task.subtasks.some((item) => item.state === "error") &&
506
+ task.subtasks.every((item) => item.state === "done" || item.state === "error" || item.state === "cancelled")) {
507
+ task.state = "error";
508
+ }
509
+ else if (task.subtasks.length > 0 && task.subtasks.every((item) => item.state === "cancelled")) {
510
+ task.state = "cancelled";
511
+ }
512
+ else {
513
+ task.state = "running";
514
+ }
515
+ task.updatedAt = nowIso();
516
+ }
517
+ function isTaskTerminal(state) {
518
+ return state === "done" || state === "error" || state === "cancelled";
519
+ }
520
+ function taskUpdatedAtMs(task) {
521
+ const parsed = Date.parse(task.updatedAt);
522
+ if (!Number.isFinite(parsed)) {
523
+ return 0;
524
+ }
525
+ return parsed;
526
+ }
527
+ function cleanupCompletedTasks() {
528
+ const now = Date.now();
529
+ const terminal = Array.from(tasks.values()).filter((task) => isTaskTerminal(task.state));
530
+ const expiryThreshold = now - TASK_TTL_MS;
531
+ for (const task of terminal) {
532
+ if (taskUpdatedAtMs(task) <= expiryThreshold) {
533
+ tasks.delete(task.id);
534
+ }
535
+ }
536
+ const remainingTerminal = Array.from(tasks.values())
537
+ .filter((task) => isTaskTerminal(task.state))
538
+ .sort((a, b) => taskUpdatedAtMs(a) - taskUpdatedAtMs(b));
539
+ const overflow = remainingTerminal.length - MAX_COMPLETED_TASKS;
540
+ if (overflow <= 0) {
541
+ return;
542
+ }
543
+ for (let i = 0; i < overflow; i += 1) {
544
+ tasks.delete(remainingTerminal[i].id);
545
+ }
546
+ }
547
+ function renderSubtaskPrompt(task, subtask) {
548
+ return subtask.prompt.replace(/\{\{\s*([A-Za-z0-9_-]+)\.result\s*\}\}/g, (_, dependencyId) => {
549
+ const dependency = findTaskSubtask(task, dependencyId);
550
+ return dependency?.result ?? "";
551
+ });
552
+ }
553
+ async function runSubtask(task, subtask) {
554
+ const abortPromise = task.cancelController.signal.aborted
555
+ ? Promise.resolve()
556
+ : new Promise((resolve) => {
557
+ task.cancelController.signal.addEventListener("abort", () => resolve(), { once: true });
558
+ });
559
+ while (subtask.state === "pending") {
560
+ if (task.cancelRequested || task.state === "cancelled") {
561
+ const now = nowIso();
562
+ subtask.state = "cancelled";
563
+ subtask.updatedAt = now;
564
+ subtask.completedAt = now;
565
+ subtask.resolveTerminal();
566
+ refreshTaskState(task);
567
+ return;
568
+ }
569
+ const dependencies = subtask.dependsOn.map((depId) => findTaskSubtask(task, depId)).filter(Boolean);
570
+ const unresolved = dependencies.filter((dep) => !isSubtaskTerminal(dep.state));
571
+ if (unresolved.length === 0) {
572
+ break;
573
+ }
574
+ await Promise.race([
575
+ abortPromise,
576
+ ...unresolved.map((dep) => dep.terminalPromise),
577
+ ]);
578
+ }
579
+ if (subtask.state !== "pending") {
580
+ return;
581
+ }
582
+ const prompt = renderSubtaskPrompt(task, subtask);
583
+ const startTime = nowIso();
584
+ subtask.state = "running";
585
+ subtask.startedAt = startTime;
586
+ subtask.updatedAt = startTime;
587
+ task.updatedAt = startTime;
588
+ try {
589
+ const result = await askAgent(subtask.agent, prompt, undefined, {
590
+ taskId: task.id,
591
+ subtaskId: subtask.id,
592
+ });
593
+ if (subtask.state !== "running") {
594
+ return;
595
+ }
596
+ const doneAt = nowIso();
597
+ subtask.state = "done";
598
+ subtask.result = result.response;
599
+ subtask.error = null;
600
+ subtask.updatedAt = doneAt;
601
+ subtask.completedAt = doneAt;
602
+ subtask.resolveTerminal();
603
+ refreshTaskState(task);
604
+ if (isTaskTerminal(task.state)) {
605
+ cleanupCompletedTasks();
606
+ }
607
+ }
608
+ catch (error) {
609
+ if (subtask.state !== "running") {
610
+ return;
611
+ }
612
+ const errorAt = nowIso();
613
+ subtask.state = "error";
614
+ subtask.error = error instanceof Error ? error.message : JSON.stringify(error) ?? String(error);
615
+ subtask.updatedAt = errorAt;
616
+ subtask.completedAt = errorAt;
617
+ subtask.resolveTerminal();
618
+ refreshTaskState(task);
619
+ if (isTaskTerminal(task.state)) {
620
+ cleanupCompletedTasks();
621
+ }
622
+ }
623
+ }
624
+ async function runTask(taskId) {
625
+ const task = tasks.get(taskId);
626
+ if (!task) {
627
+ return;
628
+ }
629
+ await Promise.allSettled(task.subtasks.map((subtask) => runSubtask(task, subtask)));
630
+ if (task.state === "cancelled") {
631
+ task.updatedAt = nowIso();
632
+ cleanupCompletedTasks();
633
+ return;
634
+ }
635
+ refreshTaskState(task);
636
+ if (isTaskTerminal(task.state)) {
637
+ cleanupCompletedTasks();
638
+ }
639
+ }
640
+ function validateSubtaskGraph(subtasks) {
641
+ const ids = new Set(subtasks.map((subtask) => subtask.id));
642
+ for (const subtask of subtasks) {
643
+ for (const depId of subtask.dependsOn) {
644
+ if (!ids.has(depId)) {
645
+ throw new HttpError(400, `subtask dependency not found: ${depId}`);
646
+ }
647
+ if (depId === subtask.id) {
648
+ throw new HttpError(400, `subtask cannot depend on itself: ${subtask.id}`);
649
+ }
650
+ }
651
+ }
652
+ const visiting = new Set();
653
+ const visited = new Set();
654
+ const byId = new Map(subtasks.map((subtask) => [subtask.id, subtask]));
655
+ const visit = (id) => {
656
+ if (visited.has(id)) {
657
+ return;
658
+ }
659
+ if (visiting.has(id)) {
660
+ throw new HttpError(400, "subtask dependency cycle detected");
661
+ }
662
+ visiting.add(id);
663
+ const subtask = byId.get(id);
664
+ if (subtask) {
665
+ for (const depId of subtask.dependsOn) {
666
+ visit(depId);
667
+ }
668
+ }
669
+ visiting.delete(id);
670
+ visited.add(id);
671
+ };
672
+ for (const subtask of subtasks) {
673
+ visit(subtask.id);
674
+ }
675
+ }
676
+ function createTask(body) {
677
+ const name = typeof body?.name === "string" ? body.name.trim() : "";
678
+ if (!name) {
679
+ throw new HttpError(400, "task name is required");
680
+ }
681
+ if (!Array.isArray(body?.subtasks) || body.subtasks.length === 0) {
682
+ throw new HttpError(400, "task subtasks are required");
683
+ }
684
+ const usedIds = new Set();
685
+ const createdAt = nowIso();
686
+ const subtasks = body.subtasks.map((raw, index) => {
687
+ if (!raw || typeof raw !== "object") {
688
+ throw new HttpError(400, `invalid subtask at index ${index}`);
689
+ }
690
+ const agent = typeof raw.agent === "string" ? raw.agent.trim() : "";
691
+ const prompt = typeof raw.prompt === "string" ? raw.prompt : "";
692
+ if (!agent) {
693
+ throw new HttpError(400, `subtask agent is required at index ${index}`);
694
+ }
695
+ if (!prompt) {
696
+ throw new HttpError(400, `subtask prompt is required at index ${index}`);
697
+ }
698
+ const requestedId = typeof raw.id === "string" ? raw.id.trim() : "";
699
+ const id = requestedId || `subtask-${index + 1}`;
700
+ if (usedIds.has(id)) {
701
+ throw new HttpError(400, `duplicate subtask id: ${id}`);
702
+ }
703
+ usedIds.add(id);
704
+ const dependsOn = Array.isArray(raw.dependsOn)
705
+ ? raw.dependsOn
706
+ .filter((item) => typeof item === "string")
707
+ .map((item) => item.trim())
708
+ .filter((item) => item.length > 0)
709
+ : [];
710
+ let resolveTerminal = () => { };
711
+ const terminalPromise = new Promise((resolve) => {
712
+ resolveTerminal = resolve;
713
+ });
714
+ return {
715
+ id,
716
+ agent,
717
+ prompt,
718
+ dependsOn,
719
+ state: "pending",
720
+ result: null,
721
+ error: null,
722
+ createdAt,
723
+ updatedAt: createdAt,
724
+ startedAt: null,
725
+ completedAt: null,
726
+ terminalPromise,
727
+ resolveTerminal,
728
+ };
729
+ });
730
+ validateSubtaskGraph(subtasks);
731
+ const task = {
732
+ id: (0, node_crypto_1.randomUUID)(),
733
+ name,
734
+ state: "running",
735
+ subtasks,
736
+ createdAt,
737
+ updatedAt: createdAt,
738
+ cancelRequested: false,
739
+ cancelController: new AbortController(),
740
+ };
741
+ tasks.set(task.id, task);
742
+ void runTask(task.id);
743
+ return task;
744
+ }
745
+ async function cancelTask(task) {
746
+ task.cancelRequested = true;
747
+ task.cancelController.abort();
748
+ task.state = "cancelled";
749
+ task.updatedAt = nowIso();
750
+ let cancelledSubtasks = 0;
751
+ const cancelAgents = new Set();
752
+ for (const subtask of task.subtasks) {
753
+ const wasRunning = subtask.state === "running";
754
+ if (subtask.state === "pending" || wasRunning) {
755
+ const cancelledAt = nowIso();
756
+ subtask.state = "cancelled";
757
+ subtask.updatedAt = cancelledAt;
758
+ subtask.completedAt = cancelledAt;
759
+ subtask.resolveTerminal();
760
+ cancelledSubtasks += 1;
761
+ if (wasRunning) {
762
+ cancelAgents.add(subtask.agent);
763
+ }
764
+ }
765
+ }
766
+ for (const agentName of cancelAgents) {
767
+ const record = agents.get(agentName);
768
+ if (!record) {
769
+ continue;
770
+ }
771
+ if (!record.activeTask || record.activeTask.taskId !== task.id) {
772
+ continue;
773
+ }
774
+ try {
775
+ await record.connection.cancel({ sessionId: record.sessionId });
776
+ const cancelledPermissions = cancelAllPendingPermissions(record);
777
+ if (cancelledPermissions > 0 || record.state === "working") {
778
+ record.state = "idle";
779
+ record.updatedAt = nowIso();
780
+ }
781
+ }
782
+ catch {
783
+ // best effort cancel
784
+ }
785
+ }
786
+ cleanupCompletedTasks();
787
+ return { cancelledSubtasks };
788
+ }
789
+ async function spawnAgentConnection(input) {
790
+ let child;
791
+ try {
792
+ child = (0, node_child_process_1.spawn)(input.command, input.args, {
793
+ cwd: input.cwd,
794
+ stdio: ["pipe", "pipe", "pipe"],
795
+ env: input.env,
796
+ });
797
+ }
798
+ catch (error) {
799
+ const message = error instanceof Error ? error.message : String(error);
800
+ throw new HttpError(400, `failed to spawn agent process: ${message}`);
801
+ }
802
+ const spawnError = new Promise((_, reject) => {
803
+ child.once("error", (error) => {
804
+ reject(new HttpError(400, `failed to spawn agent process: ${error.message}`));
805
+ });
806
+ });
807
+ child.stderr.on("data", (data) => {
808
+ const lines = data
809
+ .toString("utf8")
810
+ .split(/\r?\n/g)
811
+ .map((line) => line.trim())
812
+ .filter((line) => line.length > 0);
813
+ for (const line of lines) {
814
+ input.onStderrLine?.(line);
815
+ }
816
+ });
817
+ const stream = acp.ndJsonStream(node_stream_1.Writable.toWeb(child.stdin), node_stream_1.Readable.toWeb(child.stdout));
818
+ const connection = new acp.ClientSideConnection(input.getClient, stream);
819
+ try {
820
+ const init = await Promise.race([
821
+ connection.initialize({
822
+ protocolVersion: acp.PROTOCOL_VERSION,
823
+ clientCapabilities: {},
824
+ }),
825
+ spawnError,
826
+ ]);
827
+ const session = await Promise.race([
828
+ connection.newSession({
829
+ cwd: input.cwd,
830
+ mcpServers: [],
831
+ }),
832
+ spawnError,
833
+ ]);
834
+ return { child, connection, init, session };
835
+ }
836
+ catch (error) {
837
+ child.kill("SIGTERM");
838
+ throw error;
839
+ }
840
+ }
841
+ function subscribeChunks(name, callback) {
842
+ const set = chunkSubscribers.get(name) || new Set();
843
+ set.add(callback);
844
+ chunkSubscribers.set(name, set);
845
+ return () => {
846
+ const current = chunkSubscribers.get(name);
847
+ if (!current) {
848
+ return;
849
+ }
850
+ current.delete(callback);
851
+ if (current.size === 0) {
852
+ chunkSubscribers.delete(name);
853
+ }
854
+ };
855
+ }
856
+ function publishChunk(name, chunk) {
857
+ const subscribers = chunkSubscribers.get(name);
858
+ if (!subscribers || subscribers.size === 0) {
859
+ return;
860
+ }
861
+ for (const callback of subscribers) {
862
+ callback(chunk);
863
+ }
864
+ }
865
+ async function startAgent(input) {
866
+ const type = input.type?.trim() || "opencode";
867
+ const name = input.name?.trim();
868
+ if (!name) {
869
+ throw new Error("Agent name is required");
870
+ }
871
+ if (agents.has(name)) {
872
+ throw new Error(`Agent already exists: ${name}`);
873
+ }
874
+ const cwd = input.cwd || process.cwd();
875
+ const configuredAgent = bridgeConfig.agents?.[type];
876
+ let defaultArgs = [];
877
+ if (type === "opencode") {
878
+ defaultArgs = ["acp"];
879
+ }
880
+ const configuredArgs = configuredAgent?.args && configuredAgent.args.length > 0 ? configuredAgent.args : undefined;
881
+ const requestedArgs = input.args && input.args.length > 0 ? input.args : undefined;
882
+ const opencodeBin = `${(0, node_os_1.homedir)()}/.opencode/bin`;
883
+ const currentPath = process.env.PATH || "";
884
+ const childPath = currentPath ? `${opencodeBin}${node_path_1.delimiter}${currentPath}` : opencodeBin;
885
+ const finalEnv = {
886
+ ...process.env,
887
+ ...(configuredAgent?.env || {}),
888
+ ...(input.env || {}),
889
+ PATH: childPath,
890
+ };
891
+ let record;
892
+ const stderrBuffer = [];
893
+ const client = new BridgeClient(() => record);
894
+ const defaultCommand = input.command || configuredAgent?.command || type;
895
+ const defaultArgsList = requestedArgs || configuredArgs || defaultArgs;
896
+ const useCodexFallback = type === "codex" &&
897
+ !input.command &&
898
+ !configuredAgent?.command &&
899
+ !requestedArgs &&
900
+ !configuredArgs;
901
+ const useClaudeDefault = type === "claude" &&
902
+ !input.command &&
903
+ !configuredAgent?.command &&
904
+ !requestedArgs &&
905
+ !configuredArgs;
906
+ const useGeminiDefault = type === "gemini" &&
907
+ !input.command &&
908
+ !configuredAgent?.command &&
909
+ !requestedArgs &&
910
+ !configuredArgs;
911
+ const candidates = useCodexFallback
912
+ ? [
913
+ { command: "codex-acp", args: [] },
914
+ { command: "codex", args: ["mcp-server"] },
915
+ ]
916
+ : useClaudeDefault
917
+ ? [{ command: "claude-agent-acp", args: [] }]
918
+ : useGeminiDefault
919
+ ? [{ command: "gemini", args: ["--experimental-acp"] }]
920
+ : [{ command: defaultCommand, args: defaultArgsList }];
921
+ await preflightCheck(type, {
922
+ ...finalEnv,
923
+ ACP_BRIDGE_AGENT_COMMAND: input.command || configuredAgent?.command,
924
+ });
925
+ let child;
926
+ let connection;
927
+ let init;
928
+ let session;
929
+ let lastError;
930
+ for (const candidate of candidates) {
931
+ try {
932
+ const result = await spawnAgentConnection({
933
+ name,
934
+ cwd,
935
+ command: expandHomePath(candidate.command),
936
+ args: candidate.args,
937
+ env: finalEnv,
938
+ getClient: () => client,
939
+ onStderrLine: (line) => {
940
+ pushStderrLine(stderrBuffer, line);
941
+ if (record) {
942
+ record.updatedAt = nowIso();
943
+ record.lastError = line;
944
+ }
945
+ },
946
+ });
947
+ child = result.child;
948
+ connection = result.connection;
949
+ init = result.init;
950
+ session = result.session;
951
+ break;
952
+ }
953
+ catch (error) {
954
+ lastError = error;
955
+ }
956
+ }
957
+ if (!child || !connection || !session) {
958
+ throw lastError instanceof Error ? lastError : new HttpError(500, "failed to start agent");
959
+ }
960
+ const created = nowIso();
961
+ record = {
962
+ name,
963
+ type,
964
+ cwd,
965
+ child,
966
+ connection,
967
+ sessionId: session.sessionId,
968
+ state: "idle",
969
+ lastError: null,
970
+ stderrBuffer,
971
+ protocolVersion: typeof init.protocolVersion === "number" || typeof init.protocolVersion === "string"
972
+ ? init.protocolVersion
973
+ : null,
974
+ lastText: "",
975
+ currentText: "",
976
+ stopReason: null,
977
+ pendingPermissions: [],
978
+ activeTask: null,
979
+ createdAt: created,
980
+ updatedAt: created,
981
+ };
982
+ agents.set(name, record);
983
+ child.on("exit", (code, signal) => {
984
+ const target = agents.get(name);
985
+ if (!target) {
986
+ return;
987
+ }
988
+ cancelAllPendingPermissions(target);
989
+ target.updatedAt = nowIso();
990
+ target.state = target.state === "error" ? "error" : "stopped";
991
+ target.lastError = target.lastError ?? `exit code=${code} signal=${signal}`;
992
+ });
993
+ if (init.protocolVersion !== acp.PROTOCOL_VERSION && init.protocolVersion !== 1) {
994
+ record.lastError = `protocol mismatch: ${init.protocolVersion}`;
995
+ }
996
+ return record;
997
+ }
998
+ async function stopAgent(name) {
999
+ const record = agents.get(name);
1000
+ if (!record) {
1001
+ return false;
1002
+ }
1003
+ try {
1004
+ cancelAllPendingPermissions(record);
1005
+ record.state = "stopped";
1006
+ record.updatedAt = nowIso();
1007
+ record.child.kill("SIGTERM");
1008
+ }
1009
+ finally {
1010
+ agents.delete(name);
1011
+ }
1012
+ return true;
1013
+ }
1014
+ function parseAskTimeoutMs() {
1015
+ const raw = Number(process.env.ACP_BRIDGE_ASK_TIMEOUT_MS || "300000");
1016
+ if (!Number.isFinite(raw) || raw <= 0) {
1017
+ return 300000;
1018
+ }
1019
+ return raw;
1020
+ }
1021
+ async function askAgent(name, prompt, onChunk, activeTask) {
1022
+ const record = agents.get(name);
1023
+ if (!record) {
1024
+ throw new Error(`Agent not found: ${name}`);
1025
+ }
1026
+ if (record.state === "working") {
1027
+ throw new Error(`Agent is busy: ${name}`);
1028
+ }
1029
+ record.state = "working";
1030
+ record.updatedAt = nowIso();
1031
+ record.currentText = "";
1032
+ record.stopReason = null;
1033
+ record.activeTask = activeTask ? { taskId: activeTask.taskId, subtaskId: activeTask.subtaskId } : null;
1034
+ const timeoutMs = parseAskTimeoutMs();
1035
+ const unsubscribe = onChunk ? subscribeChunks(name, onChunk) : null;
1036
+ let timeoutHandle = null;
1037
+ try {
1038
+ const response = await Promise.race([
1039
+ record.connection.prompt({
1040
+ sessionId: record.sessionId,
1041
+ prompt: [{ type: "text", text: prompt }],
1042
+ }),
1043
+ new Promise((_, reject) => {
1044
+ timeoutHandle = setTimeout(() => {
1045
+ reject(new HttpError(408, `ask timeout after ${timeoutMs}ms`));
1046
+ }, timeoutMs);
1047
+ }),
1048
+ ]);
1049
+ record.state = "idle";
1050
+ record.stopReason = response.stopReason ?? null;
1051
+ record.lastText = record.currentText;
1052
+ record.updatedAt = nowIso();
1053
+ return {
1054
+ name: record.name,
1055
+ state: record.state,
1056
+ stopReason: record.stopReason,
1057
+ response: record.lastText,
1058
+ };
1059
+ }
1060
+ catch (error) {
1061
+ if (error instanceof HttpError && error.statusCode === 408) {
1062
+ record.state = "idle";
1063
+ record.stopReason = "timeout";
1064
+ record.lastError = error.message;
1065
+ record.updatedAt = nowIso();
1066
+ throw error;
1067
+ }
1068
+ record.state = "error";
1069
+ record.lastError = classifyAskError(error);
1070
+ record.updatedAt = nowIso();
1071
+ throw error;
1072
+ }
1073
+ finally {
1074
+ if (timeoutHandle) {
1075
+ clearTimeout(timeoutHandle);
1076
+ }
1077
+ if (unsubscribe) {
1078
+ unsubscribe();
1079
+ }
1080
+ if (!activeTask ||
1081
+ (record.activeTask &&
1082
+ record.activeTask.taskId === activeTask.taskId &&
1083
+ record.activeTask.subtaskId === activeTask.subtaskId)) {
1084
+ record.activeTask = null;
1085
+ }
1086
+ }
1087
+ }
1088
+ async function runDoctorForType(type, env) {
1089
+ let binary = false;
1090
+ let apiKey = null;
1091
+ let endpoint = null;
1092
+ let message;
1093
+ const commandHint = type === "codex"
1094
+ ? "codex-acp"
1095
+ : type === "claude"
1096
+ ? "claude-agent-acp"
1097
+ : type === "gemini"
1098
+ ? "gemini"
1099
+ : "opencode";
1100
+ binary = commandExists(commandHint, env);
1101
+ if (!binary) {
1102
+ message = `${commandHint} binary not found on PATH`;
1103
+ }
1104
+ const keyRequirement = getApiKeyRequirement(type);
1105
+ if (keyRequirement.required) {
1106
+ apiKey = Boolean(getApiKeyValue(type, env));
1107
+ if (!apiKey && !message) {
1108
+ message = (keyRequirement.message || "required API key is missing").replace(". Set it in environment or config.", "");
1109
+ }
1110
+ }
1111
+ if (binary && (apiKey === null || apiKey)) {
1112
+ const baseUrl = getTypeBaseUrl(type, env);
1113
+ if (baseUrl) {
1114
+ const endpointResult = await endpointCheck(baseUrl);
1115
+ if (endpointResult.reachable) {
1116
+ endpoint = endpointResult.statusCode !== null && endpointResult.statusCode < 500;
1117
+ if (!endpoint && !message && endpointResult.statusCode !== null) {
1118
+ message =
1119
+ endpointResult.statusCode === 503
1120
+ ? "Endpoint returned 503 (service unavailable)"
1121
+ : `Endpoint returned ${endpointResult.statusCode}`;
1122
+ }
1123
+ }
1124
+ else {
1125
+ endpoint = false;
1126
+ if (!message) {
1127
+ message = `Proxy ${baseUrl} is unreachable (${endpointResult.errorCode || "UNKNOWN"})`;
1128
+ }
1129
+ }
1130
+ }
1131
+ }
1132
+ let status = "ok";
1133
+ if (!binary || apiKey === false) {
1134
+ status = "error";
1135
+ }
1136
+ else if (endpoint === false) {
1137
+ status = "warning";
1138
+ }
1139
+ const result = {
1140
+ type,
1141
+ status,
1142
+ binary,
1143
+ apiKey,
1144
+ endpoint,
1145
+ };
1146
+ if (message) {
1147
+ result.message = message;
1148
+ }
1149
+ return result;
1150
+ }
1151
+ async function buildAgentDiagnose(record) {
1152
+ const configured = bridgeConfig.agents?.[record.type];
1153
+ const env = {
1154
+ ...process.env,
1155
+ ...(configured?.env || {}),
1156
+ };
1157
+ const type = record.type;
1158
+ const keyRequirement = getApiKeyRequirement(type);
1159
+ const apiKeySet = keyRequirement.required ? Boolean(getApiKeyValue(type, env)) : true;
1160
+ const apiKeyFormat = apiKeyFormatStatus(type, env);
1161
+ const baseUrl = getTypeBaseUrl(type, env);
1162
+ const endpointResult = baseUrl ? await endpointCheck(baseUrl) : null;
1163
+ const endpointReachable = endpointResult
1164
+ ? endpointResult.reachable && endpointResult.statusCode !== null && endpointResult.statusCode < 500
1165
+ : true;
1166
+ return {
1167
+ agent: record.name,
1168
+ processAlive: !record.child.killed && record.child.exitCode === null,
1169
+ state: record.state,
1170
+ recentStderr: [...record.stderrBuffer],
1171
+ lastError: record.lastError,
1172
+ checks: {
1173
+ apiKeySet,
1174
+ apiKeyFormat,
1175
+ endpointReachable,
1176
+ endpointLatencyMs: endpointResult?.latencyMs ?? null,
1177
+ protocolVersion: record.protocolVersion ?? 1,
1178
+ },
1179
+ };
1180
+ }
1181
+ async function handler(req, res) {
1182
+ try {
1183
+ const method = (req.method || "GET").toUpperCase();
1184
+ const parts = pathParts(req);
1185
+ if (method === "GET" && parts.length === 1 && parts[0] === "health") {
1186
+ writeJson(res, 200, { ok: true, agents: agents.size });
1187
+ return;
1188
+ }
1189
+ if (method === "POST" && parts.length === 1 && parts[0] === "agents") {
1190
+ const body = await readJson(req);
1191
+ const record = await startAgent(body);
1192
+ writeJson(res, 201, toStatus(record));
1193
+ return;
1194
+ }
1195
+ if (method === "GET" && parts.length === 1 && parts[0] === "agents") {
1196
+ writeJson(res, 200, Array.from(agents.values()).map((item) => toStatus(item)));
1197
+ return;
1198
+ }
1199
+ if (method === "GET" && parts.length === 1 && parts[0] === "doctor") {
1200
+ const types = ["codex", "claude", "gemini", "opencode"];
1201
+ const results = await Promise.all(types.map((type) => runDoctorForType(type, process.env)));
1202
+ writeJson(res, 200, { results });
1203
+ return;
1204
+ }
1205
+ if (parts.length === 2 && parts[0] === "agents" && method === "GET") {
1206
+ const record = agents.get(parts[1]);
1207
+ if (!record) {
1208
+ writeJson(res, 404, { error: "not_found" });
1209
+ return;
1210
+ }
1211
+ writeJson(res, 200, toStatus(record));
1212
+ return;
1213
+ }
1214
+ if (parts.length === 3 && parts[0] === "agents" && method === "GET" && parts[2] === "diagnose") {
1215
+ const record = agents.get(parts[1]);
1216
+ if (!record) {
1217
+ writeJson(res, 404, { error: "not_found" });
1218
+ return;
1219
+ }
1220
+ const diagnose = await buildAgentDiagnose(record);
1221
+ writeJson(res, 200, diagnose);
1222
+ return;
1223
+ }
1224
+ if (parts.length === 3 &&
1225
+ parts[0] === "agents" &&
1226
+ method === "POST" &&
1227
+ (parts[2] === "approve" || parts[2] === "deny")) {
1228
+ const record = agents.get(parts[1]);
1229
+ if (!record) {
1230
+ writeJson(res, 404, { error: "not_found" });
1231
+ return;
1232
+ }
1233
+ const body = await readJson(req);
1234
+ const optionId = typeof body.optionId === "string" ? body.optionId : undefined;
1235
+ const pending = resolvePendingPermission(record, parts[2], optionId);
1236
+ if (!pending) {
1237
+ writeJson(res, 409, { error: "no_pending_permissions" });
1238
+ return;
1239
+ }
1240
+ writeJson(res, 200, {
1241
+ ok: true,
1242
+ name: record.name,
1243
+ action: parts[2],
1244
+ requestId: pending.requestId,
1245
+ pendingPermissions: record.pendingPermissions.length,
1246
+ });
1247
+ return;
1248
+ }
1249
+ if (parts.length === 3 && parts[0] === "agents" && method === "POST" && parts[2] === "cancel") {
1250
+ const record = agents.get(parts[1]);
1251
+ if (!record) {
1252
+ writeJson(res, 404, { error: "not_found" });
1253
+ return;
1254
+ }
1255
+ await record.connection.cancel({ sessionId: record.sessionId });
1256
+ const cancelledPermissions = cancelAllPendingPermissions(record);
1257
+ record.updatedAt = nowIso();
1258
+ if (record.state === "working") {
1259
+ record.state = "idle";
1260
+ }
1261
+ writeJson(res, 200, {
1262
+ ok: true,
1263
+ name: record.name,
1264
+ cancelledPermissions,
1265
+ });
1266
+ return;
1267
+ }
1268
+ if (parts.length === 3 && parts[0] === "agents" && method === "POST" && parts[2] === "ask") {
1269
+ const name = parts[1];
1270
+ const body = await readJson(req);
1271
+ if (!body.prompt || typeof body.prompt !== "string") {
1272
+ writeJson(res, 400, { error: "prompt is required" });
1273
+ return;
1274
+ }
1275
+ const stream = requestUrl(req).searchParams.get("stream") === "true";
1276
+ if (!stream) {
1277
+ const result = await askAgent(name, body.prompt);
1278
+ writeJson(res, 200, result);
1279
+ return;
1280
+ }
1281
+ res.statusCode = 200;
1282
+ res.setHeader("content-type", "text/event-stream; charset=utf-8");
1283
+ res.setHeader("cache-control", "no-cache");
1284
+ res.setHeader("connection", "keep-alive");
1285
+ res.flushHeaders();
1286
+ try {
1287
+ const result = await askAgent(name, body.prompt, (chunk) => {
1288
+ writeSse(res, "chunk", { chunk });
1289
+ });
1290
+ writeSse(res, "done", result);
1291
+ }
1292
+ catch (error) {
1293
+ if (error instanceof HttpError) {
1294
+ writeSse(res, "error", { error: error.message, statusCode: error.statusCode });
1295
+ }
1296
+ else {
1297
+ const message = error instanceof Error ? error.message : String(error);
1298
+ writeSse(res, "error", { error: message, statusCode: 500 });
1299
+ }
1300
+ }
1301
+ finally {
1302
+ res.end();
1303
+ }
1304
+ return;
1305
+ }
1306
+ if (parts.length === 2 && parts[0] === "agents" && method === "DELETE") {
1307
+ const ok = await stopAgent(parts[1]);
1308
+ if (!ok) {
1309
+ writeJson(res, 404, { error: "not_found" });
1310
+ return;
1311
+ }
1312
+ writeJson(res, 200, { ok: true });
1313
+ return;
1314
+ }
1315
+ if (method === "POST" && parts.length === 1 && parts[0] === "tasks") {
1316
+ const body = await readJson(req);
1317
+ const task = createTask(body);
1318
+ writeJson(res, 201, toTaskStatus(task));
1319
+ return;
1320
+ }
1321
+ if (method === "GET" && parts.length === 1 && parts[0] === "tasks") {
1322
+ writeJson(res, 200, Array.from(tasks.values()).map((task) => toTaskStatus(task)));
1323
+ return;
1324
+ }
1325
+ if (method === "GET" && parts.length === 2 && parts[0] === "tasks") {
1326
+ const task = tasks.get(parts[1]);
1327
+ if (!task) {
1328
+ writeJson(res, 404, { error: "not_found" });
1329
+ return;
1330
+ }
1331
+ writeJson(res, 200, toTaskStatus(task));
1332
+ return;
1333
+ }
1334
+ if (method === "GET" && parts.length === 4 && parts[0] === "tasks" && parts[2] === "subtasks") {
1335
+ const task = tasks.get(parts[1]);
1336
+ if (!task) {
1337
+ writeJson(res, 404, { error: "not_found" });
1338
+ return;
1339
+ }
1340
+ const subtask = findTaskSubtask(task, parts[3]);
1341
+ if (!subtask) {
1342
+ writeJson(res, 404, { error: "not_found" });
1343
+ return;
1344
+ }
1345
+ writeJson(res, 200, {
1346
+ taskId: task.id,
1347
+ taskName: task.name,
1348
+ taskState: task.state,
1349
+ subtask: {
1350
+ id: subtask.id,
1351
+ agent: subtask.agent,
1352
+ prompt: subtask.prompt,
1353
+ dependsOn: subtask.dependsOn,
1354
+ state: subtask.state,
1355
+ result: subtask.result,
1356
+ error: subtask.error,
1357
+ createdAt: subtask.createdAt,
1358
+ updatedAt: subtask.updatedAt,
1359
+ startedAt: subtask.startedAt,
1360
+ completedAt: subtask.completedAt,
1361
+ },
1362
+ });
1363
+ return;
1364
+ }
1365
+ if (method === "DELETE" && parts.length === 2 && parts[0] === "tasks") {
1366
+ const task = tasks.get(parts[1]);
1367
+ if (!task) {
1368
+ writeJson(res, 404, { error: "not_found" });
1369
+ return;
1370
+ }
1371
+ const cancelled = await cancelTask(task);
1372
+ writeJson(res, 200, {
1373
+ ok: true,
1374
+ id: task.id,
1375
+ state: task.state,
1376
+ cancelledSubtasks: cancelled.cancelledSubtasks,
1377
+ });
1378
+ return;
1379
+ }
1380
+ writeJson(res, 404, { error: "not_found" });
1381
+ }
1382
+ catch (error) {
1383
+ if (error instanceof HttpError) {
1384
+ writeJson(res, error.statusCode, {
1385
+ error: error.message,
1386
+ details: error.details ?? null,
1387
+ });
1388
+ return;
1389
+ }
1390
+ const message = error instanceof Error ? error.message : JSON.stringify(error) ?? String(error);
1391
+ writeJson(res, 500, { error: message });
1392
+ }
1393
+ }
1394
+ function main() {
1395
+ const configuredPort = typeof bridgeConfig.port === "number" && Number.isFinite(bridgeConfig.port)
1396
+ ? bridgeConfig.port
1397
+ : 7800;
1398
+ const port = Number(process.env.ACP_BRIDGE_PORT || String(configuredPort));
1399
+ const host = process.env.ACP_BRIDGE_HOST || (typeof bridgeConfig.host === "string" ? bridgeConfig.host : "127.0.0.1");
1400
+ const server = (0, node_http_1.createServer)((req, res) => {
1401
+ void handler(req, res);
1402
+ });
1403
+ const cleanupInterval = setInterval(() => {
1404
+ cleanupCompletedTasks();
1405
+ }, 60000);
1406
+ server.on("error", (error) => {
1407
+ if (error.code === "EADDRINUSE") {
1408
+ process.stderr.write(JSON.stringify({
1409
+ ok: false,
1410
+ error: `port ${port} is already in use`,
1411
+ hint: `set ACP_BRIDGE_PORT to another value (host: ${host})`,
1412
+ }) + "\n");
1413
+ process.exit(1);
1414
+ }
1415
+ process.stderr.write(JSON.stringify({
1416
+ ok: false,
1417
+ error: error.message,
1418
+ }) + "\n");
1419
+ process.exit(1);
1420
+ });
1421
+ server.listen(port, host, () => {
1422
+ process.stdout.write(JSON.stringify({ ok: true, event: "listening", host, port }) + "\n");
1423
+ });
1424
+ const shutdown = async () => {
1425
+ clearInterval(cleanupInterval);
1426
+ for (const name of Array.from(agents.keys())) {
1427
+ await stopAgent(name);
1428
+ }
1429
+ server.close(() => process.exit(0));
1430
+ };
1431
+ process.on("SIGINT", () => {
1432
+ void shutdown();
1433
+ });
1434
+ process.on("SIGTERM", () => {
1435
+ void shutdown();
1436
+ });
1437
+ }
1438
+ main();