weacpx 0.2.1 → 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.
@@ -56,6 +56,14 @@ function encodeBridgePromptSegmentEvent(event) {
56
56
  return `${JSON.stringify(event)}
57
57
  `;
58
58
  }
59
+ function encodeBridgeSessionProgressEvent(event) {
60
+ return `${JSON.stringify(event)}
61
+ `;
62
+ }
63
+ function encodeBridgeSessionNoteEvent(event) {
64
+ return `${JSON.stringify(event)}
65
+ `;
66
+ }
59
67
 
60
68
  // src/transport/prompt-output.ts
61
69
  function getPromptText(result) {
@@ -205,12 +213,14 @@ var init_spawn_command = __esm(() => {
205
213
  });
206
214
 
207
215
  // src/transport/streaming-prompt.ts
208
- function createStreamingPromptState() {
216
+ function createStreamingPromptState(formatToolCalls = false) {
209
217
  return {
210
218
  buffer: "",
211
219
  segments: [],
212
220
  hasAgentMessage: false,
213
221
  pendingLine: "",
222
+ formatToolCalls,
223
+ emittedToolCallIds: new Set,
214
224
  finalize() {
215
225
  if (this.pendingLine.trim().length > 0) {
216
226
  parseStreamingChunks(this, this.pendingLine);
@@ -242,11 +252,29 @@ function parseStreamingChunks(state, line) {
242
252
  } catch {
243
253
  return;
244
254
  }
245
- const isMessageChunk = event.method === "session/update" && event.params?.update?.sessionUpdate === "agent_message_chunk" && event.params.update.content?.type === "text" && typeof event.params.update.content.text === "string";
255
+ if (event.method !== "session/update")
256
+ return;
257
+ const update = event.params?.update;
258
+ if (!update)
259
+ return;
260
+ if (state.formatToolCalls && (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update")) {
261
+ const formatted = formatToolCallEvent(update, update.sessionUpdate);
262
+ if (formatted) {
263
+ const toolCallId = update.toolCallId;
264
+ if (toolCallId) {
265
+ if (state.emittedToolCallIds.has(toolCallId))
266
+ return;
267
+ state.emittedToolCallIds.add(toolCallId);
268
+ }
269
+ state.segments.push(formatted);
270
+ }
271
+ return;
272
+ }
273
+ const isMessageChunk = update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && typeof update.content.text === "string";
246
274
  if (!isMessageChunk)
247
275
  return;
248
276
  state.hasAgentMessage = true;
249
- const chunk = event.params.update.content.text ?? "";
277
+ const chunk = update.content.text ?? "";
250
278
  if (chunk.length === 0)
251
279
  return;
252
280
  state.buffer += chunk;
@@ -261,211 +289,532 @@ function parseStreamingChunks(state, line) {
261
289
  }
262
290
  }
263
291
  }
264
-
265
- // src/bridge/bridge-main.ts
266
- import { createInterface } from "node:readline";
267
-
268
- // src/bridge/bridge-env.ts
269
- function normalizeBridgePermissionMode(value) {
270
- return value === "approve-reads" || value === "deny-all" || value === "approve-all" ? value : "approve-all";
292
+ function formatToolCallEvent(update, sessionUpdate) {
293
+ if (!update)
294
+ return null;
295
+ const kind = update.kind ?? "";
296
+ const title = update.title ?? "";
297
+ if (title.length === 0)
298
+ return null;
299
+ const emoji = KIND_EMOJI[kind] ?? "\uD83D\uDD27";
300
+ const command = getToolDisplayCommand(update);
301
+ if (command) {
302
+ return `${emoji} ${truncateToolDisplay(command)}`;
303
+ }
304
+ if (sessionUpdate === "tool_call_update" || isGenericToolTitle(kind, title))
305
+ return null;
306
+ return `${emoji} ${title}`;
271
307
  }
272
- function normalizeBridgeNonInteractivePermissions(value) {
273
- return value === "deny" || value === "fail" ? value : "deny";
308
+ function getToolDisplayCommand(update) {
309
+ if (!update)
310
+ return null;
311
+ const command = update.rawInput?.command;
312
+ if (typeof command === "string" && command.length > 0) {
313
+ return command;
314
+ }
315
+ const parsedCmd = update.rawInput?.parsed_cmd;
316
+ if (parsedCmd && parsedCmd.length > 0) {
317
+ const parts = [];
318
+ for (const entry of parsedCmd) {
319
+ if (entry && typeof entry.cmd === "string" && entry.cmd.length > 0) {
320
+ parts.push(entry.cmd);
321
+ }
322
+ }
323
+ if (parts.length > 0) {
324
+ return parts.join(" ");
325
+ }
326
+ }
327
+ return null;
274
328
  }
275
-
276
- // src/bridge/bridge-server.ts
277
- init_prompt_output();
278
-
279
- class BridgeInvalidRequestError extends Error {
329
+ function truncateToolDisplay(text) {
330
+ return text.length > 60 ? `${text.slice(0, 57)}...` : text;
280
331
  }
281
- var BRIDGE_METHODS = new Set([
282
- "ping",
283
- "shutdown",
284
- "updatePermissionPolicy",
285
- "hasSession",
286
- "ensureSession",
287
- "prompt",
288
- "setMode",
289
- "cancel"
290
- ]);
332
+ function isGenericToolTitle(kind, title) {
333
+ const normalizedTitle = title.trim().toLowerCase();
334
+ if (kind === "execute" && ["bash", "shell", "sh", "powershell", "cmd"].includes(normalizedTitle)) {
335
+ return true;
336
+ }
337
+ if (kind === "search" && ["grep", "rg", "search"].includes(normalizedTitle)) {
338
+ return true;
339
+ }
340
+ if (kind === "read" && ["read", "cat"].includes(normalizedTitle)) {
341
+ return true;
342
+ }
343
+ return false;
344
+ }
345
+ var KIND_EMOJI;
346
+ var init_streaming_prompt = __esm(() => {
347
+ KIND_EMOJI = {
348
+ read: "\uD83D\uDCD6",
349
+ search: "\uD83D\uDD0D",
350
+ execute: "\uD83D\uDCBB",
351
+ edit: "✏️"
352
+ };
353
+ });
291
354
 
292
- class BridgeServer {
293
- runtime;
294
- constructor(runtime) {
295
- this.runtime = runtime;
355
+ // src/recovery/discover-parent-package-paths.ts
356
+ import { spawn } from "node:child_process";
357
+ import { access } from "node:fs/promises";
358
+ import { homedir } from "node:os";
359
+ import { dirname, join } from "node:path";
360
+ function deriveParentPackageName(platformPackage) {
361
+ return platformPackage.replace(/-(?:linux|darwin|win32|windows|freebsd|openbsd|sunos|aix)(?:-(?:x64|arm64|ia32|arm|ppc64|s390x))?(?:-(?:baseline|musl|gnu|gnueabihf|musleabihf|msvc))?$/, "");
362
+ }
363
+ async function discoverParentPackagePaths(platformPackage, seedPath, deps = {}) {
364
+ const env = deps.env ?? process.env;
365
+ const home = deps.home ?? homedir();
366
+ const cwd = deps.cwd ?? process.cwd();
367
+ const fsExists = deps.fsExists ?? defaultFsExists;
368
+ const resolveFromCwd = deps.resolveFromCwd ?? defaultResolveFromCwd;
369
+ const queryRoot = deps.queryPackageManagerRoot ?? defaultQueryPackageManagerRoot;
370
+ const parentName = deriveParentPackageName(platformPackage);
371
+ const rawCandidates = [];
372
+ const bunGlobalRoot = env.BUN_INSTALL ? join(env.BUN_INSTALL, "install", "global", "node_modules") : join(home, ".bun", "install", "global", "node_modules");
373
+ const [npmRoot, pnpmRoot, yarnRoot] = await Promise.all([
374
+ queryRoot("npm"),
375
+ queryRoot("pnpm"),
376
+ queryRoot("yarn")
377
+ ]);
378
+ const classify = (p) => {
379
+ if (isUnder(p, bunGlobalRoot))
380
+ return "bun";
381
+ if (pnpmRoot && isUnder(p, pnpmRoot))
382
+ return "pnpm";
383
+ if (yarnRoot && isUnder(p, yarnRoot))
384
+ return "yarn";
385
+ return "npm";
386
+ };
387
+ if (seedPath) {
388
+ rawCandidates.push({ path: seedPath, manager: classify(seedPath) });
296
389
  }
297
- async handleLine(line, writeLine) {
298
- let requestId = extractRequestId(line);
299
- try {
300
- const request = parseBridgeRequest(line);
301
- requestId = request.id;
302
- const result = await this.dispatch(request.id, request.method, request.params, writeLine);
303
- return `${JSON.stringify({
304
- id: request.id,
305
- ok: true,
306
- result
307
- })}
308
- `;
309
- } catch (error) {
310
- const message = error instanceof Error ? error.message : String(error);
311
- return `${JSON.stringify({
312
- id: requestId,
313
- ok: false,
314
- error: {
315
- code: error instanceof BridgeInvalidRequestError ? "BRIDGE_INVALID_REQUEST" : "BRIDGE_INTERNAL_ERROR",
316
- message,
317
- ...error instanceof PromptCommandError ? {
318
- details: {
319
- exitCode: error.exitCode,
320
- stdout: error.stdout,
321
- stderr: error.stderr
322
- }
323
- } : {}
324
- }
325
- })}
326
- `;
327
- }
390
+ for (const name of [parentName, platformPackage]) {
391
+ const resolved = resolveFromCwd(name, cwd);
392
+ if (resolved)
393
+ rawCandidates.push({ path: resolved, manager: classify(resolved) });
328
394
  }
329
- async dispatch(requestId, method, params, writeLine) {
330
- switch (method) {
331
- case "ping":
332
- return {};
333
- case "shutdown":
334
- return await this.runtime.shutdown();
335
- case "updatePermissionPolicy":
336
- return await this.runtime.updatePermissionPolicy({
337
- permissionMode: requirePermissionMode(params, "permissionMode"),
338
- nonInteractivePermissions: requireNonInteractivePermissions(params, "nonInteractivePermissions")
339
- });
340
- case "hasSession":
341
- return await this.runtime.hasSession({
342
- agent: requireString(params, "agent"),
343
- agentCommand: asOptionalString(params.agentCommand),
344
- cwd: requireString(params, "cwd"),
345
- name: requireString(params, "name")
346
- });
347
- case "ensureSession":
348
- return await this.runtime.ensureSession({
349
- agent: requireString(params, "agent"),
350
- agentCommand: asOptionalString(params.agentCommand),
351
- cwd: requireString(params, "cwd"),
352
- name: requireString(params, "name")
353
- });
354
- case "prompt":
355
- return await this.runtime.prompt({
356
- agent: requireString(params, "agent"),
357
- agentCommand: asOptionalString(params.agentCommand),
358
- cwd: requireString(params, "cwd"),
359
- name: requireString(params, "name"),
360
- text: requireString(params, "text")
361
- }, (event) => {
362
- if (event.type === "prompt.segment") {
363
- writeLine?.(encodeBridgePromptSegmentEvent({
364
- id: requestId,
365
- event: "prompt.segment",
366
- text: event.text
367
- }));
368
- }
369
- });
370
- case "setMode":
371
- return await this.runtime.setMode({
372
- agent: requireString(params, "agent"),
373
- agentCommand: asOptionalString(params.agentCommand),
374
- cwd: requireString(params, "cwd"),
375
- name: requireString(params, "name"),
376
- modeId: requireString(params, "modeId")
377
- });
378
- case "cancel":
379
- return await this.runtime.cancel({
380
- agent: requireString(params, "agent"),
381
- agentCommand: asOptionalString(params.agentCommand),
382
- cwd: requireString(params, "cwd"),
383
- name: requireString(params, "name")
384
- });
385
- default:
386
- throw new Error(`unsupported bridge method: ${method}`);
395
+ rawCandidates.push({ path: join(bunGlobalRoot, parentName), manager: "bun" });
396
+ if (npmRoot)
397
+ rawCandidates.push({ path: join(npmRoot, parentName), manager: "npm" });
398
+ if (pnpmRoot)
399
+ rawCandidates.push({ path: join(pnpmRoot, parentName), manager: "pnpm" });
400
+ if (yarnRoot)
401
+ rawCandidates.push({ path: join(yarnRoot, parentName), manager: "yarn" });
402
+ const seen = new Set;
403
+ const verified = [];
404
+ for (const candidate of rawCandidates) {
405
+ if (seen.has(candidate.path))
406
+ continue;
407
+ seen.add(candidate.path);
408
+ if (await fsExists(join(candidate.path, "package.json"))) {
409
+ verified.push(candidate);
387
410
  }
388
411
  }
412
+ return verified;
389
413
  }
390
- function extractRequestId(line) {
414
+ function isUnder(child, parent) {
415
+ const c = child.replace(/[\\/]+$/, "");
416
+ const p = parent.replace(/[\\/]+$/, "");
417
+ return c === p || c.startsWith(p + "/") || c.startsWith(p + "\\");
418
+ }
419
+ async function defaultFsExists(path) {
391
420
  try {
392
- const raw = JSON.parse(line);
393
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
394
- return "unknown";
395
- }
396
- const id = raw.id;
397
- return typeof id === "string" && id.length > 0 ? id : "unknown";
421
+ await access(path);
422
+ return true;
398
423
  } catch {
399
- return "unknown";
424
+ return false;
400
425
  }
401
426
  }
402
- function parseBridgeRequest(line) {
403
- let raw;
427
+ function defaultResolveFromCwd(name, cwd) {
404
428
  try {
405
- raw = JSON.parse(line);
429
+ const pkgJson = __require.resolve(`${name}/package.json`, {
430
+ paths: [cwd, ...__require.resolve.paths(name) ?? []]
431
+ });
432
+ return dirname(pkgJson);
406
433
  } catch {
407
- throw new BridgeInvalidRequestError("request must be valid JSON");
434
+ return null;
408
435
  }
409
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
410
- throw new BridgeInvalidRequestError("request must be a JSON object");
436
+ }
437
+ async function defaultQueryPackageManagerRoot(tool) {
438
+ const spec = tool === "yarn" ? { cmd: "yarn", args: ["global", "dir"], postfix: "node_modules" } : { cmd: tool, args: ["root", "-g"], postfix: null };
439
+ return await new Promise((resolve) => {
440
+ let settled = false;
441
+ const done = (value) => {
442
+ if (settled)
443
+ return;
444
+ settled = true;
445
+ resolve(value);
446
+ };
447
+ let child;
448
+ try {
449
+ child = spawn(spec.cmd, spec.args, {
450
+ stdio: ["ignore", "pipe", "pipe"],
451
+ shell: process.platform === "win32"
452
+ });
453
+ } catch {
454
+ done(null);
455
+ return;
456
+ }
457
+ let stdout = "";
458
+ const timer = setTimeout(() => {
459
+ try {
460
+ child.kill();
461
+ } catch {}
462
+ done(null);
463
+ }, 2000);
464
+ timer.unref?.();
465
+ child.stdout.on("data", (chunk) => {
466
+ stdout += String(chunk);
467
+ });
468
+ child.on("error", () => {
469
+ clearTimeout(timer);
470
+ done(null);
471
+ });
472
+ child.on("close", (code) => {
473
+ clearTimeout(timer);
474
+ if (code !== 0)
475
+ return done(null);
476
+ const trimmed = stdout.trim().split(/\r?\n/).pop()?.trim() ?? "";
477
+ if (!trimmed)
478
+ return done(null);
479
+ done(spec.postfix ? join(trimmed, spec.postfix) : trimmed);
480
+ });
481
+ });
482
+ }
483
+ var init_discover_parent_package_paths = () => {};
484
+
485
+ // src/process/terminate-process-tree.ts
486
+ import { spawn as spawn2 } from "node:child_process";
487
+ async function terminateProcessTree(pid, platform = process.platform, runCommand = defaultRunProcessCommand, killProcess = (targetPid, signal) => {
488
+ process.kill(targetPid, signal);
489
+ }, isProcessRunning = defaultIsProcessRunning) {
490
+ if (pid <= 0) {
491
+ return;
411
492
  }
412
- const request = raw;
413
- const id = request.id;
414
- const method = request.method;
415
- const params = request.params;
416
- if (typeof id !== "string" || id.length === 0) {
417
- throw new BridgeInvalidRequestError("id must be a non-empty string");
493
+ if (platform === "win32") {
494
+ try {
495
+ await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
496
+ } catch {}
497
+ return;
418
498
  }
419
- if (typeof method !== "string" || method.length === 0) {
420
- throw new BridgeInvalidRequestError("method must be a non-empty string");
499
+ const targetPid = pid > 0 ? -pid : pid;
500
+ try {
501
+ killProcess(targetPid, "SIGTERM");
502
+ } catch {
503
+ return;
421
504
  }
422
- if (!BRIDGE_METHODS.has(method)) {
423
- throw new BridgeInvalidRequestError(`unsupported bridge method: ${method}`);
505
+ const deadline = Date.now() + 5000;
506
+ while (Date.now() < deadline) {
507
+ if (!isProcessRunning(targetPid)) {
508
+ return;
509
+ }
510
+ await new Promise((resolve) => setTimeout(resolve, 100));
424
511
  }
425
- if (!params || typeof params !== "object" || Array.isArray(params)) {
426
- throw new BridgeInvalidRequestError("params must be an object");
512
+ try {
513
+ killProcess(targetPid, "SIGKILL");
514
+ } catch {}
515
+ }
516
+ function defaultIsProcessRunning(pid) {
517
+ try {
518
+ process.kill(pid, 0);
519
+ return true;
520
+ } catch {
521
+ return false;
427
522
  }
523
+ }
524
+ async function defaultRunProcessCommand(command, args) {
525
+ return await new Promise((resolve, reject) => {
526
+ const child = spawn2(command, args, { stdio: "ignore" });
527
+ child.on("error", reject);
528
+ child.on("close", (code) => resolve(code ?? 1));
529
+ });
530
+ }
531
+ var init_terminate_process_tree = () => {};
532
+
533
+ // src/transport/acpx-queue-owner-launcher.ts
534
+ import { createHash } from "node:crypto";
535
+ import { spawn as spawn3 } from "node:child_process";
536
+ import { readFile, unlink } from "node:fs/promises";
537
+ import { homedir as homedir2 } from "node:os";
538
+ import { join as join2 } from "node:path";
539
+ function buildWeacpxMcpServerSpec(input) {
540
+ const { command, args } = splitCommandLine(input.weacpxCommand);
428
541
  return {
429
- id,
430
- method,
431
- params
542
+ name: "weacpx-orchestration",
543
+ type: "stdio",
544
+ command,
545
+ args: [
546
+ ...args,
547
+ "mcp-stdio",
548
+ "--coordinator-session",
549
+ input.coordinatorSession,
550
+ ...input.sourceHandle ? ["--source-handle", input.sourceHandle] : []
551
+ ]
432
552
  };
433
553
  }
434
- function requireString(params, key) {
435
- const value = params[key];
436
- if (typeof value !== "string" || value.length === 0) {
437
- throw new BridgeInvalidRequestError(`${key} must be a non-empty string`);
438
- }
439
- return value;
554
+ function buildQueueOwnerPayload(input) {
555
+ return {
556
+ sessionId: input.sessionId,
557
+ permissionMode: input.permissionMode,
558
+ nonInteractivePermissions: input.nonInteractivePermissions,
559
+ ttlMs: input.ttlMs ?? 300000,
560
+ maxQueueDepth: input.maxQueueDepth ?? 16,
561
+ mcpServers: input.mcpServers
562
+ };
440
563
  }
441
- function requirePermissionMode(params, key) {
442
- const value = params[key];
443
- if (value === "approve-all" || value === "approve-reads" || value === "deny-all") {
444
- return value;
564
+
565
+ class AcpxQueueOwnerLauncher {
566
+ acpxCommand;
567
+ weacpxCommand;
568
+ spawnOwner;
569
+ terminateOwner;
570
+ baseEnv;
571
+ ttlMs;
572
+ maxQueueDepth;
573
+ launchLocks = new Map;
574
+ constructor(options) {
575
+ this.acpxCommand = options.acpxCommand;
576
+ this.weacpxCommand = options.weacpxCommand ?? resolveDefaultWeacpxCommand(options.baseEnv ?? process.env);
577
+ this.spawnOwner = options.spawnOwner ?? defaultQueueOwnerSpawner;
578
+ this.terminateOwner = options.terminateOwner ?? createDefaultQueueOwnerTerminator(options.acpxCommand);
579
+ this.baseEnv = options.baseEnv ?? process.env;
580
+ this.ttlMs = options.ttlMs;
581
+ this.maxQueueDepth = options.maxQueueDepth;
582
+ }
583
+ async launch(input) {
584
+ const key = input.acpxRecordId;
585
+ const previous = this.launchLocks.get(key) ?? Promise.resolve();
586
+ const next = previous.then(() => this.doLaunch(input), () => this.doLaunch(input));
587
+ this.launchLocks.set(key, next.catch(() => {}));
588
+ return next;
589
+ }
590
+ async doLaunch(input) {
591
+ await this.terminateOwner(input.acpxRecordId);
592
+ const payload = buildQueueOwnerPayload({
593
+ sessionId: input.acpxRecordId,
594
+ permissionMode: input.permissionMode,
595
+ nonInteractivePermissions: input.nonInteractivePermissions,
596
+ ttlMs: this.ttlMs,
597
+ maxQueueDepth: this.maxQueueDepth,
598
+ mcpServers: [buildWeacpxMcpServerSpec({
599
+ weacpxCommand: this.weacpxCommand,
600
+ coordinatorSession: input.coordinatorSession,
601
+ ...input.sourceHandle ? { sourceHandle: input.sourceHandle } : {}
602
+ })]
603
+ });
604
+ const spawnSpec = resolveSpawnCommand(this.acpxCommand, ["__queue-owner"]);
605
+ await this.spawnOwner(spawnSpec.command, spawnSpec.args, {
606
+ env: {
607
+ ...stringEnv(this.baseEnv),
608
+ ACPX_QUEUE_OWNER_PAYLOAD: JSON.stringify(payload)
609
+ }
610
+ });
445
611
  }
446
- throw new BridgeInvalidRequestError(`${key} must be approve-all, approve-reads, or deny-all`);
447
612
  }
448
- function requireNonInteractivePermissions(params, key) {
449
- const value = params[key];
450
- if (value === "deny" || value === "fail") {
451
- return value;
613
+ function splitCommandLine(value) {
614
+ const parts = [];
615
+ let current = "";
616
+ let quote = null;
617
+ let escaping = false;
618
+ for (const char of value) {
619
+ if (escaping) {
620
+ current += char;
621
+ escaping = false;
622
+ continue;
623
+ }
624
+ if (char === "\\" && quote !== "'") {
625
+ escaping = true;
626
+ continue;
627
+ }
628
+ if (quote) {
629
+ if (char === quote) {
630
+ quote = null;
631
+ } else {
632
+ current += char;
633
+ }
634
+ continue;
635
+ }
636
+ if (char === "'" || char === '"') {
637
+ quote = char;
638
+ continue;
639
+ }
640
+ if (/\s/.test(char)) {
641
+ if (current.length > 0) {
642
+ parts.push(current);
643
+ current = "";
644
+ }
645
+ continue;
646
+ }
647
+ current += char;
452
648
  }
453
- throw new BridgeInvalidRequestError(`${key} must be deny or fail`);
649
+ if (escaping) {
650
+ current += "\\";
651
+ }
652
+ if (quote) {
653
+ throw new Error("weacpx MCP command has an unterminated quote");
654
+ }
655
+ if (current.length > 0) {
656
+ parts.push(current);
657
+ }
658
+ if (parts.length === 0) {
659
+ throw new Error("weacpx MCP command must not be empty");
660
+ }
661
+ return { command: parts[0], args: parts.slice(1) };
454
662
  }
455
- function asOptionalString(value) {
456
- if (typeof value !== "string" || value.length === 0) {
663
+ function stringEnv(env) {
664
+ return Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string"));
665
+ }
666
+ async function defaultQueueOwnerSpawner(command, args, options) {
667
+ await new Promise((resolve, reject) => {
668
+ const child = spawn3(command, args, {
669
+ detached: true,
670
+ stdio: "ignore",
671
+ env: options.env,
672
+ windowsHide: true
673
+ });
674
+ child.once("error", reject);
675
+ child.once("spawn", () => {
676
+ child.unref();
677
+ resolve();
678
+ });
679
+ });
680
+ }
681
+ function createDefaultQueueOwnerTerminator(_acpxCommand) {
682
+ return async (sessionId) => {
683
+ await terminateAcpxQueueOwner(sessionId);
684
+ };
685
+ }
686
+ async function terminateAcpxQueueOwner(sessionId) {
687
+ const lockPath = queueLockFilePath(sessionId);
688
+ let owner;
689
+ try {
690
+ owner = JSON.parse(await readFile(lockPath, "utf8"));
691
+ } catch {
457
692
  return;
458
693
  }
459
- return value;
694
+ if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0) {
695
+ await terminateProcessTree(owner.pid);
696
+ }
697
+ await unlink(lockPath).catch(() => {});
460
698
  }
461
-
699
+ function queueLockFilePath(sessionId) {
700
+ return join2(homedir2(), ".acpx", "queues", `${shortHash(sessionId, 24)}.lock`);
701
+ }
702
+ function shortHash(value, length) {
703
+ return createHash("sha256").update(value).digest("hex").slice(0, length);
704
+ }
705
+ function resolveDefaultWeacpxCommand(env) {
706
+ if (env.WEACPX_CLI_COMMAND?.trim()) {
707
+ return env.WEACPX_CLI_COMMAND.trim();
708
+ }
709
+ if (env.WEACPX_DAEMON_ARG0?.trim()) {
710
+ return `${quoteCommandPart(process.execPath)} ${quoteCommandPart(env.WEACPX_DAEMON_ARG0.trim())}`;
711
+ }
712
+ if (process.argv[1]) {
713
+ return `${quoteCommandPart(process.execPath)} ${quoteCommandPart(process.argv[1])}`;
714
+ }
715
+ return "weacpx";
716
+ }
717
+ function quoteCommandPart(value) {
718
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
719
+ }
720
+ var init_acpx_queue_owner_launcher = __esm(() => {
721
+ init_spawn_command();
722
+ init_terminate_process_tree();
723
+ });
724
+
725
+ // src/transport/permission-mode-flag.ts
726
+ function permissionModeToFlag(permissionMode) {
727
+ switch (permissionMode) {
728
+ case "approve-reads":
729
+ return "--approve-reads";
730
+ case "deny-all":
731
+ return "--deny-all";
732
+ case "approve-all":
733
+ return "--approve-all";
734
+ }
735
+ }
736
+
737
+ // src/bridge/bridge-main.ts
738
+ import { createInterface } from "node:readline";
739
+
740
+ // src/bridge/bridge-env.ts
741
+ function normalizeBridgePermissionMode(value) {
742
+ return value === "approve-reads" || value === "deny-all" || value === "approve-all" ? value : "approve-all";
743
+ }
744
+ function normalizeBridgeNonInteractivePermissions(value) {
745
+ return value === "deny" || value === "fail" ? value : "deny";
746
+ }
747
+
748
+ // src/bridge/bridge-server.ts
749
+ init_prompt_output();
750
+
751
+ // src/bridge/bridge-request-scheduler.ts
752
+ class BridgeRequestScheduler {
753
+ sessions = new Map;
754
+ run(sessionName, lane, task) {
755
+ if (lane === "control") {
756
+ return Promise.resolve().then(task);
757
+ }
758
+ const state = this.sessions.get(sessionName) ?? this.createSessionState(sessionName);
759
+ state.pendingNormals += 1;
760
+ const result = state.tail.then(() => task());
761
+ state.tail = result.then(() => {
762
+ return;
763
+ }, () => {
764
+ return;
765
+ });
766
+ return result.finally(() => {
767
+ state.pendingNormals -= 1;
768
+ if (state.pendingNormals === 0 && this.sessions.get(sessionName) === state) {
769
+ this.sessions.delete(sessionName);
770
+ }
771
+ });
772
+ }
773
+ createSessionState(sessionName) {
774
+ const state = {
775
+ pendingNormals: 0,
776
+ tail: Promise.resolve()
777
+ };
778
+ this.sessions.set(sessionName, state);
779
+ return state;
780
+ }
781
+ }
782
+
462
783
  // src/bridge/bridge-runtime.ts
463
784
  init_spawn_command();
464
785
  init_prompt_output();
786
+ init_streaming_prompt();
465
787
  import { copyFile, readdir } from "node:fs/promises";
466
- import { homedir } from "node:os";
467
- import { join } from "node:path";
468
- import { spawn } from "node:child_process";
788
+ import { homedir as homedir3 } from "node:os";
789
+ import { dirname as dirname2, join as join3, win32 } from "node:path";
790
+ import { spawn as spawn4 } from "node:child_process";
791
+
792
+ // src/bridge/parse-missing-optional-dep.ts
793
+ var PATTERN = /You can try manually installing ["']([^"']+)["']/;
794
+ var VALID_NAME = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
795
+ function parseMissingOptionalDep(text) {
796
+ const match = PATTERN.exec(text);
797
+ if (!match || !match[1])
798
+ return null;
799
+ const pkg = match[1];
800
+ if (!VALID_NAME.test(pkg))
801
+ return null;
802
+ return { package: pkg };
803
+ }
804
+
805
+ // src/bridge/bridge-runtime.ts
806
+ init_discover_parent_package_paths();
807
+ init_acpx_queue_owner_launcher();
808
+ class EnsureSessionFailedError extends Error {
809
+ kind;
810
+ data;
811
+ constructor(message, kind, data) {
812
+ super(message);
813
+ this.name = "EnsureSessionFailedError";
814
+ this.kind = kind;
815
+ this.data = data;
816
+ }
817
+ }
469
818
 
470
819
  class BridgeRuntime {
471
820
  command;
@@ -473,12 +822,19 @@ class BridgeRuntime {
473
822
  runSessionCreate;
474
823
  options;
475
824
  runPromptCommand;
476
- constructor(command = "acpx", run = defaultRunner, runSessionCreate = shellSessionCreateRunner, options = {}, runPromptCommand = defaultPromptRunner) {
825
+ repairSessionIndex;
826
+ queueOwnerLauncher;
827
+ acpxVerboseSupported = undefined;
828
+ constructor(command = "acpx", run = defaultRunner, runSessionCreate = shellSessionCreateRunner, options = {}, runPromptCommand = defaultPromptRunner, repairSessionIndex = tryRepairAcpxSessionIndex, queueOwnerLauncher = new AcpxQueueOwnerLauncher({
829
+ acpxCommand: command
830
+ })) {
477
831
  this.command = command;
478
832
  this.run = run;
479
833
  this.runSessionCreate = runSessionCreate;
480
834
  this.options = options;
481
835
  this.runPromptCommand = runPromptCommand;
836
+ this.repairSessionIndex = repairSessionIndex;
837
+ this.queueOwnerLauncher = queueOwnerLauncher;
482
838
  }
483
839
  async updatePermissionPolicy(policy) {
484
840
  this.options.permissionMode = policy.permissionMode;
@@ -494,46 +850,137 @@ class BridgeRuntime {
494
850
  const result = await this.run(spawnSpec.command, spawnSpec.args);
495
851
  return { exists: result.code === 0 };
496
852
  }
497
- async ensureSession(input) {
498
- const ensuredSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
499
- "sessions",
500
- "ensure",
501
- "--name",
502
- input.name
503
- ]));
504
- const ensured = await this.run(ensuredSpec.command, ensuredSpec.args);
853
+ async ensureSession(input, onProgress) {
854
+ onProgress?.("spawn");
855
+ const onStderrLine = onProgress ? (line) => {
856
+ const trimmed = line.replace(/\r$/, "").trimEnd();
857
+ if (trimmed.length === 0)
858
+ return;
859
+ onProgress({ kind: "note", text: trimmed });
860
+ } : undefined;
861
+ const runWithVerboseFallback = async (tailArgs, runner) => {
862
+ const useVerbose = this.acpxVerboseSupported !== false;
863
+ const spec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, tailArgs, { verbose: useVerbose }));
864
+ const result = await runner(spec.command, spec.args);
865
+ if (result.code === 0) {
866
+ if (useVerbose)
867
+ this.acpxVerboseSupported = true;
868
+ return result;
869
+ }
870
+ if (useVerbose && isUnknownVerboseOption(result.stderr, result.stdout)) {
871
+ this.acpxVerboseSupported = false;
872
+ const retrySpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, tailArgs, { verbose: false }));
873
+ return await runner(retrySpec.command, retrySpec.args);
874
+ }
875
+ return result;
876
+ };
877
+ const ensured = await runWithVerboseFallback(["sessions", "ensure", "--name", input.name], (command, args) => this.run(command, args, { onStderrLine }));
505
878
  if (ensured.code === 0) {
879
+ onProgress?.("ready");
506
880
  return {};
507
881
  }
508
882
  const existingSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, ["sessions", "show", input.name]));
509
883
  const existing = await this.run(existingSpec.command, existingSpec.args);
510
884
  if (existing.code === 0) {
885
+ onProgress?.("ready");
511
886
  return {};
512
887
  }
513
- const createSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, ["sessions", "new", "--name", input.name]));
514
- const created = await this.runSessionCreate(createSpec.command, createSpec.args, input.cwd);
888
+ onProgress?.("initializing");
889
+ const created = await runWithVerboseFallback(["sessions", "new", "--name", input.name], (command, args) => this.runSessionCreate(command, args, input.cwd, { onStderrLine }));
515
890
  if (created.code === 0) {
891
+ onProgress?.("ready");
516
892
  return {};
517
893
  }
518
894
  const output = created.stderr || created.stdout || "";
519
- if (output.includes("EPERM") && await tryRepairAcpxSessionIndex()) {
895
+ if (output.includes("EPERM") && await this.repairSessionIndex()) {
520
896
  const repaired = await this.run(existingSpec.command, existingSpec.args);
521
897
  if (repaired.code === 0) {
898
+ onProgress?.("ready");
522
899
  return {};
523
900
  }
524
901
  }
525
- throw new Error(output || ensured.stderr || ensured.stdout || "failed to create session");
902
+ const rawMessage = output || ensured.stderr || ensured.stdout || "failed to create session";
903
+ const parseInput = rawMessage.split(/\r\n|\r|\n/).filter((line) => !/^\s*\[acpx\]/.test(line)).join(`
904
+ `);
905
+ const parsed = parseMissingOptionalDep(parseInput);
906
+ if (parsed) {
907
+ const parentPackagePath = this.resolveParentPackagePath(input, parsed.package);
908
+ throw new EnsureSessionFailedError(rawMessage, "missing_optional_dep", {
909
+ package: parsed.package,
910
+ parentPackagePath
911
+ });
912
+ }
913
+ throw new EnsureSessionFailedError(rawMessage, "generic");
914
+ }
915
+ resolveParentPackagePath(input, platformPackage) {
916
+ const guess = deriveParentPackageName(platformPackage);
917
+ const candidates = [input.agentCommand, input.agent, guess].filter((c) => Boolean(c));
918
+ for (const candidate of candidates) {
919
+ try {
920
+ const resolved = __require.resolve(`${candidate}/package.json`, {
921
+ paths: [process.cwd(), ...__require.resolve.paths(candidate) ?? []]
922
+ });
923
+ return dirname2(resolved);
924
+ } catch {
925
+ continue;
926
+ }
927
+ }
928
+ return null;
526
929
  }
527
930
  async prompt(input, onEvent) {
931
+ await this.launchMcpQueueOwnerIfNeeded(input);
528
932
  const spawnSpec = resolveSpawnCommand(this.command, this.buildPromptArgs(input, [
529
933
  "prompt",
530
934
  "-s",
531
935
  input.name,
532
936
  input.text
533
937
  ]));
534
- const result = onEvent ? await this.runPromptCommand(spawnSpec.command, spawnSpec.args, onEvent) : await this.run(spawnSpec.command, spawnSpec.args);
938
+ const formatToolCalls = input.replyMode === "verbose";
939
+ const result = onEvent ? await this.runPromptCommand(spawnSpec.command, spawnSpec.args, onEvent, { formatToolCalls }) : await this.run(spawnSpec.command, spawnSpec.args);
535
940
  return { text: getPromptText(result) };
536
941
  }
942
+ async launchMcpQueueOwnerIfNeeded(input) {
943
+ if (!input.mcpCoordinatorSession) {
944
+ return;
945
+ }
946
+ const record = await this.readSessionRecord(input);
947
+ await this.queueOwnerLauncher.launch({
948
+ acpxRecordId: record.acpxRecordId,
949
+ coordinatorSession: input.mcpCoordinatorSession,
950
+ ...input.mcpSourceHandle ? { sourceHandle: input.mcpSourceHandle } : {},
951
+ permissionMode: this.options.permissionMode ?? "approve-all",
952
+ nonInteractivePermissions: this.options.nonInteractivePermissions ?? "deny"
953
+ });
954
+ }
955
+ async readSessionRecord(input) {
956
+ const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
957
+ "sessions",
958
+ "show",
959
+ input.name
960
+ ]));
961
+ const result = await this.run(spawnSpec.command, spawnSpec.args);
962
+ if (result.code !== 0) {
963
+ throw new Error(result.stderr || result.stdout || "sessions show failed");
964
+ }
965
+ try {
966
+ const parsed = JSON.parse(result.stdout);
967
+ let acpxRecordId;
968
+ if (typeof parsed.acpxRecordId === "string") {
969
+ acpxRecordId = parsed.acpxRecordId;
970
+ } else if (typeof parsed.id === "string") {
971
+ acpxRecordId = parsed.id;
972
+ }
973
+ if (acpxRecordId) {
974
+ return { acpxRecordId };
975
+ }
976
+ } catch {
977
+ const firstLine = result.stdout.trim().split(/\r?\n/, 1)[0];
978
+ if (firstLine && /^[\w.:-]+$/.test(firstLine) && firstLine.length >= 8) {
979
+ return { acpxRecordId: firstLine };
980
+ }
981
+ }
982
+ throw new Error("failed to resolve acpx session record id");
983
+ }
537
984
  async setMode(input) {
538
985
  const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
539
986
  "set-mode",
@@ -562,10 +1009,25 @@ class BridgeRuntime {
562
1009
  message: result.stdout.trim()
563
1010
  };
564
1011
  }
1012
+ async removeSession(input) {
1013
+ const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
1014
+ "sessions",
1015
+ "close",
1016
+ input.name
1017
+ ]));
1018
+ const result = await this.run(spawnSpec.command, spawnSpec.args);
1019
+ if (result.code === 0) {
1020
+ return {};
1021
+ }
1022
+ if (isMissingBridgeSessionError(result.stderr, result.stdout)) {
1023
+ return {};
1024
+ }
1025
+ throw new Error(result.stderr || result.stdout || "sessions close failed");
1026
+ }
565
1027
  async shutdown() {
566
1028
  return {};
567
1029
  }
568
- buildSessionArgs(input, tail) {
1030
+ buildSessionArgs(input, tail, options = {}) {
569
1031
  const prefix = [
570
1032
  "--format",
571
1033
  "quiet",
@@ -573,6 +1035,9 @@ class BridgeRuntime {
573
1035
  input.cwd,
574
1036
  ...this.buildPermissionArgs()
575
1037
  ];
1038
+ if (options.verbose) {
1039
+ prefix.push("--verbose");
1040
+ }
576
1041
  if (input.agentCommand) {
577
1042
  return [...prefix, "--agent", input.agentCommand, ...tail];
578
1043
  }
@@ -595,29 +1060,47 @@ class BridgeRuntime {
595
1060
  buildPermissionArgs() {
596
1061
  const permissionMode = this.options.permissionMode ?? "approve-all";
597
1062
  const nonInteractivePermissions = this.options.nonInteractivePermissions ?? "deny";
598
- const modeFlag = permissionMode === "approve-reads" ? "--approve-reads" : permissionMode === "deny-all" ? "--deny-all" : "--approve-all";
1063
+ const modeFlag = permissionModeToFlag(permissionMode);
599
1064
  return [modeFlag, "--non-interactive-permissions", nonInteractivePermissions];
600
1065
  }
601
1066
  }
602
- async function defaultRunner(command, args) {
603
- return await new Promise((resolve, reject) => {
604
- const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
1067
+ function spawnCapture(command, args, options) {
1068
+ return new Promise((resolve, reject) => {
1069
+ const child = spawn4(command, args, { cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"] });
1070
+ child.stdout.setEncoding("utf8");
1071
+ child.stderr.setEncoding("utf8");
605
1072
  let stdout = "";
606
1073
  let stderr = "";
1074
+ let stderrTail = "";
607
1075
  child.stdout.on("data", (chunk) => {
608
1076
  stdout += String(chunk);
609
1077
  });
610
1078
  child.stderr.on("data", (chunk) => {
611
- stderr += String(chunk);
1079
+ const text = String(chunk);
1080
+ stderr += text;
1081
+ if (!options?.onStderrLine)
1082
+ return;
1083
+ stderrTail += text;
1084
+ const matches = stderrTail.split(/\r\n|\r|\n/);
1085
+ stderrTail = matches.pop() ?? "";
1086
+ for (const line of matches) {
1087
+ options.onStderrLine(line);
1088
+ }
612
1089
  });
613
1090
  child.on("error", reject);
614
1091
  child.on("close", (code) => {
1092
+ if (options?.onStderrLine && stderrTail.length > 0) {
1093
+ options.onStderrLine(stderrTail);
1094
+ }
615
1095
  resolve({ code: code ?? 1, stdout, stderr });
616
1096
  });
617
1097
  });
618
1098
  }
1099
+ async function defaultRunner(command, args, options) {
1100
+ return await spawnCapture(command, args, options);
1101
+ }
619
1102
  async function runStreamingPrompt(command, args, onEvent, options = {}) {
620
- const spawnPrompt = options.spawnPrompt ?? ((spawnCommand, spawnArgs) => spawn(spawnCommand, spawnArgs, { stdio: ["ignore", "pipe", "pipe"] }));
1103
+ const spawnPrompt = options.spawnPrompt ?? ((spawnCommand, spawnArgs) => spawn4(spawnCommand, spawnArgs, { stdio: ["ignore", "pipe", "pipe"] }));
621
1104
  const setIntervalFn = options.setIntervalFn ?? ((fn, delay) => setInterval(fn, delay));
622
1105
  const clearIntervalFn = options.clearIntervalFn ?? ((timer) => clearInterval(timer));
623
1106
  const maxSegmentWaitMs = options.maxSegmentWaitMs ?? 30000;
@@ -627,7 +1110,7 @@ async function runStreamingPrompt(command, args, onEvent, options = {}) {
627
1110
  const child = spawnPrompt(command, args);
628
1111
  let stdout = "";
629
1112
  let stderr = "";
630
- const state = createStreamingPromptState();
1113
+ const state = createStreamingPromptState(options.formatToolCalls ?? false);
631
1114
  let lastReplyAt = now();
632
1115
  const flushBuffer = () => {
633
1116
  const remaining = state.buffer.trim();
@@ -669,81 +1152,375 @@ async function runStreamingPrompt(command, args, onEvent, options = {}) {
669
1152
  });
670
1153
  });
671
1154
  }
672
- async function defaultPromptRunner(command, args, onEvent) {
673
- return await runStreamingPrompt(command, args, onEvent);
1155
+ async function defaultPromptRunner(command, args, onEvent, options) {
1156
+ return await runStreamingPrompt(command, args, onEvent, options);
674
1157
  }
675
- async function shellSessionCreateRunner(command, args, cwd) {
676
- return await new Promise((resolve, reject) => {
677
- const child = spawn(command, args, {
678
- cwd,
679
- stdio: ["ignore", "pipe", "pipe"]
680
- });
681
- let stdout = "";
682
- let stderr = "";
683
- child.stdout.on("data", (chunk) => {
684
- stdout += String(chunk);
685
- });
686
- child.stderr.on("data", (chunk) => {
687
- stderr += String(chunk);
688
- });
689
- child.on("error", reject);
690
- child.on("close", (code) => {
691
- resolve({ code: code ?? 1, stdout, stderr });
692
- });
693
- });
1158
+ async function shellSessionCreateRunner(command, args, cwd, options) {
1159
+ return await spawnCapture(command, args, { cwd, onStderrLine: options?.onStderrLine });
1160
+ }
1161
+ function selectLatestAcpxSessionIndexTmp(files) {
1162
+ let latestTmp = null;
1163
+ let latestTime = 0;
1164
+ for (const file of files) {
1165
+ const match = file.match(/^index\.json\.\d+\.(\d+)\.tmp$/);
1166
+ if (!match) {
1167
+ continue;
1168
+ }
1169
+ const timestamp = Number(match[1]);
1170
+ if (timestamp > latestTime) {
1171
+ latestTime = timestamp;
1172
+ latestTmp = file;
1173
+ }
1174
+ }
1175
+ return latestTmp;
694
1176
  }
695
- async function tryRepairAcpxSessionIndex() {
696
- if (process.platform !== "win32") {
1177
+ async function tryRepairAcpxSessionIndex(deps = {}) {
1178
+ const platform = deps.platform ?? process.platform;
1179
+ if (platform !== "win32") {
697
1180
  return false;
698
1181
  }
699
- const home = process.env.HOME ?? process.env.USERPROFILE ?? homedir();
1182
+ const home = deps.home ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir3();
700
1183
  if (!home) {
701
1184
  return false;
702
1185
  }
703
- const sessionsDir = join(home, ".acpx", "sessions");
704
- const indexPath = join(sessionsDir, "index.json");
1186
+ const pathJoin = platform === "win32" ? win32.join : join3;
1187
+ const sessionsDir = pathJoin(home, ".acpx", "sessions");
1188
+ const indexPath = pathJoin(sessionsDir, "index.json");
1189
+ const readdirFn = deps.readdirFn ?? readdir;
1190
+ const copyFileFn = deps.copyFileFn ?? copyFile;
705
1191
  let files;
706
1192
  try {
707
- files = await readdir(sessionsDir);
1193
+ files = await readdirFn(sessionsDir);
708
1194
  } catch {
709
1195
  return false;
710
1196
  }
711
- const tmpFiles = files.filter((f) => f.startsWith("index.json.") && f.endsWith(".tmp"));
712
- if (tmpFiles.length === 0) {
713
- return false;
714
- }
715
- let latestTmp = "";
716
- let latestTime = 0;
717
- for (const f of tmpFiles) {
718
- const match = f.match(/^index\.json\.\d+\.(\d+)\.tmp$/);
719
- if (match && Number(match[1]) > latestTime) {
720
- latestTime = Number(match[1]);
721
- latestTmp = f;
722
- }
723
- }
1197
+ const latestTmp = selectLatestAcpxSessionIndexTmp(files);
724
1198
  if (!latestTmp) {
725
1199
  return false;
726
1200
  }
727
1201
  try {
728
- await copyFile(join(sessionsDir, latestTmp), indexPath);
1202
+ await copyFileFn(pathJoin(sessionsDir, latestTmp), indexPath);
729
1203
  return true;
730
1204
  } catch {
731
1205
  return false;
732
1206
  }
733
1207
  }
1208
+ function isUnknownVerboseOption(stderr, stdout) {
1209
+ const combined = `${stderr}
1210
+ ${stdout}`;
1211
+ return /(unknown|unrecognized)\b[^\n]*--verbose/i.test(combined);
1212
+ }
1213
+ function isMissingBridgeSessionError(stderr, stdout) {
1214
+ const combined = `${stderr}
1215
+ ${stdout}`.toLowerCase();
1216
+ return combined.includes("no named session") || combined.includes("no cwd session") || combined.includes("session not found") || combined.includes("unknown session") || combined.includes("no acpx session found");
1217
+ }
1218
+
1219
+ // src/bridge/bridge-server.ts
1220
+ class BridgeInvalidRequestError extends Error {
1221
+ }
1222
+ var BRIDGE_METHODS = new Set([
1223
+ "ping",
1224
+ "shutdown",
1225
+ "updatePermissionPolicy",
1226
+ "hasSession",
1227
+ "ensureSession",
1228
+ "prompt",
1229
+ "setMode",
1230
+ "cancel",
1231
+ "removeSession"
1232
+ ]);
1233
+ var SESSION_SCOPED_METHODS = new Set([
1234
+ "hasSession",
1235
+ "ensureSession",
1236
+ "prompt",
1237
+ "setMode",
1238
+ "cancel",
1239
+ "removeSession"
1240
+ ]);
1241
+
1242
+ class BridgeServer {
1243
+ runtime;
1244
+ scheduler = new BridgeRequestScheduler;
1245
+ constructor(runtime) {
1246
+ this.runtime = runtime;
1247
+ }
1248
+ async handleLine(line, writeLine) {
1249
+ let requestId = extractRequestId(line);
1250
+ try {
1251
+ const request = parseBridgeRequest(line);
1252
+ requestId = request.id;
1253
+ const result = await this.dispatchRequest(request.id, request.method, request.params, writeLine);
1254
+ return `${JSON.stringify({
1255
+ id: request.id,
1256
+ ok: true,
1257
+ result
1258
+ })}
1259
+ `;
1260
+ } catch (error) {
1261
+ const message = error instanceof Error ? error.message : String(error);
1262
+ const ensureSessionFields = error instanceof EnsureSessionFailedError ? { kind: error.kind, ...error.data ? { data: error.data } : {} } : {};
1263
+ const promptDetails = error instanceof PromptCommandError ? { details: { exitCode: error.exitCode, stdout: error.stdout, stderr: error.stderr } } : {};
1264
+ return `${JSON.stringify({
1265
+ id: requestId,
1266
+ ok: false,
1267
+ error: {
1268
+ code: error instanceof BridgeInvalidRequestError ? "BRIDGE_INVALID_REQUEST" : "BRIDGE_INTERNAL_ERROR",
1269
+ message,
1270
+ ...ensureSessionFields,
1271
+ ...promptDetails
1272
+ }
1273
+ })}
1274
+ `;
1275
+ }
1276
+ }
1277
+ async dispatchRequest(requestId, method, params, writeLine) {
1278
+ if (!SESSION_SCOPED_METHODS.has(method)) {
1279
+ return await this.dispatch(requestId, method, params, writeLine);
1280
+ }
1281
+ const sessionName = getSessionName(params);
1282
+ if (!sessionName) {
1283
+ return await this.dispatch(requestId, method, params, writeLine);
1284
+ }
1285
+ const sessionKey = getSessionScheduleKey(params);
1286
+ if (!sessionKey) {
1287
+ return await this.dispatch(requestId, method, params, writeLine);
1288
+ }
1289
+ const lane = method === "cancel" ? "control" : "normal";
1290
+ return await this.scheduler.run(sessionKey, lane, () => this.dispatch(requestId, method, params, writeLine));
1291
+ }
1292
+ async dispatch(requestId, method, params, writeLine) {
1293
+ switch (method) {
1294
+ case "ping":
1295
+ return {};
1296
+ case "shutdown":
1297
+ return await this.runtime.shutdown();
1298
+ case "updatePermissionPolicy":
1299
+ return await this.runtime.updatePermissionPolicy({
1300
+ permissionMode: requirePermissionMode(params, "permissionMode"),
1301
+ nonInteractivePermissions: requireNonInteractivePermissions(params, "nonInteractivePermissions")
1302
+ });
1303
+ case "hasSession":
1304
+ return await this.runtime.hasSession({
1305
+ agent: requireString(params, "agent"),
1306
+ agentCommand: asOptionalString(params.agentCommand),
1307
+ cwd: requireString(params, "cwd"),
1308
+ name: requireString(params, "name")
1309
+ });
1310
+ case "ensureSession":
1311
+ return await this.runtime.ensureSession({
1312
+ agent: requireString(params, "agent"),
1313
+ agentCommand: asOptionalString(params.agentCommand),
1314
+ cwd: requireString(params, "cwd"),
1315
+ name: requireString(params, "name"),
1316
+ mcpCoordinatorSession: asOptionalString(params.mcpCoordinatorSession),
1317
+ mcpSourceHandle: asOptionalString(params.mcpSourceHandle)
1318
+ }, (progress) => {
1319
+ if (typeof progress === "string") {
1320
+ writeLine?.(encodeBridgeSessionProgressEvent({
1321
+ id: requestId,
1322
+ event: "session.progress",
1323
+ stage: progress
1324
+ }));
1325
+ } else if (progress.kind === "note") {
1326
+ writeLine?.(encodeBridgeSessionNoteEvent({
1327
+ id: requestId,
1328
+ event: "session.note",
1329
+ text: progress.text
1330
+ }));
1331
+ }
1332
+ });
1333
+ case "prompt":
1334
+ return await this.runtime.prompt({
1335
+ agent: requireString(params, "agent"),
1336
+ agentCommand: asOptionalString(params.agentCommand),
1337
+ cwd: requireString(params, "cwd"),
1338
+ name: requireString(params, "name"),
1339
+ mcpCoordinatorSession: asOptionalString(params.mcpCoordinatorSession),
1340
+ mcpSourceHandle: asOptionalString(params.mcpSourceHandle),
1341
+ text: requireString(params, "text"),
1342
+ replyMode: asOptionalReplyMode(params.replyMode)
1343
+ }, (event) => {
1344
+ if (event.type === "prompt.segment") {
1345
+ writeLine?.(encodeBridgePromptSegmentEvent({
1346
+ id: requestId,
1347
+ event: "prompt.segment",
1348
+ text: event.text
1349
+ }));
1350
+ }
1351
+ });
1352
+ case "setMode":
1353
+ return await this.runtime.setMode({
1354
+ agent: requireString(params, "agent"),
1355
+ agentCommand: asOptionalString(params.agentCommand),
1356
+ cwd: requireString(params, "cwd"),
1357
+ name: requireString(params, "name"),
1358
+ modeId: requireString(params, "modeId")
1359
+ });
1360
+ case "cancel":
1361
+ return await this.runtime.cancel({
1362
+ agent: requireString(params, "agent"),
1363
+ agentCommand: asOptionalString(params.agentCommand),
1364
+ cwd: requireString(params, "cwd"),
1365
+ name: requireString(params, "name")
1366
+ });
1367
+ case "removeSession":
1368
+ return await this.runtime.removeSession({
1369
+ agent: requireString(params, "agent"),
1370
+ agentCommand: asOptionalString(params.agentCommand),
1371
+ cwd: requireString(params, "cwd"),
1372
+ name: requireString(params, "name")
1373
+ });
1374
+ default:
1375
+ throw new Error(`unsupported bridge method: ${method}`);
1376
+ }
1377
+ }
1378
+ }
1379
+ function extractRequestId(line) {
1380
+ try {
1381
+ const raw = JSON.parse(line);
1382
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1383
+ return "unknown";
1384
+ }
1385
+ const id = raw.id;
1386
+ return typeof id === "string" && id.length > 0 ? id : "unknown";
1387
+ } catch {
1388
+ return "unknown";
1389
+ }
1390
+ }
1391
+ function parseBridgeRequest(line) {
1392
+ let raw;
1393
+ try {
1394
+ raw = JSON.parse(line);
1395
+ } catch {
1396
+ throw new BridgeInvalidRequestError("request must be valid JSON");
1397
+ }
1398
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1399
+ throw new BridgeInvalidRequestError("request must be a JSON object");
1400
+ }
1401
+ const request = raw;
1402
+ const id = request.id;
1403
+ const method = request.method;
1404
+ const params = request.params;
1405
+ if (typeof id !== "string" || id.length === 0) {
1406
+ throw new BridgeInvalidRequestError("id must be a non-empty string");
1407
+ }
1408
+ if (typeof method !== "string" || method.length === 0) {
1409
+ throw new BridgeInvalidRequestError("method must be a non-empty string");
1410
+ }
1411
+ if (!BRIDGE_METHODS.has(method)) {
1412
+ throw new BridgeInvalidRequestError(`unsupported bridge method: ${method}`);
1413
+ }
1414
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
1415
+ throw new BridgeInvalidRequestError("params must be an object");
1416
+ }
1417
+ return {
1418
+ id,
1419
+ method,
1420
+ params
1421
+ };
1422
+ }
1423
+ function getSessionName(params) {
1424
+ return asNonEmptyString(params.name);
1425
+ }
1426
+ function getSessionScheduleKey(params) {
1427
+ const name = asNonEmptyString(params.name);
1428
+ const cwd = asNonEmptyString(params.cwd);
1429
+ const agentIdentity = asNonEmptyString(params.agentCommand) ?? asNonEmptyString(params.agent);
1430
+ if (!name || !cwd || !agentIdentity) {
1431
+ return;
1432
+ }
1433
+ return JSON.stringify([agentIdentity, cwd, name]);
1434
+ }
1435
+ function asNonEmptyString(value) {
1436
+ if (typeof value !== "string" || value.length === 0) {
1437
+ return;
1438
+ }
1439
+ return value;
1440
+ }
1441
+ function requireString(params, key) {
1442
+ const value = params[key];
1443
+ if (typeof value !== "string" || value.length === 0) {
1444
+ throw new BridgeInvalidRequestError(`${key} must be a non-empty string`);
1445
+ }
1446
+ return value;
1447
+ }
1448
+ function requirePermissionMode(params, key) {
1449
+ const value = params[key];
1450
+ if (value === "approve-all" || value === "approve-reads" || value === "deny-all") {
1451
+ return value;
1452
+ }
1453
+ throw new BridgeInvalidRequestError(`${key} must be approve-all, approve-reads, or deny-all`);
1454
+ }
1455
+ function requireNonInteractivePermissions(params, key) {
1456
+ const value = params[key];
1457
+ if (value === "deny" || value === "fail") {
1458
+ return value;
1459
+ }
1460
+ throw new BridgeInvalidRequestError(`${key} must be deny or fail`);
1461
+ }
1462
+ function asOptionalString(value) {
1463
+ if (typeof value !== "string" || value.length === 0) {
1464
+ return;
1465
+ }
1466
+ return value;
1467
+ }
1468
+ var VALID_REPLY_MODES = new Set(["stream", "final", "verbose"]);
1469
+ function asOptionalReplyMode(value) {
1470
+ if (typeof value !== "string" || !VALID_REPLY_MODES.has(value)) {
1471
+ return;
1472
+ }
1473
+ return value;
1474
+ }
734
1475
 
735
1476
  // src/bridge/bridge-main.ts
736
- var server = new BridgeServer(new BridgeRuntime(process.env.WEACPX_BRIDGE_ACPX_COMMAND ?? "acpx", undefined, undefined, {
737
- permissionMode: normalizeBridgePermissionMode(process.env.WEACPX_BRIDGE_PERMISSION_MODE),
738
- nonInteractivePermissions: normalizeBridgeNonInteractivePermissions(process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS)
739
- }));
740
- var input = createInterface({
741
- input: process.stdin,
742
- crlfDelay: Infinity
743
- });
744
- for await (const line of input) {
745
- const response = await server.handleLine(line, (chunk) => {
746
- process.stdout.write(chunk);
1477
+ async function processBridgeInput(options) {
1478
+ const pendingWrites = new Set;
1479
+ let firstError;
1480
+ for await (const line of options.input) {
1481
+ const pendingWrite = (async () => {
1482
+ const response = await options.server.handleLine(line, (chunk) => {
1483
+ options.write(chunk);
1484
+ });
1485
+ options.write(response);
1486
+ })();
1487
+ const observedPendingWrite = pendingWrite.catch((error) => {
1488
+ if (firstError === undefined) {
1489
+ firstError = error;
1490
+ options.input.close();
1491
+ }
1492
+ });
1493
+ pendingWrites.add(pendingWrite);
1494
+ observedPendingWrite.finally(() => {
1495
+ pendingWrites.delete(pendingWrite);
1496
+ });
1497
+ }
1498
+ await Promise.allSettled(pendingWrites);
1499
+ if (firstError !== undefined) {
1500
+ throw firstError;
1501
+ }
1502
+ }
1503
+ async function runBridgeMain() {
1504
+ const server = new BridgeServer(new BridgeRuntime(process.env.WEACPX_BRIDGE_ACPX_COMMAND ?? "acpx", undefined, undefined, {
1505
+ permissionMode: normalizeBridgePermissionMode(process.env.WEACPX_BRIDGE_PERMISSION_MODE),
1506
+ nonInteractivePermissions: normalizeBridgeNonInteractivePermissions(process.env.WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS)
1507
+ }));
1508
+ const input = createInterface({
1509
+ input: process.stdin,
1510
+ crlfDelay: Infinity
1511
+ });
1512
+ await processBridgeInput({
1513
+ input,
1514
+ server,
1515
+ write: (chunk) => {
1516
+ process.stdout.write(chunk);
1517
+ }
747
1518
  });
748
- process.stdout.write(response);
749
1519
  }
1520
+ if (__require.main == __require.module) {
1521
+ await runBridgeMain();
1522
+ }
1523
+ export {
1524
+ runBridgeMain,
1525
+ processBridgeInput
1526
+ };