weacpx 0.2.2 → 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,6 +289,450 @@ 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 { 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) });
389
+ }
390
+ for (const name of [parentName, platformPackage]) {
391
+ const resolved = resolveFromCwd(name, cwd);
392
+ if (resolved)
393
+ rawCandidates.push({ path: resolved, manager: classify(resolved) });
394
+ }
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);
410
+ }
411
+ }
412
+ return verified;
413
+ }
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) {
420
+ try {
421
+ await access(path);
422
+ return true;
423
+ } catch {
424
+ return false;
425
+ }
426
+ }
427
+ function defaultResolveFromCwd(name, cwd) {
428
+ try {
429
+ const pkgJson = __require.resolve(`${name}/package.json`, {
430
+ paths: [cwd, ...__require.resolve.paths(name) ?? []]
431
+ });
432
+ return dirname(pkgJson);
433
+ } catch {
434
+ return null;
435
+ }
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;
492
+ }
493
+ if (platform === "win32") {
494
+ try {
495
+ await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
496
+ } catch {}
497
+ return;
498
+ }
499
+ const targetPid = pid > 0 ? -pid : pid;
500
+ try {
501
+ killProcess(targetPid, "SIGTERM");
502
+ } catch {
503
+ return;
504
+ }
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));
511
+ }
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;
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);
541
+ return {
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
+ ]
552
+ };
553
+ }
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
+ };
563
+ }
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
+ });
611
+ }
612
+ }
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;
648
+ }
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) };
662
+ }
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 {
692
+ return;
693
+ }
694
+ if (typeof owner.pid === "number" && Number.isInteger(owner.pid) && owner.pid > 0) {
695
+ await terminateProcessTree(owner.pid);
696
+ }
697
+ await unlink(lockPath).catch(() => {});
698
+ }
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
+ }
264
736
 
265
737
  // src/bridge/bridge-main.ts
266
738
  import { createInterface } from "node:readline";
@@ -308,299 +780,206 @@ class BridgeRequestScheduler {
308
780
  }
309
781
  }
310
782
 
311
- // src/bridge/bridge-server.ts
312
- class BridgeInvalidRequestError extends Error {
783
+ // src/bridge/bridge-runtime.ts
784
+ init_spawn_command();
785
+ init_prompt_output();
786
+ init_streaming_prompt();
787
+ import { copyFile, readdir } from "node:fs/promises";
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 };
313
803
  }
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
804
 
326
- class BridgeServer {
327
- runtime;
328
- scheduler = new BridgeRequestScheduler;
329
- constructor(runtime) {
330
- this.runtime = runtime;
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;
331
816
  }
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
- }
817
+ }
818
+
819
+ class BridgeRuntime {
820
+ command;
821
+ run;
822
+ runSessionCreate;
823
+ options;
824
+ runPromptCommand;
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
+ })) {
831
+ this.command = command;
832
+ this.run = run;
833
+ this.runSessionCreate = runSessionCreate;
834
+ this.options = options;
835
+ this.runPromptCommand = runPromptCommand;
836
+ this.repairSessionIndex = repairSessionIndex;
837
+ this.queueOwnerLauncher = queueOwnerLauncher;
363
838
  }
364
- async dispatchRequest(requestId, method, params, writeLine) {
365
- if (!SESSION_SCOPED_METHODS.has(method)) {
366
- return await this.dispatch(requestId, method, params, writeLine);
839
+ async updatePermissionPolicy(policy) {
840
+ this.options.permissionMode = policy.permissionMode;
841
+ this.options.nonInteractivePermissions = policy.nonInteractivePermissions;
842
+ return {};
843
+ }
844
+ async hasSession(input) {
845
+ const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
846
+ "sessions",
847
+ "show",
848
+ input.name
849
+ ]));
850
+ const result = await this.run(spawnSpec.command, spawnSpec.args);
851
+ return { exists: result.code === 0 };
852
+ }
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 }));
878
+ if (ensured.code === 0) {
879
+ onProgress?.("ready");
880
+ return {};
367
881
  }
368
- const sessionName = getSessionName(params);
369
- if (!sessionName) {
370
- return await this.dispatch(requestId, method, params, writeLine);
882
+ const existingSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, ["sessions", "show", input.name]));
883
+ const existing = await this.run(existingSpec.command, existingSpec.args);
884
+ if (existing.code === 0) {
885
+ onProgress?.("ready");
886
+ return {};
371
887
  }
372
- const sessionKey = getSessionScheduleKey(params);
373
- if (!sessionKey) {
374
- return await this.dispatch(requestId, method, params, writeLine);
888
+ onProgress?.("initializing");
889
+ const created = await runWithVerboseFallback(["sessions", "new", "--name", input.name], (command, args) => this.runSessionCreate(command, args, input.cwd, { onStderrLine }));
890
+ if (created.code === 0) {
891
+ onProgress?.("ready");
892
+ return {};
375
893
  }
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":
894
+ const output = created.stderr || created.stdout || "";
895
+ if (output.includes("EPERM") && await this.repairSessionIndex()) {
896
+ const repaired = await this.run(existingSpec.command, existingSpec.args);
897
+ if (repaired.code === 0) {
898
+ onProgress?.("ready");
382
899
  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}`);
900
+ }
437
901
  }
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";
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
+ });
445
912
  }
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;
913
+ throw new EnsureSessionFailedError(rawMessage, "generic");
513
914
  }
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;
520
- }
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;
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
- 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;
930
+ async prompt(input, onEvent) {
931
+ await this.launchMcpQueueOwnerIfNeeded(input);
932
+ const spawnSpec = resolveSpawnCommand(this.command, this.buildPromptArgs(input, [
933
+ "prompt",
934
+ "-s",
935
+ input.name,
936
+ input.text
937
+ ]));
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);
940
+ return { text: getPromptText(result) };
550
941
  }
551
- async updatePermissionPolicy(policy) {
552
- this.options.permissionMode = policy.permissionMode;
553
- this.options.nonInteractivePermissions = policy.nonInteractivePermissions;
554
- return {};
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
+ });
555
954
  }
556
- async hasSession(input) {
955
+ async readSessionRecord(input) {
557
956
  const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
558
957
  "sessions",
559
958
  "show",
560
959
  input.name
561
960
  ]));
562
961
  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 {};
962
+ if (result.code !== 0) {
963
+ throw new Error(result.stderr || result.stdout || "sessions show failed");
585
964
  }
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 {};
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 };
591
980
  }
592
981
  }
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) };
982
+ throw new Error("failed to resolve acpx session record id");
604
983
  }
605
984
  async setMode(input) {
606
985
  const spawnSpec = resolveSpawnCommand(this.command, this.buildSessionArgs(input, [
@@ -630,10 +1009,25 @@ class BridgeRuntime {
630
1009
  message: result.stdout.trim()
631
1010
  };
632
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
+ }
633
1027
  async shutdown() {
634
1028
  return {};
635
1029
  }
636
- buildSessionArgs(input, tail) {
1030
+ buildSessionArgs(input, tail, options = {}) {
637
1031
  const prefix = [
638
1032
  "--format",
639
1033
  "quiet",
@@ -641,6 +1035,9 @@ class BridgeRuntime {
641
1035
  input.cwd,
642
1036
  ...this.buildPermissionArgs()
643
1037
  ];
1038
+ if (options.verbose) {
1039
+ prefix.push("--verbose");
1040
+ }
644
1041
  if (input.agentCommand) {
645
1042
  return [...prefix, "--agent", input.agentCommand, ...tail];
646
1043
  }
@@ -663,29 +1060,47 @@ class BridgeRuntime {
663
1060
  buildPermissionArgs() {
664
1061
  const permissionMode = this.options.permissionMode ?? "approve-all";
665
1062
  const nonInteractivePermissions = this.options.nonInteractivePermissions ?? "deny";
666
- const modeFlag = permissionMode === "approve-reads" ? "--approve-reads" : permissionMode === "deny-all" ? "--deny-all" : "--approve-all";
1063
+ const modeFlag = permissionModeToFlag(permissionMode);
667
1064
  return [modeFlag, "--non-interactive-permissions", nonInteractivePermissions];
668
1065
  }
669
1066
  }
670
- async function defaultRunner(command, args) {
671
- return await new Promise((resolve, reject) => {
672
- 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");
673
1072
  let stdout = "";
674
1073
  let stderr = "";
1074
+ let stderrTail = "";
675
1075
  child.stdout.on("data", (chunk) => {
676
1076
  stdout += String(chunk);
677
1077
  });
678
1078
  child.stderr.on("data", (chunk) => {
679
- 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
+ }
680
1089
  });
681
1090
  child.on("error", reject);
682
1091
  child.on("close", (code) => {
1092
+ if (options?.onStderrLine && stderrTail.length > 0) {
1093
+ options.onStderrLine(stderrTail);
1094
+ }
683
1095
  resolve({ code: code ?? 1, stdout, stderr });
684
1096
  });
685
1097
  });
686
1098
  }
1099
+ async function defaultRunner(command, args, options) {
1100
+ return await spawnCapture(command, args, options);
1101
+ }
687
1102
  async function runStreamingPrompt(command, args, onEvent, options = {}) {
688
- 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"] }));
689
1104
  const setIntervalFn = options.setIntervalFn ?? ((fn, delay) => setInterval(fn, delay));
690
1105
  const clearIntervalFn = options.clearIntervalFn ?? ((timer) => clearInterval(timer));
691
1106
  const maxSegmentWaitMs = options.maxSegmentWaitMs ?? 30000;
@@ -695,7 +1110,7 @@ async function runStreamingPrompt(command, args, onEvent, options = {}) {
695
1110
  const child = spawnPrompt(command, args);
696
1111
  let stdout = "";
697
1112
  let stderr = "";
698
- const state = createStreamingPromptState();
1113
+ const state = createStreamingPromptState(options.formatToolCalls ?? false);
699
1114
  let lastReplyAt = now();
700
1115
  const flushBuffer = () => {
701
1116
  const remaining = state.buffer.trim();
@@ -737,68 +1152,326 @@ async function runStreamingPrompt(command, args, onEvent, options = {}) {
737
1152
  });
738
1153
  });
739
1154
  }
740
- async function defaultPromptRunner(command, args, onEvent) {
741
- return await runStreamingPrompt(command, args, onEvent);
1155
+ async function defaultPromptRunner(command, args, onEvent, options) {
1156
+ return await runStreamingPrompt(command, args, onEvent, options);
742
1157
  }
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
- });
1158
+ async function shellSessionCreateRunner(command, args, cwd, options) {
1159
+ return await spawnCapture(command, args, { cwd, onStderrLine: options?.onStderrLine });
762
1160
  }
763
- async function tryRepairAcpxSessionIndex() {
764
- if (process.platform !== "win32") {
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;
1176
+ }
1177
+ async function tryRepairAcpxSessionIndex(deps = {}) {
1178
+ const platform = deps.platform ?? process.platform;
1179
+ if (platform !== "win32") {
765
1180
  return false;
766
1181
  }
767
- const home = process.env.HOME ?? process.env.USERPROFILE ?? homedir();
1182
+ const home = deps.home ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir3();
768
1183
  if (!home) {
769
1184
  return false;
770
1185
  }
771
- const sessionsDir = join(home, ".acpx", "sessions");
772
- 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;
773
1191
  let files;
774
1192
  try {
775
- files = await readdir(sessionsDir);
1193
+ files = await readdirFn(sessionsDir);
776
1194
  } catch {
777
1195
  return false;
778
1196
  }
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
- }
1197
+ const latestTmp = selectLatestAcpxSessionIndexTmp(files);
792
1198
  if (!latestTmp) {
793
1199
  return false;
794
1200
  }
795
1201
  try {
796
- await copyFile(join(sessionsDir, latestTmp), indexPath);
1202
+ await copyFileFn(pathJoin(sessionsDir, latestTmp), indexPath);
797
1203
  return true;
798
1204
  } catch {
799
1205
  return false;
800
1206
  }
801
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
+ }
802
1475
 
803
1476
  // src/bridge/bridge-main.ts
804
1477
  async function processBridgeInput(options) {