weacpx 0.2.2 → 0.3.1

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