@raysonmeng/agentbridge 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -120,7 +120,7 @@ function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env)
120
120
  var require_package = __commonJS((exports, module) => {
121
121
  module.exports = {
122
122
  name: "@raysonmeng/agentbridge",
123
- version: "0.1.11",
123
+ version: "0.1.12",
124
124
  description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
125
125
  type: "module",
126
126
  packageManager: "bun@1.3.11",
@@ -323,6 +323,48 @@ import { join as join2 } from "path";
323
323
  function isRecord(value) {
324
324
  return typeof value === "object" && value !== null && !Array.isArray(value);
325
325
  }
326
+ function isCoercibleNumber(value) {
327
+ if (typeof value === "number")
328
+ return Number.isFinite(value);
329
+ if (typeof value === "string")
330
+ return Number.isFinite(Number(value));
331
+ return false;
332
+ }
333
+ function findShapeViolation(raw) {
334
+ if ("idleShutdownSeconds" in raw && !isCoercibleNumber(raw.idleShutdownSeconds)) {
335
+ return "idleShutdownSeconds is present but not a number";
336
+ }
337
+ if ("budget" in raw) {
338
+ const budget = raw.budget;
339
+ if (!isRecord(budget)) {
340
+ return "budget is present but not an object";
341
+ }
342
+ const numericKeys = ["pauseAt", "resumeBelow", "pollSeconds", "syncDriftPct"];
343
+ for (const key of numericKeys) {
344
+ if (key in budget && !isCoercibleNumber(budget[key])) {
345
+ return `budget.${key} is present but not a number`;
346
+ }
347
+ }
348
+ if ("parallel" in budget) {
349
+ const parallel = budget.parallel;
350
+ if (!isRecord(parallel)) {
351
+ return "budget.parallel is present but not an object";
352
+ }
353
+ for (const key of ["minRemainingPct", "timeWindowSec"]) {
354
+ if (key in parallel && !isCoercibleNumber(parallel[key])) {
355
+ return `budget.parallel.${key} is present but not a number`;
356
+ }
357
+ }
358
+ }
359
+ }
360
+ return null;
361
+ }
362
+ function hasCustomDecisionValues(config) {
363
+ const d = DEFAULT_CONFIG;
364
+ const b = config.budget;
365
+ const db = d.budget;
366
+ return config.idleShutdownSeconds !== d.idleShutdownSeconds || config.turnCoordination.attentionWindowSeconds !== d.turnCoordination.attentionWindowSeconds || config.codex.appPort !== d.codex.appPort || config.codex.proxyPort !== d.codex.proxyPort || b.enabled !== db.enabled || b.pollSeconds !== db.pollSeconds || b.pauseAt !== db.pauseAt || b.resumeBelow !== db.resumeBelow || b.syncDriftPct !== db.syncDriftPct || b.parallel.minRemainingPct !== db.parallel.minRemainingPct || b.parallel.timeWindowSec !== db.parallel.timeWindowSec || b.codexTierControl !== db.codexTierControl;
367
+ }
326
368
  function normalizeInteger(value, fallback) {
327
369
  if (typeof value === "number" && Number.isFinite(value))
328
370
  return value;
@@ -358,35 +400,35 @@ function normalizeCodexOverride(raw) {
358
400
  override.effort = raw.effort.trim();
359
401
  return Object.keys(override).length > 0 ? override : null;
360
402
  }
361
- function normalizeCodexTiers(raw) {
403
+ function normalizeCodexTiers(raw, fallback = DEFAULT_BUDGET_CONFIG.codexTiers) {
362
404
  const tiers = isRecord(raw) ? raw : {};
363
405
  return {
364
406
  full: normalizeCodexOverride(tiers.full),
365
- balanced: normalizeCodexOverride(tiers.balanced) ?? DEFAULT_BUDGET_CONFIG.codexTiers.balanced,
366
- eco: normalizeCodexOverride(tiers.eco) ?? DEFAULT_BUDGET_CONFIG.codexTiers.eco
407
+ balanced: normalizeCodexOverride(tiers.balanced) ?? fallback.balanced,
408
+ eco: normalizeCodexOverride(tiers.eco) ?? fallback.eco
367
409
  };
368
410
  }
369
- function normalizeBudgetConfig(raw) {
411
+ function normalizeBudgetConfig(raw, fallback = DEFAULT_BUDGET_CONFIG) {
370
412
  const budget = isRecord(raw) ? raw : {};
371
413
  const parallel = isRecord(budget.parallel) ? budget.parallel : {};
372
- const codexTiers = normalizeCodexTiers(budget.codexTiers);
373
- let pauseAt = normalizeBoundedInteger(budget.pauseAt, DEFAULT_BUDGET_CONFIG.pauseAt, 1, 100);
374
- let resumeBelow = normalizeBoundedInteger(budget.resumeBelow, DEFAULT_BUDGET_CONFIG.resumeBelow, 0, 99);
414
+ const codexTiers = normalizeCodexTiers(budget.codexTiers, fallback.codexTiers);
415
+ let pauseAt = normalizeBoundedInteger(budget.pauseAt, fallback.pauseAt, 1, 100);
416
+ let resumeBelow = normalizeBoundedInteger(budget.resumeBelow, fallback.resumeBelow, 0, 99);
375
417
  if (pauseAt <= resumeBelow) {
376
418
  pauseAt = DEFAULT_BUDGET_CONFIG.pauseAt;
377
419
  resumeBelow = DEFAULT_BUDGET_CONFIG.resumeBelow;
378
420
  }
379
421
  return {
380
- enabled: normalizeBoolean(budget.enabled, DEFAULT_BUDGET_CONFIG.enabled),
381
- pollSeconds: normalizeBoundedInteger(budget.pollSeconds, DEFAULT_BUDGET_CONFIG.pollSeconds, 5, 3600),
422
+ enabled: normalizeBoolean(budget.enabled, fallback.enabled),
423
+ pollSeconds: normalizeBoundedInteger(budget.pollSeconds, fallback.pollSeconds, 5, 3600),
382
424
  pauseAt,
383
425
  resumeBelow,
384
- syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, DEFAULT_BUDGET_CONFIG.syncDriftPct, 1, 100),
426
+ syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, fallback.syncDriftPct, 1, 100),
385
427
  parallel: {
386
- minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct, DEFAULT_BUDGET_CONFIG.parallel.minRemainingPct, 1, 100),
387
- timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, DEFAULT_BUDGET_CONFIG.parallel.timeWindowSec, 60, 604800)
428
+ minRemainingPct: normalizeBoundedInteger(parallel.minRemainingPct, fallback.parallel.minRemainingPct, 1, 100),
429
+ timeWindowSec: normalizeBoundedInteger(parallel.timeWindowSec, fallback.parallel.timeWindowSec, 60, 604800)
388
430
  },
389
- codexTierControl: normalizeBoolean(budget.codexTierControl, DEFAULT_BUDGET_CONFIG.codexTierControl) && codexTiers.full !== null,
431
+ codexTierControl: normalizeBoolean(budget.codexTierControl, fallback.codexTierControl) && codexTiers.full !== null,
390
432
  codexTiers
391
433
  };
392
434
  }
@@ -423,15 +465,59 @@ class ConfigService {
423
465
  return existsSync2(this.configPath);
424
466
  }
425
467
  load() {
468
+ let raw;
426
469
  try {
427
- const raw = readFileSync2(this.configPath, "utf-8");
428
- return normalizeConfig(JSON.parse(raw));
429
- } catch {
430
- return null;
470
+ raw = readFileSync2(this.configPath, "utf-8");
471
+ } catch (err) {
472
+ if (err?.code === "ENOENT") {
473
+ return { state: "absent" };
474
+ }
475
+ return { state: "corrupt", reason: `config.json is unreadable: ${err.message}` };
476
+ }
477
+ let parsed;
478
+ try {
479
+ parsed = JSON.parse(raw);
480
+ } catch (err) {
481
+ return {
482
+ state: "corrupt",
483
+ reason: `config.json is not valid JSON: ${err.message}`
484
+ };
431
485
  }
486
+ if (!isRecord(parsed)) {
487
+ return { state: "corrupt", reason: "config.json is not a JSON object" };
488
+ }
489
+ const violation = findShapeViolation(parsed);
490
+ if (violation) {
491
+ return { state: "corrupt", reason: `config.json is shape-invalid: ${violation}` };
492
+ }
493
+ const config = normalizeConfig(parsed);
494
+ if (!config) {
495
+ return { state: "corrupt", reason: "config.json could not be normalized" };
496
+ }
497
+ return { state: "parsed", config };
432
498
  }
433
- loadOrDefault() {
434
- return this.load() ?? structuredClone(DEFAULT_CONFIG);
499
+ loadOrDefault(log = NOOP_LOGGER) {
500
+ const result = this.load();
501
+ if (result.state === "parsed")
502
+ return result.config;
503
+ if (result.state === "corrupt") {
504
+ log(`config.json at ${this.configPath} is unusable (${result.reason}); ` + "falling back to defaults \u2014 your custom budget thresholds / idle-shutdown settings are NOT in effect. " + "Fix the file and restart to re-apply them.");
505
+ }
506
+ return structuredClone(DEFAULT_CONFIG);
507
+ }
508
+ describeConfig() {
509
+ const result = this.load();
510
+ if (result.state === "absent") {
511
+ return { state: "absent", path: this.configPath, customValues: false };
512
+ }
513
+ if (result.state === "corrupt") {
514
+ return { state: "corrupt", path: this.configPath, reason: result.reason, customValues: false };
515
+ }
516
+ return {
517
+ state: "parsed",
518
+ path: this.configPath,
519
+ customValues: hasCustomDecisionValues(result.config)
520
+ };
435
521
  }
436
522
  save(config) {
437
523
  this.ensureConfigDir();
@@ -456,11 +542,11 @@ class ConfigService {
456
542
  }
457
543
  }
458
544
  }
459
- var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json";
545
+ var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json", NOOP_LOGGER = () => {};
460
546
  var init_config_service = __esm(() => {
461
547
  DEFAULT_BUDGET_CONFIG = {
462
548
  enabled: true,
463
- pollSeconds: 60,
549
+ pollSeconds: 300,
464
550
  pauseAt: 90,
465
551
  resumeBelow: 30,
466
552
  syncDriftPct: 10,
@@ -513,6 +599,30 @@ function registerMarketplace(marketplaceRoot) {
513
599
  }
514
600
  var init_pkg_root = () => {};
515
601
 
602
+ // src/cli/plugin-cache.ts
603
+ import { existsSync as existsSync4 } from "fs";
604
+ import { homedir as homedir2 } from "os";
605
+ import { join as join4, resolve } from "path";
606
+ function pluginCacheRoot(home = homedir2()) {
607
+ return join4(home, ".claude", "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME);
608
+ }
609
+ function isInsideRepoCheckout(projectRoot) {
610
+ const buildScript = resolve(projectRoot, "scripts", "build-bundles.mjs");
611
+ return existsSync4(buildScript);
612
+ }
613
+ function shouldWarnMissingPluginCache(cacheExists) {
614
+ return !cacheExists;
615
+ }
616
+ var MARKETPLACE_STEPS;
617
+ var init_plugin_cache = __esm(() => {
618
+ init_cli();
619
+ MARKETPLACE_STEPS = [
620
+ `/plugin marketplace add raysonmeng/agent-bridge`,
621
+ `/plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME}`,
622
+ `/reload-plugins`
623
+ ];
624
+ });
625
+
516
626
  // src/marker-section.ts
517
627
  function upsertMarkedSection(content, sectionId, section) {
518
628
  const startMarker = MARKER_START(sectionId);
@@ -649,11 +759,12 @@ var exports_init = {};
649
759
  __export(exports_init, {
650
760
  writeCollaborationSections: () => writeCollaborationSections,
651
761
  runInit: () => runInit,
762
+ pluginInstallFallbackGuidance: () => pluginInstallFallbackGuidance,
652
763
  compareVersions: () => compareVersions
653
764
  });
654
765
  import { execSync, execFileSync as execFileSync2 } from "child_process";
655
766
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
656
- import { join as join4 } from "path";
767
+ import { join as join5 } from "path";
657
768
  async function runInit() {
658
769
  console.log(`AgentBridge Init
659
770
  `);
@@ -681,25 +792,54 @@ async function runInit() {
681
792
  }
682
793
  console.log("");
683
794
  console.log("Installing AgentBridge plugin...");
795
+ let pluginInstalled = false;
684
796
  try {
685
- registerMarketplace(findPackageRoot());
797
+ const packageRoot = findPackageRoot();
798
+ registerMarketplace(packageRoot);
686
799
  execFileSync2("claude", ["plugin", "install", `${PLUGIN_NAME}@${MARKETPLACE_NAME}`], {
687
800
  stdio: "inherit"
688
801
  });
689
802
  console.log(" Plugin installed successfully.");
803
+ pluginInstalled = true;
690
804
  } catch {
691
805
  console.log(" Plugin install skipped (marketplace registration or install failed).");
692
- console.log(" You can install it later with:");
693
- console.log(` abg dev # registers marketplace and installs plugin`);
806
+ for (const line of pluginInstallFallbackGuidance(detectRepoCheckout())) {
807
+ console.log(line);
808
+ }
694
809
  }
695
810
  console.log("");
696
- console.log(`Setup complete!
811
+ if (pluginInstalled) {
812
+ console.log(`Setup complete!
697
813
  `);
814
+ } else {
815
+ console.log(`Setup incomplete \u2014 plugin not installed.
816
+ `);
817
+ process.exitCode = 1;
818
+ }
698
819
  console.log("Next steps:");
699
820
  console.log(" 1. If Claude Code is already running, execute /reload-plugins in your session");
700
821
  console.log(" 2. Start Claude Code: agentbridge claude");
701
822
  console.log(" 3. Start Codex TUI: agentbridge codex");
702
823
  }
824
+ function detectRepoCheckout() {
825
+ try {
826
+ return isInsideRepoCheckout(findPackageRoot());
827
+ } catch {
828
+ return false;
829
+ }
830
+ }
831
+ function pluginInstallFallbackGuidance(insideRepo) {
832
+ if (insideRepo) {
833
+ return [
834
+ " You can install it later with:",
835
+ " abg dev # registers marketplace and installs plugin"
836
+ ];
837
+ }
838
+ return [
839
+ " Install the plugin from Claude Code with these steps:",
840
+ ...MARKETPLACE_STEPS.map((step) => ` ${step}`)
841
+ ];
842
+ }
703
843
  function checkBun() {
704
844
  try {
705
845
  const version = execSync("bun --version", { encoding: "utf-8" }).trim();
@@ -745,8 +885,8 @@ function checkCodex() {
745
885
  function writeCollaborationSections(projectRoot) {
746
886
  const results = [];
747
887
  const files = [
748
- { name: "CLAUDE.md", path: join4(projectRoot, "CLAUDE.md"), section: CLAUDE_MD_SECTION },
749
- { name: "AGENTS.md", path: join4(projectRoot, "AGENTS.md"), section: AGENTS_MD_SECTION }
888
+ { name: "CLAUDE.md", path: join5(projectRoot, "CLAUDE.md"), section: CLAUDE_MD_SECTION },
889
+ { name: "AGENTS.md", path: join5(projectRoot, "AGENTS.md"), section: AGENTS_MD_SECTION }
750
890
  ];
751
891
  for (const { name, path, section } of files) {
752
892
  let existing = "";
@@ -781,6 +921,7 @@ var init_init = __esm(() => {
781
921
  init_config_service();
782
922
  init_cli();
783
923
  init_pkg_root();
924
+ init_plugin_cache();
784
925
  init_version_utils();
785
926
  });
786
927
 
@@ -790,19 +931,17 @@ __export(exports_dev, {
790
931
  runDev: () => runDev
791
932
  });
792
933
  import { execFileSync as execFileSync3, spawnSync } from "child_process";
793
- import { resolve } from "path";
794
- import { existsSync as existsSync4, cpSync, rmSync } from "fs";
795
- import { homedir as homedir2 } from "os";
934
+ import { resolve as resolve2 } from "path";
935
+ import { existsSync as existsSync5, cpSync, rmSync } from "fs";
796
936
  async function runDev(args = []) {
797
937
  console.log(`AgentBridge Dev Setup
798
938
  `);
799
939
  const skipBuild = args.includes("--skip-build");
800
940
  const projectRoot = findPackageRoot();
801
- const marketplacePath = resolve(projectRoot, ".claude-plugin", "marketplace.json");
802
- const pluginDir = resolve(projectRoot, "plugins", "agentbridge");
803
- const pluginManifest = resolve(pluginDir, ".claude-plugin", "plugin.json");
804
- const buildScript = resolve(projectRoot, "scripts", "build-bundles.mjs");
805
- if (!existsSync4(buildScript)) {
941
+ const marketplacePath = resolve2(projectRoot, ".claude-plugin", "marketplace.json");
942
+ const pluginDir = resolve2(projectRoot, "plugins", "agentbridge");
943
+ const pluginManifest = resolve2(pluginDir, ".claude-plugin", "plugin.json");
944
+ if (!isInsideRepoCheckout(projectRoot)) {
806
945
  console.error(" ERROR: 'agentbridge dev' must run inside an AgentBridge repository checkout \u2014");
807
946
  console.error(" the published package does not ship the build scripts.");
808
947
  console.error("");
@@ -839,12 +978,12 @@ async function runDev(args = []) {
839
978
  console.log(` \u2713 Plugin built successfully
840
979
  `);
841
980
  }
842
- if (!existsSync4(pluginManifest)) {
981
+ if (!existsSync5(pluginManifest)) {
843
982
  console.error(` ERROR: Plugin manifest not found at ${pluginManifest}`);
844
983
  console.error(" Run 'bun run build:plugin' first, or check your working tree.");
845
984
  process.exit(1);
846
985
  }
847
- if (!existsSync4(marketplacePath)) {
986
+ if (!existsSync5(marketplacePath)) {
848
987
  console.error(` ERROR: Marketplace manifest not found at ${marketplacePath}`);
849
988
  process.exit(1);
850
989
  }
@@ -873,12 +1012,12 @@ Installing plugin...`);
873
1012
  }
874
1013
  console.log(`
875
1014
  Syncing local plugin to cache...`);
876
- const cacheDir = resolve(homedir2(), ".claude", "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME);
877
- if (existsSync4(cacheDir)) {
1015
+ const cacheDir = pluginCacheRoot();
1016
+ if (existsSync5(cacheDir)) {
878
1017
  const versionDirs = Bun.spawnSync(["ls", cacheDir]).stdout.toString().trim().split(`
879
1018
  `).filter(Boolean);
880
1019
  for (const ver of versionDirs) {
881
- const targetDir = resolve(cacheDir, ver);
1020
+ const targetDir = resolve2(cacheDir, ver);
882
1021
  rmSync(targetDir, { recursive: true, force: true });
883
1022
  cpSync(pluginDir, targetDir, { recursive: true });
884
1023
  console.log(` Synced to ${targetDir}`);
@@ -898,15 +1037,23 @@ Syncing local plugin to cache...`);
898
1037
  var init_dev = __esm(() => {
899
1038
  init_cli();
900
1039
  init_pkg_root();
1040
+ init_plugin_cache();
901
1041
  });
902
1042
 
903
1043
  // src/control-protocol.ts
904
1044
  var CLOSE_CODE_REPLACED = 4001, CLOSE_CODE_EVICTED_STALE = 4002, CLOSE_CODE_PROBE_IN_PROGRESS = 4003, CLOSE_CODE_PAIR_MISMATCH = 4004;
905
1045
 
1046
+ // src/interrupt-timing.ts
1047
+ var CLIENT_REPLY_TIMEOUT_MS = 15000, INTERRUPT_CLIENT_MARGIN_MS = 2000, MAX_INTERRUPT_TIMEOUT_MS;
1048
+ var init_interrupt_timing = __esm(() => {
1049
+ MAX_INTERRUPT_TIMEOUT_MS = CLIENT_REPLY_TIMEOUT_MS - INTERRUPT_CLIENT_MARGIN_MS;
1050
+ });
1051
+
906
1052
  // src/daemon-client.ts
907
1053
  import { EventEmitter } from "events";
908
1054
  var nextSocketId = 0, DaemonClient;
909
1055
  var init_daemon_client = __esm(() => {
1056
+ init_interrupt_timing();
910
1057
  DaemonClient = class DaemonClient extends EventEmitter {
911
1058
  url;
912
1059
  options;
@@ -933,7 +1080,7 @@ var init_daemon_client = __esm(() => {
933
1080
  this.ws = null;
934
1081
  }
935
1082
  const socketId = ++nextSocketId;
936
- await new Promise((resolve2, reject) => {
1083
+ await new Promise((resolve3, reject) => {
937
1084
  const ws = new WebSocket(this.url);
938
1085
  let settled = false;
939
1086
  ws.onopen = () => {
@@ -942,7 +1089,7 @@ var init_daemon_client = __esm(() => {
942
1089
  this.wsId = socketId;
943
1090
  this.attachSocketHandlers(ws, socketId);
944
1091
  this.log(`ws#${socketId} opened and attached`);
945
- resolve2();
1092
+ resolve3();
946
1093
  };
947
1094
  ws.onerror = () => {
948
1095
  if (settled)
@@ -968,7 +1115,7 @@ var init_daemon_client = __esm(() => {
968
1115
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
969
1116
  return null;
970
1117
  }
971
- return await new Promise((resolve2) => {
1118
+ return await new Promise((resolve3) => {
972
1119
  let settled = false;
973
1120
  let timer = null;
974
1121
  const cleanup = () => {
@@ -985,7 +1132,7 @@ var init_daemon_client = __esm(() => {
985
1132
  };
986
1133
  const finish = (value) => {
987
1134
  cleanup();
988
- resolve2(value);
1135
+ resolve3(value);
989
1136
  };
990
1137
  const onStatus = (status) => finish(status);
991
1138
  const onRejected = () => finish(null);
@@ -1007,7 +1154,7 @@ var init_daemon_client = __esm(() => {
1007
1154
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1008
1155
  return { connected: false, alive: false };
1009
1156
  }
1010
- return await new Promise((resolve2) => {
1157
+ return await new Promise((resolve3) => {
1011
1158
  let settled = false;
1012
1159
  let timer = null;
1013
1160
  const finish = (value) => {
@@ -1019,7 +1166,7 @@ var init_daemon_client = __esm(() => {
1019
1166
  this.off("incumbentStatus", onStatus);
1020
1167
  this.off("disconnect", onDisconnect);
1021
1168
  this.off("rejected", onRejected);
1022
- resolve2(value);
1169
+ resolve3(value);
1023
1170
  };
1024
1171
  const onStatus = (s) => finish(s);
1025
1172
  const onDisconnect = () => finish({ connected: false, alive: false });
@@ -1047,23 +1194,24 @@ var init_daemon_client = __esm(() => {
1047
1194
  this.ws = null;
1048
1195
  this.rejectPendingReplies("Daemon connection closed");
1049
1196
  }
1050
- async sendReply(message, requireReply, onBusy) {
1197
+ async sendReply(message, requireReply, onBusy, idempotencyKey) {
1051
1198
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1052
1199
  return { success: false, error: "AgentBridge daemon is not connected." };
1053
1200
  }
1054
1201
  const requestId = `reply_${Date.now()}_${this.nextRequestId++}`;
1055
- return new Promise((resolve2) => {
1202
+ return new Promise((resolve3) => {
1056
1203
  const timer = setTimeout(() => {
1057
1204
  this.pendingReplies.delete(requestId);
1058
- resolve2({ success: false, error: "Timed out waiting for AgentBridge daemon reply." });
1059
- }, 15000);
1060
- this.pendingReplies.set(requestId, { resolve: resolve2, timer });
1205
+ resolve3({ success: false, error: "Timed out waiting for AgentBridge daemon reply." });
1206
+ }, CLIENT_REPLY_TIMEOUT_MS);
1207
+ this.pendingReplies.set(requestId, { resolve: resolve3, timer });
1061
1208
  this.send({
1062
1209
  type: "claude_to_codex",
1063
1210
  requestId,
1064
1211
  message,
1065
1212
  ...requireReply ? { requireReply: true } : {},
1066
- ...onBusy && onBusy !== "reject" ? { onBusy } : {}
1213
+ ...onBusy && onBusy !== "reject" ? { onBusy } : {},
1214
+ ...idempotencyKey ? { idempotencyKey } : {}
1067
1215
  });
1068
1216
  });
1069
1217
  }
@@ -1086,9 +1234,23 @@ var init_daemon_client = __esm(() => {
1086
1234
  return;
1087
1235
  clearTimeout(pending.timer);
1088
1236
  this.pendingReplies.delete(message.requestId);
1089
- pending.resolve({ success: message.success, error: message.error });
1237
+ pending.resolve({
1238
+ success: message.success,
1239
+ error: message.error,
1240
+ ...message.code !== undefined ? { code: message.code } : {},
1241
+ ...message.phase !== undefined ? { phase: message.phase } : {},
1242
+ ...message.retryAfterMs !== undefined ? { retryAfterMs: message.retryAfterMs } : {}
1243
+ });
1090
1244
  return;
1091
1245
  }
1246
+ case "turn_started":
1247
+ this.emit("turnStarted", {
1248
+ requestId: message.requestId,
1249
+ ...message.idempotencyKey !== undefined ? { idempotencyKey: message.idempotencyKey } : {},
1250
+ threadId: message.threadId,
1251
+ turnId: message.turnId
1252
+ });
1253
+ return;
1092
1254
  case "status":
1093
1255
  this.emit("status", message.status);
1094
1256
  return;
@@ -1165,8 +1327,8 @@ function formatBuildInfo(build) {
1165
1327
  var BUILD_INFO;
1166
1328
  var init_build_info = __esm(() => {
1167
1329
  BUILD_INFO = Object.freeze({
1168
- version: defineString("0.1.11", "0.0.0-source"),
1169
- commit: defineString("48eb0ed", "source"),
1330
+ version: defineString("0.1.12", "0.0.0-source"),
1331
+ commit: defineString("eec6018", "source"),
1170
1332
  bundle: defineBundle("dist"),
1171
1333
  contractVersion: defineNumber(1, CONTRACT_VERSION)
1172
1334
  });
@@ -1332,8 +1494,50 @@ var init_process_lifecycle = __esm(() => {
1332
1494
 
1333
1495
  // src/daemon-lifecycle.ts
1334
1496
  import { spawn } from "child_process";
1335
- import { existsSync as existsSync5, readFileSync as readFileSync4, statSync, unlinkSync, writeFileSync as writeFileSync4, openSync, closeSync, constants } from "fs";
1497
+ import { existsSync as existsSync6, readFileSync as readFileSync4, statSync, unlinkSync, writeFileSync as writeFileSync4, openSync, closeSync, constants } from "fs";
1336
1498
  import { fileURLToPath } from "url";
1499
+ function isReuseVerdict(verdict) {
1500
+ return verdict === "reuse" || verdict === "reuse-despite-drift";
1501
+ }
1502
+ function classifyDaemon(expectedPairId, status, buildInfo) {
1503
+ if (!status) {
1504
+ return { verdict: "unreachable", reason: "daemon status is unavailable or unparseable" };
1505
+ }
1506
+ const reportedPairId = status.pairId;
1507
+ if (!expectedPairId && reportedPairId != null) {
1508
+ return {
1509
+ verdict: "manual-conflict",
1510
+ reason: `manual mode must not adopt registered pair ${reportedPairId}`
1511
+ };
1512
+ }
1513
+ if (expectedPairId) {
1514
+ if (reportedPairId == null) {
1515
+ return {
1516
+ verdict: "replace-foreign",
1517
+ reason: `pair ${expectedPairId} found daemon without pair identity`
1518
+ };
1519
+ }
1520
+ if (reportedPairId !== expectedPairId) {
1521
+ return {
1522
+ verdict: "replace-foreign",
1523
+ reason: `pair ${expectedPairId} found daemon for pair ${reportedPairId}`
1524
+ };
1525
+ }
1526
+ }
1527
+ if (!sameRuntimeContract(status.build, buildInfo)) {
1528
+ if (compatibleContractVersion(status.build, buildInfo) && status.tuiConnected === true) {
1529
+ return {
1530
+ verdict: "reuse-despite-drift",
1531
+ reason: "runtime build drift has a compatible contract and a live Codex TUI is attached"
1532
+ };
1533
+ }
1534
+ return {
1535
+ verdict: "replace-drifted",
1536
+ reason: `runtime build ${formatBuildInfo(status.build)} does not match launcher ${formatBuildInfo(buildInfo)}`
1537
+ };
1538
+ }
1539
+ return { verdict: "reuse", reason: "daemon pair and runtime contract match" };
1540
+ }
1337
1541
 
1338
1542
  class DaemonLifecycle {
1339
1543
  stateDir;
@@ -1366,52 +1570,37 @@ class DaemonLifecycle {
1366
1570
  return null;
1367
1571
  }
1368
1572
  }
1369
- isForeignDaemon(status) {
1370
- const expected = this.expectedPairId;
1371
- if (!expected)
1372
- return false;
1373
- if (!status)
1374
- return false;
1375
- const reported = status.pairId;
1376
- if (reported == null)
1377
- return true;
1378
- return reported !== expected;
1379
- }
1380
- isRegisteredPairDaemonInManualMode(status) {
1381
- return !this.expectedPairId && status?.pairId != null;
1382
- }
1383
- isBuildDrifted(status) {
1384
- if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1")
1385
- return false;
1386
- const runtime = status?.build;
1387
- if (!runtime)
1388
- return true;
1389
- return !sameRuntimeContract(runtime, BUILD_INFO);
1573
+ classifyDaemon(status) {
1574
+ const classification = classifyDaemon(this.expectedPairId, status, BUILD_INFO);
1575
+ if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1" && (classification.verdict === "replace-drifted" || classification.verdict === "unreachable")) {
1576
+ return { verdict: "reuse", reason: "build drift replacement disabled by AGENTBRIDGE_ALLOW_BUILD_DRIFT" };
1577
+ }
1578
+ return classification;
1390
1579
  }
1391
- canReuseDespiteDrift(status) {
1392
- if (!compatibleContractVersion(status?.build, BUILD_INFO))
1393
- return false;
1394
- return status?.tuiConnected === true;
1580
+ manualConflictError(status) {
1581
+ return new Error(`Control port ${this.controlPort} is owned by registered pair ${status?.pairId}. ` + `This session has no pair identity (manual mode) and will not reuse or replace it \u2014 ` + `start with \`agentbridge claude\` from that pair's directory, or set AGENTBRIDGE_CONTROL_PORT to a free port.`);
1395
1582
  }
1396
1583
  async ensureRunning() {
1397
1584
  if (await this.isHealthy()) {
1398
1585
  const status = await this.fetchStatus();
1399
- if (this.isRegisteredPairDaemonInManualMode(status)) {
1400
- throw new Error(`Control port ${this.controlPort} is owned by registered pair ${status?.pairId}. ` + `This session has no pair identity (manual mode) and will not reuse or replace it \u2014 ` + `start with \`agentbridge claude\` from that pair's directory, or set AGENTBRIDGE_CONTROL_PORT to a free port.`);
1401
- }
1402
- if (this.isForeignDaemon(status)) {
1403
- this.log(`Control port ${this.controlPort} held by a daemon for pair ${status?.pairId ?? "<none>"}, ` + `but this pair is ${this.expectedPairId} \u2014 replacing foreign daemon`);
1404
- await this.replaceUnhealthyDaemon(status?.pid);
1405
- return;
1406
- }
1407
- if (this.isBuildDrifted(status)) {
1408
- if (this.canReuseDespiteDrift(status)) {
1409
- this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `(launcher ${formatBuildInfo(BUILD_INFO)}) but a live Codex TUI is attached \u2014 reusing instead of ` + `replacing; the new build is picked up at the next restart (abg kill, then relaunch)`);
1410
- } else {
1586
+ const classification = this.classifyDaemon(status);
1587
+ switch (classification.verdict) {
1588
+ case "manual-conflict":
1589
+ throw this.manualConflictError(status);
1590
+ case "replace-foreign":
1591
+ this.log(`Control port ${this.controlPort} held by a daemon for pair ${status?.pairId ?? "<none>"}, ` + `but this pair is ${this.expectedPairId} \u2014 replacing foreign daemon`);
1592
+ await this.replaceUnhealthyDaemon(status?.pid);
1593
+ return;
1594
+ case "replace-drifted":
1595
+ case "unreachable":
1411
1596
  this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `but launcher is ${formatBuildInfo(BUILD_INFO)} \u2014 replacing drifted daemon`);
1412
1597
  await this.replaceUnhealthyDaemon(status?.pid);
1413
1598
  return;
1414
- }
1599
+ case "reuse-despite-drift":
1600
+ this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `(launcher ${formatBuildInfo(BUILD_INFO)}) but a live Codex TUI is attached \u2014 reusing instead of ` + `replacing; the new build is picked up at the next restart (abg kill, then relaunch)`);
1601
+ break;
1602
+ case "reuse":
1603
+ break;
1415
1604
  }
1416
1605
  try {
1417
1606
  await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
@@ -1441,14 +1630,17 @@ class DaemonLifecycle {
1441
1630
  }
1442
1631
  await this.withStartupLockStrict(async (locked) => {
1443
1632
  if (!locked) {
1444
- this.log("Another process holds the startup lock, waiting for readiness+identity...");
1445
- await this.waitForReadyAndOurs();
1633
+ await this.waitForContendedStartupLock();
1446
1634
  return;
1447
1635
  }
1448
1636
  if (await this.isHealthy()) {
1449
1637
  const status = await this.fetchStatus();
1450
- if (this.isForeignDaemon(status) || this.isBuildDrifted(status) && !this.canReuseDespiteDrift(status)) {
1451
- this.log(`Daemon on control port ${this.controlPort} is not reusable under startup lock ` + `(pair=${status?.pairId ?? "<none>"}, build=${formatBuildInfo(status?.build)}) \u2014 replacing`);
1638
+ const classification = this.classifyDaemon(status);
1639
+ if (classification.verdict === "manual-conflict") {
1640
+ throw this.manualConflictError(status);
1641
+ }
1642
+ if (!isReuseVerdict(classification.verdict)) {
1643
+ this.log(`Daemon on control port ${this.controlPort} is not reusable under startup lock ` + `(pair=${status?.pairId ?? "<none>"}, build=${formatBuildInfo(status?.build)}, ` + `reason=${classification.reason}) \u2014 replacing`);
1452
1644
  await this.kill(3000, status?.pid);
1453
1645
  } else {
1454
1646
  try {
@@ -1476,7 +1668,7 @@ class DaemonLifecycle {
1476
1668
  for (let attempt = 0;attempt < maxRetries; attempt++) {
1477
1669
  if (await this.isHealthy())
1478
1670
  return;
1479
- await new Promise((resolve2) => setTimeout(resolve2, delayMs));
1671
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
1480
1672
  }
1481
1673
  throw new Error(`Timed out waiting for AgentBridge daemon health on ${this.healthUrl}`);
1482
1674
  }
@@ -1492,7 +1684,7 @@ class DaemonLifecycle {
1492
1684
  for (let attempt = 0;attempt < maxRetries; attempt++) {
1493
1685
  if (await this.isReady())
1494
1686
  return;
1495
- await new Promise((resolve2) => setTimeout(resolve2, delayMs));
1687
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
1496
1688
  }
1497
1689
  throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
1498
1690
  }
@@ -1500,11 +1692,15 @@ class DaemonLifecycle {
1500
1692
  for (let attempt = 0;attempt < maxRetries; attempt++) {
1501
1693
  if (await this.isReady()) {
1502
1694
  const status = await this.fetchStatus();
1503
- if (!this.isForeignDaemon(status) && (!this.isBuildDrifted(status) || this.canReuseDespiteDrift(status))) {
1695
+ const classification = this.classifyDaemon(status);
1696
+ if (classification.verdict === "manual-conflict") {
1697
+ throw this.manualConflictError(status);
1698
+ }
1699
+ if (isReuseVerdict(classification.verdict)) {
1504
1700
  return;
1505
1701
  }
1506
1702
  }
1507
- await new Promise((resolve2) => setTimeout(resolve2, delayMs));
1703
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
1508
1704
  }
1509
1705
  throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
1510
1706
  }
@@ -1558,7 +1754,7 @@ class DaemonLifecycle {
1558
1754
  } catch {}
1559
1755
  }
1560
1756
  wasKilled() {
1561
- return existsSync5(this.stateDir.killedFile);
1757
+ return existsSync6(this.stateDir.killedFile);
1562
1758
  }
1563
1759
  launch() {
1564
1760
  this.stateDir.ensure();
@@ -1582,13 +1778,16 @@ class DaemonLifecycle {
1582
1778
  async replaceUnhealthyDaemon(statusPid) {
1583
1779
  await this.withStartupLockStrict(async (locked) => {
1584
1780
  if (!locked) {
1585
- this.log("Another process holds the startup lock, waiting for readiness+identity...");
1586
- await this.waitForReadyAndOurs();
1781
+ await this.waitForContendedStartupLock();
1587
1782
  return;
1588
1783
  }
1589
1784
  if (await this.isHealthy()) {
1590
1785
  const status = await this.fetchStatus();
1591
- if (!this.isForeignDaemon(status) && (!this.isBuildDrifted(status) || this.canReuseDespiteDrift(status))) {
1786
+ const classification = this.classifyDaemon(status);
1787
+ if (classification.verdict === "manual-conflict") {
1788
+ throw this.manualConflictError(status);
1789
+ }
1790
+ if (isReuseVerdict(classification.verdict)) {
1592
1791
  try {
1593
1792
  await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1594
1793
  return;
@@ -1601,6 +1800,10 @@ class DaemonLifecycle {
1601
1800
  await this.waitForReady();
1602
1801
  });
1603
1802
  }
1803
+ async waitForContendedStartupLock() {
1804
+ this.log("Another process holds the startup lock, waiting for readiness+identity...");
1805
+ await this.waitForReadyAndOurs();
1806
+ }
1604
1807
  async withStartupLockStrict(fn) {
1605
1808
  const locked = this.acquireLockStrict();
1606
1809
  try {
@@ -1693,7 +1896,7 @@ class DaemonLifecycle {
1693
1896
  this.cleanup();
1694
1897
  return true;
1695
1898
  }
1696
- await new Promise((resolve2) => setTimeout(resolve2, 200));
1899
+ await new Promise((resolve3) => setTimeout(resolve3, 200));
1697
1900
  }
1698
1901
  this.log(`Daemon pid ${pid} did not stop gracefully, sending SIGKILL`);
1699
1902
  try {
@@ -1731,7 +1934,7 @@ var init_daemon_lifecycle = __esm(() => {
1731
1934
  import { execFileSync as execFileSync5 } from "child_process";
1732
1935
  import {
1733
1936
  closeSync as closeSync2,
1734
- existsSync as existsSync6,
1937
+ existsSync as existsSync7,
1735
1938
  fsyncSync,
1736
1939
  linkSync,
1737
1940
  lstatSync,
@@ -1749,7 +1952,7 @@ import {
1749
1952
  import { createServer } from "net";
1750
1953
  import { createHash, randomUUID } from "crypto";
1751
1954
  import { hostname, userInfo } from "os";
1752
- import { basename as basename2, join as join5, resolve as resolve2, sep } from "path";
1955
+ import { basename as basename2, join as join6, resolve as resolve3, sep } from "path";
1753
1956
  function portsForSlot(slot) {
1754
1957
  if (!Number.isInteger(slot) || slot < 0) {
1755
1958
  throw new PairError("PAIR_ID_INVALID", `Invalid slot: ${slot}`);
@@ -1787,14 +1990,14 @@ function pickLowestFreeSlot(entries) {
1787
1990
  return slot;
1788
1991
  }
1789
1992
  function pairsDir(base) {
1790
- return join5(base, "pairs");
1993
+ return join6(base, "pairs");
1791
1994
  }
1792
1995
  function registryPath(base) {
1793
- return join5(pairsDir(base), REGISTRY_FILE_NAME);
1996
+ return join6(pairsDir(base), REGISTRY_FILE_NAME);
1794
1997
  }
1795
1998
  function readRegistry(base) {
1796
1999
  const path = registryPath(base);
1797
- if (!existsSync6(path))
2000
+ if (!existsSync7(path))
1798
2001
  return { version: 1, pairs: [] };
1799
2002
  let parsed;
1800
2003
  try {
@@ -1844,7 +2047,7 @@ function writeRegistry(base, reg) {
1844
2047
  renameSync(tmp, target);
1845
2048
  }
1846
2049
  function lockFilePath(base) {
1847
- return join5(pairsDir(base), LOCK_FILE_NAME);
2050
+ return join6(pairsDir(base), LOCK_FILE_NAME);
1848
2051
  }
1849
2052
  function readLockOwner(lockFile) {
1850
2053
  try {
@@ -1878,7 +2081,7 @@ function safeUid() {
1878
2081
  }
1879
2082
  }
1880
2083
  function sleep(ms) {
1881
- return new Promise((resolve3) => setTimeout(resolve3, ms));
2084
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
1882
2085
  }
1883
2086
  function lockIsStale(lockFile) {
1884
2087
  const owner = readLockOwner(lockFile);
@@ -1990,8 +2193,8 @@ async function withRegistryLock(base, fn) {
1990
2193
  }
1991
2194
  }
1992
2195
  function detectLegacyRootDaemon(base) {
1993
- const rootPidFile = join5(base, "daemon.pid");
1994
- if (!existsSync6(rootPidFile))
2196
+ const rootPidFile = join6(base, "daemon.pid");
2197
+ if (!existsSync7(rootPidFile))
1995
2198
  return null;
1996
2199
  let pid;
1997
2200
  try {
@@ -2005,11 +2208,11 @@ function detectLegacyRootDaemon(base) {
2005
2208
  return { pid, controlPort: LEGACY_ROOT_CONTROL_PORT };
2006
2209
  }
2007
2210
  function probePortFree(port) {
2008
- return new Promise((resolve3) => {
2211
+ return new Promise((resolve4) => {
2009
2212
  const server = createServer();
2010
- server.once("error", () => resolve3(false));
2213
+ server.once("error", () => resolve4(false));
2011
2214
  server.once("listening", () => {
2012
- server.close(() => resolve3(true));
2215
+ server.close(() => resolve4(true));
2013
2216
  });
2014
2217
  server.listen(port, "127.0.0.1");
2015
2218
  });
@@ -2086,7 +2289,7 @@ async function resolvePair(base, opts) {
2086
2289
  pairId: entry.pairId,
2087
2290
  slot,
2088
2291
  ports,
2089
- stateDir: join5(pairsDir(base), entry.pairId),
2292
+ stateDir: join6(pairsDir(base), entry.pairId),
2090
2293
  name: entry.name ?? name,
2091
2294
  entry,
2092
2295
  warning
@@ -2094,7 +2297,7 @@ async function resolvePair(base, opts) {
2094
2297
  }
2095
2298
  async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
2096
2299
  await withRegistryLock(base, () => {
2097
- if (existsSync6(pairDirPath(base, pairId)) || pairDirDaemonAlive(base, pairId))
2300
+ if (existsSync7(pairDirPath(base, pairId)) || pairDirDaemonAlive(base, pairId))
2098
2301
  return;
2099
2302
  const reg = readRegistry(base);
2100
2303
  const nextPairs = reg.pairs.filter((pair) => !(pair.pairId === pairId && pair.slot === slot));
@@ -2105,19 +2308,19 @@ async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
2105
2308
  }
2106
2309
  function pairDirPath(base, pairId) {
2107
2310
  const id = validatePairId(pairId);
2108
- return join5(pairsDir(base), id);
2311
+ return join6(pairsDir(base), id);
2109
2312
  }
2110
2313
  function removePairDir(base, pairId) {
2111
2314
  const id = validatePairId(pairId);
2112
2315
  const root = pairsDir(base);
2113
- const dir = join5(root, id);
2114
- const canonicalRoot = resolve2(root);
2115
- const canonicalDir = resolve2(dir);
2316
+ const dir = join6(root, id);
2317
+ const canonicalRoot = resolve3(root);
2318
+ const canonicalDir = resolve3(dir);
2116
2319
  if (canonicalDir === canonicalRoot || !canonicalDir.startsWith(canonicalRoot + sep)) {
2117
2320
  throw new PairError("PAIR_ID_INVALID", `Refusing to remove a pair dir outside ${canonicalRoot}: ${canonicalDir}`, { pairId });
2118
2321
  }
2119
2322
  assertPairsRootNotSymlinked(root);
2120
- if (!existsSync6(canonicalDir))
2323
+ if (!existsSync7(canonicalDir))
2121
2324
  return false;
2122
2325
  rmSync2(canonicalDir, { recursive: true, force: true });
2123
2326
  return true;
@@ -2135,22 +2338,22 @@ function assertPairsRootNotSymlinked(root) {
2135
2338
  }
2136
2339
  function listPairDirs(base) {
2137
2340
  const root = pairsDir(base);
2138
- if (!existsSync6(root))
2341
+ if (!existsSync7(root))
2139
2342
  return [];
2140
2343
  if (lstatSync(root).isSymbolicLink())
2141
2344
  return [];
2142
2345
  return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2143
2346
  }
2144
2347
  function pairDirDaemonAlive(base, pairId) {
2145
- const dir = join5(pairsDir(base), pairId);
2348
+ const dir = join6(pairsDir(base), pairId);
2146
2349
  const pids = [];
2147
2350
  try {
2148
- const pid = Number.parseInt(readFileSync5(join5(dir, "daemon.pid"), "utf-8").trim(), 10);
2351
+ const pid = Number.parseInt(readFileSync5(join6(dir, "daemon.pid"), "utf-8").trim(), 10);
2149
2352
  if (Number.isFinite(pid))
2150
2353
  pids.push(pid);
2151
2354
  } catch {}
2152
2355
  try {
2153
- const status = JSON.parse(readFileSync5(join5(dir, "status.json"), "utf-8"));
2356
+ const status = JSON.parse(readFileSync5(join6(dir, "status.json"), "utf-8"));
2154
2357
  if (typeof status?.pid === "number")
2155
2358
  pids.push(status.pid);
2156
2359
  } catch {}
@@ -2283,7 +2486,7 @@ var init_env_guard = __esm(() => {
2283
2486
 
2284
2487
  // src/pair-resolver.ts
2285
2488
  import { realpathSync as realpathSync2 } from "fs";
2286
- import { join as join6, resolve as resolve3 } from "path";
2489
+ import { join as join7, resolve as resolve4 } from "path";
2287
2490
  function computeBaseDir() {
2288
2491
  return process.env.AGENTBRIDGE_BASE_DIR || process.env.AGENTBRIDGE_STATE_DIR || StateDirResolver.platformBaseDir();
2289
2492
  }
@@ -2410,7 +2613,7 @@ function resolvePairReadOnly(pairFlag) {
2410
2613
  pairId: entry.pairId,
2411
2614
  slot: entry.slot,
2412
2615
  ports: portsForEntry(entry),
2413
- stateDir: new StateDirResolver(join6(base, "pairs", entry.pairId)),
2616
+ stateDir: new StateDirResolver(join7(base, "pairs", entry.pairId)),
2414
2617
  name: entry.name ?? name,
2415
2618
  manual: false
2416
2619
  }
@@ -2423,7 +2626,7 @@ function resolvePairReadOnly(pairFlag) {
2423
2626
  pairId,
2424
2627
  slot: null,
2425
2628
  ports: { appPort: 0, proxyPort: 0, controlPort: 0 },
2426
- stateDir: new StateDirResolver(join6(base, "pairs", pairId)),
2629
+ stateDir: new StateDirResolver(join7(base, "pairs", pairId)),
2427
2630
  name,
2428
2631
  manual: false
2429
2632
  }
@@ -2453,7 +2656,7 @@ function portsForEntry(entry) {
2453
2656
  return portsForSlot(entry.slot);
2454
2657
  }
2455
2658
  function canonicalizeCwd(cwd) {
2456
- const absolute = resolve3(cwd);
2659
+ const absolute = resolve4(cwd);
2457
2660
  try {
2458
2661
  return realpathSync2.native(absolute);
2459
2662
  } catch {
@@ -2470,8 +2673,8 @@ var init_pair_resolver = __esm(() => {
2470
2673
  });
2471
2674
 
2472
2675
  // src/trace-log.ts
2473
- import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
2474
- import { join as join7 } from "path";
2676
+ import { appendFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, statSync as statSync3, unlinkSync as unlinkSync3 } from "fs";
2677
+ import { join as join8 } from "path";
2475
2678
  function pickRelevantEnv(env) {
2476
2679
  const picked = {};
2477
2680
  for (const [key, value] of Object.entries(env)) {
@@ -2506,7 +2709,7 @@ function redactArgv(argv) {
2506
2709
  }
2507
2710
  function traceLogPath(cwd, timestamp) {
2508
2711
  const day = timestamp.slice(0, 10);
2509
- return join7(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
2712
+ return join8(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
2510
2713
  }
2511
2714
  function appendTraceEvent(input) {
2512
2715
  const timestamp = input.timestamp ?? new Date().toISOString();
@@ -2520,11 +2723,39 @@ function appendTraceEvent(input) {
2520
2723
  ...input.env ? { env: pickRelevantEnv(input.env) } : {},
2521
2724
  ...input.data ? { data: redactData(input.data) } : {}
2522
2725
  };
2523
- mkdirSync4(join7(input.cwd, ".agentbridge", "logs"), { recursive: true });
2726
+ const logsDir = join8(input.cwd, ".agentbridge", "logs");
2727
+ const isNewDayFile = !existsSync8(path);
2728
+ mkdirSync4(logsDir, { recursive: true });
2729
+ if (isNewDayFile) {
2730
+ pruneOldTraceLogs(logsDir, path, Date.parse(timestamp));
2731
+ }
2524
2732
  appendFileSync(path, JSON.stringify(event) + `
2525
2733
  `, "utf-8");
2526
2734
  return path;
2527
2735
  }
2736
+ function pruneOldTraceLogs(logsDir, keepPath, nowMs) {
2737
+ if (!Number.isFinite(nowMs))
2738
+ return;
2739
+ const cutoff = nowMs - TRACE_RETENTION_DAYS * 24 * 60 * 60 * 1000;
2740
+ let entries;
2741
+ try {
2742
+ entries = readdirSync2(logsDir);
2743
+ } catch {
2744
+ return;
2745
+ }
2746
+ for (const name of entries) {
2747
+ if (!TRACE_FILE_RE.test(name))
2748
+ continue;
2749
+ const filePath = join8(logsDir, name);
2750
+ if (filePath === keepPath)
2751
+ continue;
2752
+ try {
2753
+ if (statSync3(filePath).mtimeMs < cutoff) {
2754
+ unlinkSync3(filePath);
2755
+ }
2756
+ } catch {}
2757
+ }
2758
+ }
2528
2759
  function isEnvSnapshot(key, value) {
2529
2760
  return /env$/i.test(key) && !!value && typeof value === "object" && !Array.isArray(value);
2530
2761
  }
@@ -2550,8 +2781,9 @@ function redactData(value, key = "") {
2550
2781
  }
2551
2782
  return value;
2552
2783
  }
2553
- var SECRET_KEY_RE, SECRET_ARG_RE, RELEVANT_ENV_RE;
2784
+ var TRACE_RETENTION_DAYS = 7, TRACE_FILE_RE, SECRET_KEY_RE, SECRET_ARG_RE, RELEVANT_ENV_RE;
2554
2785
  var init_trace_log = __esm(() => {
2786
+ TRACE_FILE_RE = /^trace-\d{4}-\d{2}-\d{2}\.jsonl$/;
2555
2787
  SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
2556
2788
  SECRET_ARG_RE = /^--?(?:token|secret|password|passwd|apikey|api-key|api_key|auth|cookie|session)(?:=.*)?$/i;
2557
2789
  RELEVANT_ENV_RE = /^(AGENTBRIDGE_|CODEX_)/;
@@ -2600,10 +2832,12 @@ var init_max_permissions = __esm(() => {
2600
2832
  // src/cli/claude.ts
2601
2833
  var exports_claude = {};
2602
2834
  __export(exports_claude, {
2835
+ warnIfPluginCacheMissing: () => warnIfPluginCacheMissing,
2603
2836
  runClaude: () => runClaude,
2604
2837
  checkOwnedFlagConflicts: () => checkOwnedFlagConflicts
2605
2838
  });
2606
2839
  import { spawn as spawn2 } from "child_process";
2840
+ import { existsSync as existsSync9 } from "fs";
2607
2841
  async function runClaude(args) {
2608
2842
  const originalEnv = { ...process.env };
2609
2843
  const envGuardResult = guardAgentBridgeEnv({
@@ -2641,6 +2875,7 @@ async function runClaude(args) {
2641
2875
  }
2642
2876
  await assertPairNotLive(lifecycle, pair);
2643
2877
  lifecycle.clearKilled();
2878
+ warnIfPluginCacheMissing();
2644
2879
  const channelEntry = `plugin:${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
2645
2880
  if (permissionPlan.inject) {
2646
2881
  console.error(`[agentbridge] running with ${CLAUDE_MAX_PERMISSION_FLAG} (default; opt out with --safe or AGENTBRIDGE_SAFE=1)`);
@@ -2668,6 +2903,18 @@ async function runClaude(args) {
2668
2903
  process.exit(1);
2669
2904
  });
2670
2905
  }
2906
+ function warnIfPluginCacheMissing(cacheRoot = pluginCacheRoot(), log = (msg) => console.error(msg)) {
2907
+ let cacheExists;
2908
+ try {
2909
+ cacheExists = existsSync9(cacheRoot);
2910
+ } catch {
2911
+ return false;
2912
+ }
2913
+ if (!shouldWarnMissingPluginCache(cacheExists))
2914
+ return false;
2915
+ log("[agentbridge] \u26A0\uFE0F Plugin not installed (no plugin cache found). Run `abg init`, " + `or in Claude Code: ${MARKETPLACE_STEPS.join(" \u2192 ")}. Launching anyway\u2026`);
2916
+ return true;
2917
+ }
2671
2918
  function traceCliStart(event, args, originalEnv, envGuardAction, pair) {
2672
2919
  try {
2673
2920
  appendTraceEvent({
@@ -2744,6 +2991,7 @@ function checkOwnedFlagConflicts(args, commandName, ownedFlags) {
2744
2991
  var OWNED_FLAGS;
2745
2992
  var init_claude = __esm(() => {
2746
2993
  init_cli();
2994
+ init_plugin_cache();
2747
2995
  init_daemon_client();
2748
2996
  init_daemon_lifecycle();
2749
2997
  init_build_info();
@@ -2755,11 +3003,11 @@ var init_claude = __esm(() => {
2755
3003
  });
2756
3004
 
2757
3005
  // src/agents-contract.ts
2758
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
2759
- import { join as join8 } from "path";
3006
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
3007
+ import { join as join9 } from "path";
2760
3008
  function checkAgentsMdContract(cwd) {
2761
- const path = join8(cwd, "AGENTS.md");
2762
- const exists = existsSync7(path);
3009
+ const path = join9(cwd, "AGENTS.md");
3010
+ const exists = existsSync10(path);
2763
3011
  let content = "";
2764
3012
  if (exists) {
2765
3013
  try {
@@ -2790,8 +3038,8 @@ function isFreshAgentsMdContract(content) {
2790
3038
  var init_agents_contract = () => {};
2791
3039
 
2792
3040
  // src/wrapper-exit-observability.ts
2793
- import { readFileSync as readFileSync7, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
2794
- import { join as join9 } from "path";
3041
+ import { readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
3042
+ import { join as join10 } from "path";
2795
3043
  function discoverNativeChildPid(launcherPid, run) {
2796
3044
  try {
2797
3045
  const out = run("pgrep", ["-P", String(launcherPid)]);
@@ -2820,12 +3068,12 @@ function refineCleanExitClassification(turnInProgress) {
2820
3068
  return "exit_0_idle";
2821
3069
  return "exit_0_turn_unknown";
2822
3070
  }
2823
- function findCodexSqliteLog(codexHome, fs = { readdir: readdirSync2, stat: statSync3 }) {
3071
+ function findCodexSqliteLog(codexHome, fs = { readdir: readdirSync3, stat: statSync4 }) {
2824
3072
  try {
2825
3073
  const entries = fs.readdir(codexHome).filter((name) => /^logs.*\.sqlite$/.test(String(name)));
2826
3074
  let best = null;
2827
3075
  for (const name of entries) {
2828
- const path = join9(codexHome, String(name));
3076
+ const path = join10(codexHome, String(name));
2829
3077
  try {
2830
3078
  const mtime = fs.stat(path).mtimeMs;
2831
3079
  if (!best || mtime > best.mtime)
@@ -2885,15 +3133,15 @@ var init_pair_command = __esm(() => {
2885
3133
  });
2886
3134
 
2887
3135
  // src/rotating-log.ts
2888
- import { appendFileSync as appendFileSync2, existsSync as existsSync8, renameSync as renameSync2, statSync as statSync4, unlinkSync as unlinkSync3 } from "fs";
3136
+ import { appendFileSync as appendFileSync2, existsSync as existsSync11, renameSync as renameSync2, statSync as statSync5, unlinkSync as unlinkSync4 } from "fs";
2889
3137
  import { dirname as dirname2 } from "path";
2890
- function appendRotatingLog(path, content, options = {}) {
3138
+ function appendRotatingLog(path, content, options = {}, fsOps = REAL_FS_OPS) {
2891
3139
  const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
2892
3140
  const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
2893
- if (!existsSync8(dirname2(path)))
3141
+ if (!fsOps.existsSync(dirname2(path)))
2894
3142
  return;
2895
- rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep);
2896
- appendFileSync2(path, content, "utf-8");
3143
+ rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep, fsOps);
3144
+ fsOps.appendFileSync(path, content, "utf-8");
2897
3145
  }
2898
3146
  function positiveIntFromEnv(name, fallback) {
2899
3147
  const value = process.env[name];
@@ -2902,30 +3150,53 @@ function positiveIntFromEnv(name, fallback) {
2902
3150
  const parsed = Number(value);
2903
3151
  return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
2904
3152
  }
2905
- function rotateIfNeeded(path, incomingBytes, maxBytes, keep) {
3153
+ function isEnoent(error) {
3154
+ return !!error && error.code === "ENOENT";
3155
+ }
3156
+ function renameIfPresent(from, to, fsOps) {
3157
+ try {
3158
+ fsOps.renameSync(from, to);
3159
+ } catch (error) {
3160
+ if (!isEnoent(error))
3161
+ throw error;
3162
+ }
3163
+ }
3164
+ function unlinkIfPresent(path, fsOps) {
3165
+ try {
3166
+ fsOps.unlinkSync(path);
3167
+ } catch (error) {
3168
+ if (!isEnoent(error))
3169
+ throw error;
3170
+ }
3171
+ }
3172
+ function rotateIfNeeded(path, incomingBytes, maxBytes, keep, fsOps) {
2906
3173
  if (!Number.isFinite(maxBytes) || maxBytes <= 0 || keep <= 0)
2907
3174
  return;
2908
- if (!existsSync8(path))
2909
- return;
2910
- const size = statSync4(path).size;
3175
+ let size;
3176
+ try {
3177
+ size = fsOps.statSync(path).size;
3178
+ } catch (error) {
3179
+ if (isEnoent(error))
3180
+ return;
3181
+ throw error;
3182
+ }
2911
3183
  if (size + incomingBytes <= maxBytes)
2912
3184
  return;
2913
3185
  for (let index = keep;index >= 1; index--) {
2914
3186
  const current = `${path}.${index}`;
2915
3187
  const next = `${path}.${index + 1}`;
2916
- if (!existsSync8(current))
2917
- continue;
2918
3188
  if (index === keep) {
2919
- unlinkSync3(current);
3189
+ unlinkIfPresent(current, fsOps);
2920
3190
  } else {
2921
- renameSync2(current, next);
3191
+ renameIfPresent(current, next, fsOps);
2922
3192
  }
2923
3193
  }
2924
- renameSync2(path, `${path}.1`);
3194
+ renameIfPresent(path, `${path}.1`, fsOps);
2925
3195
  }
2926
- var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3;
3196
+ var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3, REAL_FS_OPS;
2927
3197
  var init_rotating_log = __esm(() => {
2928
3198
  DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
3199
+ REAL_FS_OPS = { statSync: statSync5, renameSync: renameSync2, unlinkSync: unlinkSync4, appendFileSync: appendFileSync2, existsSync: existsSync11 };
2929
3200
  });
2930
3201
 
2931
3202
  // src/stderr-ring-buffer.ts
@@ -2978,20 +3249,20 @@ var init_stderr_ring_buffer = __esm(() => {
2978
3249
 
2979
3250
  // src/thread-state.ts
2980
3251
  import {
2981
- existsSync as existsSync9,
3252
+ existsSync as existsSync12,
2982
3253
  mkdirSync as mkdirSync5,
2983
- readdirSync as readdirSync3,
3254
+ readdirSync as readdirSync4,
2984
3255
  readFileSync as readFileSync8,
2985
3256
  renameSync as renameSync3,
2986
3257
  writeFileSync as writeFileSync6
2987
3258
  } from "fs";
2988
3259
  import { homedir as homedir3 } from "os";
2989
- import { basename as basename3, dirname as dirname3, join as join10 } from "path";
3260
+ import { basename as basename3, dirname as dirname3, join as join11 } from "path";
2990
3261
  function nowIso() {
2991
3262
  return new Date().toISOString();
2992
3263
  }
2993
3264
  function codexHome(env = process.env) {
2994
- return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join10(homedir3(), ".codex");
3265
+ return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join11(homedir3(), ".codex");
2995
3266
  }
2996
3267
  function atomicWriteJson(path, value) {
2997
3268
  mkdirSync5(dirname3(path), { recursive: true });
@@ -3010,8 +3281,8 @@ function readRawCurrentThread(stateDir) {
3010
3281
  return null;
3011
3282
  }
3012
3283
  function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
3013
- const sessionsDir = join10(codexHome(env), "sessions");
3014
- if (!threadId || !existsSync9(sessionsDir))
3284
+ const sessionsDir = join11(codexHome(env), "sessions");
3285
+ if (!threadId || !existsSync12(sessionsDir))
3015
3286
  return null;
3016
3287
  const exactName = `rollout-${threadId}.jsonl`;
3017
3288
  const stack = [sessionsDir];
@@ -3020,13 +3291,13 @@ function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
3020
3291
  const dir = stack.pop();
3021
3292
  let entries;
3022
3293
  try {
3023
- entries = readdirSync3(dir, { withFileTypes: true });
3294
+ entries = readdirSync4(dir, { withFileTypes: true });
3024
3295
  } catch {
3025
3296
  continue;
3026
3297
  }
3027
3298
  for (const entry of entries) {
3028
3299
  visited++;
3029
- const path = join10(dir, entry.name);
3300
+ const path = join11(dir, entry.name);
3030
3301
  if (entry.isDirectory()) {
3031
3302
  stack.push(path);
3032
3303
  continue;
@@ -3051,7 +3322,7 @@ function readUsableCurrentThread(identity, env = process.env) {
3051
3322
  return null;
3052
3323
  if (state.cwd !== identity.cwd)
3053
3324
  return null;
3054
- if (state.rolloutPath && existsSync9(state.rolloutPath))
3325
+ if (state.rolloutPath && existsSync12(state.rolloutPath))
3055
3326
  return state;
3056
3327
  const rolloutPath = findCodexRolloutFile(state.threadId, env);
3057
3328
  if (!rolloutPath)
@@ -3082,16 +3353,16 @@ import {
3082
3353
  closeSync as closeSync3,
3083
3354
  writeFileSync as writeFileSync7,
3084
3355
  readFileSync as readFileSync9,
3085
- unlinkSync as unlinkSync4,
3086
- existsSync as existsSync10,
3356
+ unlinkSync as unlinkSync5,
3357
+ existsSync as existsSync13,
3087
3358
  mkdirSync as mkdirSync6
3088
3359
  } from "fs";
3089
3360
  import { homedir as homedir4 } from "os";
3090
- import { dirname as dirname4, join as join11 } from "path";
3361
+ import { dirname as dirname4, join as join12 } from "path";
3091
3362
  function appendWrapperLog(path, entry) {
3092
3363
  try {
3093
3364
  const dir = dirname4(path);
3094
- if (!existsSync10(dir)) {
3365
+ if (!existsSync13(dir)) {
3095
3366
  mkdirSync6(dir, { recursive: true });
3096
3367
  }
3097
3368
  appendRotatingLog(path, `[${new Date().toISOString()}] ${entry}
@@ -3258,7 +3529,7 @@ async function runCodex(args) {
3258
3529
  if (status?.proxyUrl) {
3259
3530
  proxyUrl = status.proxyUrl;
3260
3531
  } else {
3261
- const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault().codex.proxyPort);
3532
+ const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault((msg) => console.error(`[agentbridge] ${msg}`)).codex.proxyPort);
3262
3533
  proxyUrl = `ws://127.0.0.1:${fallbackProxyPort}`;
3263
3534
  console.error(`[agentbridge] No daemon status found, using fallback proxy port: ${proxyUrl}`);
3264
3535
  }
@@ -3378,7 +3649,7 @@ async function runCodex(args) {
3378
3649
  return;
3379
3650
  cleanedTuiPid = true;
3380
3651
  try {
3381
- unlinkSync4(stateDir.tuiPidFile);
3652
+ unlinkSync5(stateDir.tuiPidFile);
3382
3653
  } catch {}
3383
3654
  }
3384
3655
  function requestChildTermination(reason) {
@@ -3452,7 +3723,7 @@ async function runCodex(args) {
3452
3723
  classification = refineCleanExitClassification(readTurnInProgress(stateDir.statusFile));
3453
3724
  }
3454
3725
  const tuiLogTail = captureTuiLogTail({
3455
- codexHome: join11(homedir4(), ".codex"),
3726
+ codexHome: join12(homedir4(), ".codex"),
3456
3727
  nativePid: nativeChildPid,
3457
3728
  run: (cmd, args2) => execFileSync6(cmd, args2, { encoding: "utf-8", timeout: 2000 })
3458
3729
  });
@@ -3508,12 +3779,12 @@ function guardNoLiveManagedTui(stateDir, proxyUrl) {
3508
3779
  if (pid) {
3509
3780
  if (!isProcessAlive(pid)) {
3510
3781
  try {
3511
- unlinkSync4(stateDir.tuiPidFile);
3782
+ unlinkSync5(stateDir.tuiPidFile);
3512
3783
  } catch {}
3513
3784
  } else if (!isManagedCodexTuiProcess(pid, proxyUrl)) {
3514
3785
  appendWrapperLog(stateDir.codexWrapperLogFile, `stale tui pid file pointed at unmanaged live pid=${pid}; removing`);
3515
3786
  try {
3516
- unlinkSync4(stateDir.tuiPidFile);
3787
+ unlinkSync5(stateDir.tuiPidFile);
3517
3788
  } catch {}
3518
3789
  } else {
3519
3790
  console.error(`[agentbridge] This pair already has a managed Codex TUI running (pid ${pid}).`);
@@ -3560,7 +3831,7 @@ async function waitForProxyReady(proxyUrl, maxRetries = 20, delayMs = 100) {
3560
3831
  return;
3561
3832
  }
3562
3833
  } catch {}
3563
- await new Promise((resolve4) => setTimeout(resolve4, delayMs));
3834
+ await new Promise((resolve5) => setTimeout(resolve5, delayMs));
3564
3835
  }
3565
3836
  throw new Error(`Timed out waiting for Codex proxy readiness on ${healthUrl}`);
3566
3837
  }
@@ -3609,17 +3880,17 @@ var init_codex = __esm(() => {
3609
3880
  });
3610
3881
 
3611
3882
  // src/claude-session.ts
3612
- import { readdirSync as readdirSync4, statSync as statSync5 } from "fs";
3883
+ import { readdirSync as readdirSync5, statSync as statSync6 } from "fs";
3613
3884
  import { homedir as homedir5 } from "os";
3614
- import { join as join12 } from "path";
3885
+ import { join as join13 } from "path";
3615
3886
  function encodeClaudeProjectDir(cwd) {
3616
3887
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
3617
3888
  }
3618
- function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR || join12(homedir5(), ".claude")) {
3619
- const dir = join12(claudeHome, "projects", encodeClaudeProjectDir(cwd));
3889
+ function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR || join13(homedir5(), ".claude")) {
3890
+ const dir = join13(claudeHome, "projects", encodeClaudeProjectDir(cwd));
3620
3891
  let entries;
3621
3892
  try {
3622
- entries = readdirSync4(dir);
3893
+ entries = readdirSync5(dir);
3623
3894
  } catch {
3624
3895
  return null;
3625
3896
  }
@@ -3630,10 +3901,10 @@ function findLatestClaudeSession(cwd, claudeHome = process.env.CLAUDE_CONFIG_DIR
3630
3901
  const sessionId = name.slice(0, -".jsonl".length);
3631
3902
  if (!SESSION_ID_PATTERN.test(sessionId))
3632
3903
  continue;
3633
- const file = join12(dir, name);
3904
+ const file = join13(dir, name);
3634
3905
  let mtimeMs;
3635
3906
  try {
3636
- const st = statSync5(file);
3907
+ const st = statSync6(file);
3637
3908
  if (!st.isFile())
3638
3909
  continue;
3639
3910
  mtimeMs = st.mtimeMs;
@@ -3739,8 +4010,8 @@ __export(exports_kill, {
3739
4010
  runKill: () => runKill,
3740
4011
  formatKillReport: () => formatKillReport
3741
4012
  });
3742
- import { readFileSync as readFileSync10, unlinkSync as unlinkSync5 } from "fs";
3743
- import { join as join13 } from "path";
4013
+ import { readFileSync as readFileSync10, unlinkSync as unlinkSync6 } from "fs";
4014
+ import { join as join14 } from "path";
3744
4015
  async function runKill(args = []) {
3745
4016
  const argError = validateKillArgs(args);
3746
4017
  if (argError === "help") {
@@ -3792,7 +4063,7 @@ async function runKill(args = []) {
3792
4063
  for (const dirName of listPairDirsSafe(base)) {
3793
4064
  if (registeredIds.has(dirName))
3794
4065
  continue;
3795
- const stateDir = new StateDirResolver(join13(base, "pairs", dirName));
4066
+ const stateDir = new StateDirResolver(join14(base, "pairs", dirName));
3796
4067
  results.push(await stopStateDir(`${dirName} (unregistered)`, stateDir, portsFromStateDir(stateDir)));
3797
4068
  }
3798
4069
  const legacy = detectLegacyRootDaemon(base);
@@ -3877,7 +4148,7 @@ No arguments stop this directory's registered pairs and any legacy-root daemon.
3877
4148
  }
3878
4149
  async function stopPairEntry(base, pair) {
3879
4150
  const ports = portsForEntry(pair);
3880
- const stateDir = new StateDirResolver(join13(base, "pairs", pair.pairId));
4151
+ const stateDir = new StateDirResolver(join14(base, "pairs", pair.pairId));
3881
4152
  return stopStateDir(pair.pairId, stateDir, ports);
3882
4153
  }
3883
4154
  function listPairDirsSafe(base) {
@@ -4046,7 +4317,7 @@ function readTuiPid2(stateDir) {
4046
4317
  }
4047
4318
  function removeTuiPidFile(stateDir) {
4048
4319
  try {
4049
- unlinkSync5(stateDir.tuiPidFile);
4320
+ unlinkSync6(stateDir.tuiPidFile);
4050
4321
  } catch {}
4051
4322
  }
4052
4323
  function isManagedCodexTuiProcess2(pid, proxyUrl) {
@@ -4066,7 +4337,7 @@ var exports_pairs = {};
4066
4337
  __export(exports_pairs, {
4067
4338
  runPairs: () => runPairs
4068
4339
  });
4069
- import { join as join14 } from "path";
4340
+ import { join as join15 } from "path";
4070
4341
  async function runPairs(args = []) {
4071
4342
  const [command, ...rest] = args;
4072
4343
  if (command === "rm") {
@@ -4232,7 +4503,7 @@ async function collectRows() {
4232
4503
  }
4233
4504
  async function rowForPair(base, pair) {
4234
4505
  const ports = portsForEntry(pair);
4235
- const stateDir = new StateDirResolver(join14(base, "pairs", pair.pairId));
4506
+ const stateDir = new StateDirResolver(join15(base, "pairs", pair.pairId));
4236
4507
  const lifecycle = new DaemonLifecycle({
4237
4508
  stateDir,
4238
4509
  controlPort: ports.controlPort,
@@ -4339,18 +4610,18 @@ var DAEMON_STATUS_FETCH_TIMEOUT_MS = 1000;
4339
4610
  import { Database } from "bun:sqlite";
4340
4611
  import {
4341
4612
  copyFileSync,
4342
- existsSync as existsSync11,
4613
+ existsSync as existsSync14,
4343
4614
  mkdirSync as mkdirSync7,
4344
4615
  readFileSync as readFileSync11
4345
4616
  } from "fs";
4346
- import { dirname as dirname5, join as join15 } from "path";
4617
+ import { dirname as dirname5, join as join16 } from "path";
4347
4618
  function isKickoffText(text) {
4348
4619
  if (!text)
4349
4620
  return false;
4350
4621
  return KICKOFF_FINGERPRINTS.some((fingerprint) => text.includes(fingerprint));
4351
4622
  }
4352
4623
  function extractFirstRealUserMessage(rolloutPath) {
4353
- if (!existsSync11(rolloutPath))
4624
+ if (!existsSync14(rolloutPath))
4354
4625
  return null;
4355
4626
  const raw = readFileSync11(rolloutPath, "utf-8");
4356
4627
  for (const line of raw.split(`
@@ -4374,8 +4645,8 @@ function extractFirstRealUserMessage(rolloutPath) {
4374
4645
  }
4375
4646
  function scanResumePollution(options = {}) {
4376
4647
  const codexHome2 = options.codexHome ?? codexHome();
4377
- const dbPath = options.dbPath ?? join15(codexHome2, "state_5.sqlite");
4378
- if (!existsSync11(dbPath)) {
4648
+ const dbPath = options.dbPath ?? join16(codexHome2, "state_5.sqlite");
4649
+ if (!existsSync14(dbPath)) {
4379
4650
  return { codexHome: codexHome2, dbPath, scanned: 0, candidates: [], applied: 0, renamed: 0, deleted: 0 };
4380
4651
  }
4381
4652
  const db = options.apply ? new Database(dbPath) : new Database(dbPath, { readonly: true });
@@ -4468,12 +4739,12 @@ function scanResumePollution(options = {}) {
4468
4739
  }
4469
4740
  function backupCodexStateFiles(dbPath, now = new Date().toISOString()) {
4470
4741
  const safeStamp = now.replace(/[:.]/g, "-");
4471
- const base = join15(dirname5(dbPath), "agentbridge-backups", `resume-pollution-${safeStamp}`);
4742
+ const base = join16(dirname5(dbPath), "agentbridge-backups", `resume-pollution-${safeStamp}`);
4472
4743
  mkdirSync7(base, { recursive: true });
4473
4744
  for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
4474
- if (!existsSync11(path))
4745
+ if (!existsSync14(path))
4475
4746
  continue;
4476
- const target = join15(base, path.split("/").pop());
4747
+ const target = join16(base, path.split("/").pop());
4477
4748
  mkdirSync7(dirname5(target), { recursive: true });
4478
4749
  copyFileSync(path, target);
4479
4750
  }
@@ -4521,9 +4792,8 @@ __export(exports_doctor, {
4521
4792
  runDoctor: () => runDoctor,
4522
4793
  formatDoctorReport: () => formatDoctorReport
4523
4794
  });
4524
- import { existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync5, realpathSync as realpathSync3, statSync as statSync6 } from "fs";
4525
- import { homedir as homedir6 } from "os";
4526
- import { join as join16 } from "path";
4795
+ import { existsSync as existsSync15, readFileSync as readFileSync12, readdirSync as readdirSync6, realpathSync as realpathSync3, statSync as statSync7 } from "fs";
4796
+ import { join as join17 } from "path";
4527
4797
  async function runDoctor(args = []) {
4528
4798
  if (args[0] === "resume-pollution") {
4529
4799
  runResumePollution(args.slice(1));
@@ -4626,6 +4896,7 @@ async function buildDoctorReport(pair, registered) {
4626
4896
  detail: env.ok ? "AgentBridge env matches cwd" : env.reasons.join("; "),
4627
4897
  hint: env.ok ? undefined : "\u73AF\u5883\u53D8\u91CF\u4E0E\u5F53\u524D\u76EE\u5F55\u4E0D\u5339\u914D\uFF1A\u8BF7\u5728\u6B63\u786E\u7684\u9879\u76EE\u76EE\u5F55\u91CC\u91CD\u65B0\u8FD0\u884C `agentbridge claude`\uFF0C\u4E0D\u8981\u590D\u7528\u5176\u4ED6\u76EE\u5F55\u7684\u4F1A\u8BDD\u73AF\u5883\u3002"
4628
4898
  });
4899
+ checks.push(configParseabilityCheck(cwd));
4629
4900
  checks.push({
4630
4901
  name: "daemon health",
4631
4902
  status: health ? "ok" : "warn",
@@ -4712,16 +4983,16 @@ function artifactAlignmentCheck() {
4712
4983
  stamps.push({ label: "global-cli", commit });
4713
4984
  } catch {}
4714
4985
  }
4715
- const cacheRoot = join16(homedir6(), ".claude", "plugins", "cache", "agentbridge", "agentbridge");
4986
+ const cacheRoot = pluginCacheRoot();
4716
4987
  try {
4717
- for (const version of readdirSync5(cacheRoot)) {
4718
- const commit = extractBundleCommit(join16(cacheRoot, version, "server", "daemon.js"));
4988
+ for (const version of readdirSync6(cacheRoot)) {
4989
+ const commit = extractBundleCommit(join17(cacheRoot, version, "server", "daemon.js"));
4719
4990
  if (commit)
4720
4991
  stamps.push({ label: `plugin-cache@${version}`, commit });
4721
4992
  }
4722
4993
  } catch {}
4723
- const repoBundle = join16(process.cwd(), "plugins", "agentbridge", "server", "daemon.js");
4724
- if (existsSync12(repoBundle)) {
4994
+ const repoBundle = join17(process.cwd(), "plugins", "agentbridge", "server", "daemon.js");
4995
+ if (existsSync15(repoBundle)) {
4725
4996
  const commit = extractBundleCommit(repoBundle);
4726
4997
  if (commit)
4727
4998
  stamps.push({ label: "repo-bundle", commit });
@@ -4753,8 +5024,31 @@ function extractBundleCommit(path) {
4753
5024
  return null;
4754
5025
  }
4755
5026
  }
5027
+ function configParseabilityCheck(cwd) {
5028
+ const desc = new ConfigService(cwd).describeConfig();
5029
+ if (desc.state === "absent") {
5030
+ return {
5031
+ name: "config.json",
5032
+ status: "ok",
5033
+ detail: `no project config at ${desc.path} \u2014 built-in defaults in effect`
5034
+ };
5035
+ }
5036
+ if (desc.state === "corrupt") {
5037
+ return {
5038
+ name: "config.json",
5039
+ status: "warn",
5040
+ detail: `unparseable at ${desc.path} (${desc.reason}) \u2014 custom thresholds NOT in effect, using defaults`,
5041
+ hint: "config.json \u635F\u574F\u6216\u5B57\u6BB5\u7C7B\u578B\u9519\u8BEF\uFF1Abridge \u5DF2\u56DE\u9000\u5230\u9ED8\u8BA4\u9608\u503C\uFF0C\u4F60\u7684\u81EA\u5B9A\u4E49 budget/idle \u8BBE\u7F6E\u672A\u751F\u6548\u3002" + "\u4FEE\u6B63\u8BE5\u6587\u4EF6\u7684 JSON \u8BED\u6CD5/\u5B57\u6BB5\u7C7B\u578B\u540E\u91CD\u542F `agentbridge claude` \u5373\u53EF\u91CD\u65B0\u751F\u6548\u3002"
5042
+ };
5043
+ }
5044
+ return {
5045
+ name: "config.json",
5046
+ status: "ok",
5047
+ detail: desc.customValues ? `parsed at ${desc.path} \u2014 custom values in effect` : `parsed at ${desc.path} \u2014 all values match defaults`
5048
+ };
5049
+ }
4756
5050
  function logCheck(name, path) {
4757
- if (!existsSync12(path)) {
5051
+ if (!existsSync15(path)) {
4758
5052
  return {
4759
5053
  name,
4760
5054
  status: "warn",
@@ -4762,7 +5056,7 @@ function logCheck(name, path) {
4762
5056
  hint: "\u65E5\u5FD7\u4F1A\u5728\u76F8\u5E94\u8FDB\u7A0B\u9996\u6B21\u542F\u52A8\u65F6\u521B\u5EFA\uFF1B\u8FDB\u7A0B\u4ECE\u672A\u542F\u52A8\u8FC7\u65F6\u8FD9\u662F\u6B63\u5E38\u7684\u3002"
4763
5057
  };
4764
5058
  }
4765
- const stat = statSync6(path);
5059
+ const stat = statSync7(path);
4766
5060
  if (stat.size > LARGE_LOG_WARN_BYTES) {
4767
5061
  return {
4768
5062
  name,
@@ -4804,7 +5098,9 @@ function printDoctorReport(report) {
4804
5098
  }
4805
5099
  var LARGE_LOG_WARN_BYTES;
4806
5100
  var init_doctor = __esm(() => {
5101
+ init_plugin_cache();
4807
5102
  init_build_info();
5103
+ init_config_service();
4808
5104
  init_env_guard();
4809
5105
  init_pair_resolver();
4810
5106
  init_thread_state();
@@ -4836,6 +5132,9 @@ function formatAgent(name, usage, snapshotAt) {
4836
5132
  if (usage.rateLimitedUntil > 0) {
4837
5133
  parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
4838
5134
  }
5135
+ if (usage.parsedVia === "positional") {
5136
+ parts.push("\u26A0\uFE0F \u7A97\u53E3\u8BC6\u522B\u4F7F\u7528\u4F4D\u7F6E\u515C\u5E95");
5137
+ }
4839
5138
  const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
4840
5139
  if (ageSec > 300) {
4841
5140
  parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
@@ -4947,6 +5246,160 @@ var init_budget = __esm(() => {
4947
5246
  init_render();
4948
5247
  });
4949
5248
 
5249
+ // src/cli/logs.ts
5250
+ var exports_logs = {};
5251
+ __export(exports_logs, {
5252
+ tailLines: () => tailLines,
5253
+ runLogs: () => runLogs,
5254
+ parseLogsArgs: () => parseLogsArgs,
5255
+ followLog: () => followLog
5256
+ });
5257
+ import { existsSync as existsSync16, readFileSync as readFileSync13 } from "fs";
5258
+ import { spawn as spawn4 } from "child_process";
5259
+ function parseLogsArgs(args) {
5260
+ let codex = false;
5261
+ let follow = false;
5262
+ let lines = DEFAULT_LINES;
5263
+ for (let i = 0;i < args.length; i++) {
5264
+ const a = args[i];
5265
+ if (a === "--codex") {
5266
+ codex = true;
5267
+ continue;
5268
+ }
5269
+ if (a === "-f" || a === "--follow") {
5270
+ follow = true;
5271
+ continue;
5272
+ }
5273
+ if (a === "-n" || a === "--lines") {
5274
+ const next = args[i + 1];
5275
+ if (next === undefined) {
5276
+ throw new Error(`${a} requires a positive integer (e.g. ${a} 200)`);
5277
+ }
5278
+ lines = parsePositiveInt(next, a);
5279
+ i++;
5280
+ continue;
5281
+ }
5282
+ if (a.startsWith("-n")) {
5283
+ lines = parsePositiveInt(a.slice(2), "-n");
5284
+ continue;
5285
+ }
5286
+ if (a.startsWith("--lines=")) {
5287
+ lines = parsePositiveInt(a.slice("--lines=".length), "--lines");
5288
+ continue;
5289
+ }
5290
+ throw new Error(`Unknown logs flag: ${a}`);
5291
+ }
5292
+ return { codex, follow, lines };
5293
+ }
5294
+ function parsePositiveInt(raw, flag) {
5295
+ if (!/^\d+$/.test(raw)) {
5296
+ throw new Error(`${flag} must be a positive integer, got "${raw}"`);
5297
+ }
5298
+ const n = Number.parseInt(raw, 10);
5299
+ if (!Number.isInteger(n) || n <= 0) {
5300
+ throw new Error(`${flag} must be a positive integer, got "${raw}"`);
5301
+ }
5302
+ return n;
5303
+ }
5304
+ function tailLines(text, count) {
5305
+ const body = text.endsWith(`
5306
+ `) ? text.slice(0, -1) : text;
5307
+ if (body.length === 0)
5308
+ return [];
5309
+ const all = body.split(`
5310
+ `);
5311
+ return all.length <= count ? all : all.slice(all.length - count);
5312
+ }
5313
+ async function runLogs(args) {
5314
+ const { pairFlag } = parsePairFlag(args);
5315
+ const rest = stripPairTokens(args);
5316
+ let options;
5317
+ try {
5318
+ options = parseLogsArgs(rest);
5319
+ } catch (err) {
5320
+ console.error(`[agentbridge] ${err instanceof Error ? err.message : String(err)}`);
5321
+ process.exit(1);
5322
+ return;
5323
+ }
5324
+ let resolution;
5325
+ try {
5326
+ resolution = resolvePairReadOnly(pairFlag);
5327
+ } catch (err) {
5328
+ console.error(`[agentbridge] ${err instanceof Error ? err.message : String(err)}`);
5329
+ process.exit(1);
5330
+ return;
5331
+ }
5332
+ const { pair } = resolution;
5333
+ const logPath = options.codex ? pair.stateDir.codexWrapperLogFile : pair.stateDir.logFile;
5334
+ const logLabel = options.codex ? "codex wrapper log" : "daemon log";
5335
+ if (!existsSync16(logPath)) {
5336
+ const which = options.codex ? "codex wrapper log" : "daemon log";
5337
+ console.error(`no ${which} for pair ${pair.name} yet \u2014 start it with \`abg claude\` (${logPath})`);
5338
+ process.exit(1);
5339
+ return;
5340
+ }
5341
+ if (options.follow) {
5342
+ await followLog(logPath, options.lines);
5343
+ return;
5344
+ }
5345
+ printTail(logPath, options.lines, logLabel, pair.name);
5346
+ }
5347
+ function printTail(logPath, count, label, pairName) {
5348
+ let text;
5349
+ try {
5350
+ text = readFileSync13(logPath, "utf8");
5351
+ } catch (err) {
5352
+ console.error(`[agentbridge] failed to read ${label} for pair ${pairName}: ` + `${err instanceof Error ? err.message : String(err)} (${logPath})`);
5353
+ process.exit(1);
5354
+ return;
5355
+ }
5356
+ const lines = tailLines(text, count);
5357
+ for (const line of lines)
5358
+ console.log(line);
5359
+ }
5360
+ function followLog(logPath, count) {
5361
+ return new Promise((resolvePromise) => {
5362
+ const child = spawn4("tail", ["-f", "-n", String(count), logPath], {
5363
+ stdio: "inherit"
5364
+ });
5365
+ child.on("error", (err) => {
5366
+ console.error(`[agentbridge] failed to follow log: ${err.message}`);
5367
+ process.exit(1);
5368
+ });
5369
+ child.on("exit", (code, signal) => {
5370
+ if (signal === "SIGINT" || signal === "SIGTERM") {
5371
+ resolvePromise();
5372
+ return;
5373
+ }
5374
+ if (code != null && code !== 0) {
5375
+ process.exit(code);
5376
+ return;
5377
+ }
5378
+ resolvePromise();
5379
+ });
5380
+ });
5381
+ }
5382
+ function stripPairTokens(args) {
5383
+ const out = [];
5384
+ for (let i = 0;i < args.length; i++) {
5385
+ const a = args[i];
5386
+ if (a === "--pair") {
5387
+ const next = args[i + 1];
5388
+ if (next !== undefined && !next.startsWith("-"))
5389
+ i++;
5390
+ continue;
5391
+ }
5392
+ if (a.startsWith("--pair="))
5393
+ continue;
5394
+ out.push(a);
5395
+ }
5396
+ return out;
5397
+ }
5398
+ var DEFAULT_LINES = 100;
5399
+ var init_logs = __esm(() => {
5400
+ init_pair_resolver();
5401
+ });
5402
+
4950
5403
  // src/cli.ts
4951
5404
  function parseTopLevel(args) {
4952
5405
  const pairTokens = [];
@@ -5019,6 +5472,10 @@ async function main(command, restArgs) {
5019
5472
  const { runBudget: runBudget2 } = await Promise.resolve().then(() => (init_budget(), exports_budget));
5020
5473
  await runBudget2(restArgs);
5021
5474
  break;
5475
+ case "logs":
5476
+ const { runLogs: runLogs2 } = await Promise.resolve().then(() => (init_logs(), exports_logs));
5477
+ await runLogs2(restArgs);
5478
+ break;
5022
5479
  case "--help":
5023
5480
  case "-h":
5024
5481
  case undefined:
@@ -5057,6 +5514,9 @@ Commands:
5057
5514
  doctor [--json] Diagnose env, daemon, build drift, logs, and current thread
5058
5515
  doctor resume-pollution [--apply] Find/fix old AgentBridge kickoff metadata
5059
5516
  budget [--json] Show both agents' subscription quota snapshot (5h/weekly, drift, pause state)
5517
+ logs [--codex] [-f] [-n N]
5518
+ Tail this pair's daemon log (or the codex wrapper log with
5519
+ --codex). -n N: last N lines (default 100). -f: follow/stream.
5060
5520
  kill [all | --all | --pair <name|id>]
5061
5521
  Stop this directory's pairs (default), every pair (all/--all), or one (--pair)
5062
5522
 
@@ -5095,6 +5555,10 @@ Examples:
5095
5555
  abg pairs # List all pairs and their ports/status
5096
5556
  abg pairs --threads # Include current thread mapping
5097
5557
  abg doctor --json # Emit a structured diagnostics report
5558
+ abg logs # Tail the last 100 lines of this pair's daemon log
5559
+ abg logs -f -n 200 # Follow the log, starting from the last 200 lines
5560
+ abg logs --codex # Tail the codex wrapper log instead
5561
+ abg --pair work logs # Tail the "work" pair's daemon log
5098
5562
  abg pairs rm work # Stop this directory's "work" pair and free its slot
5099
5563
  abg pairs rm work-1a2b3c4d # ...or by its full id (from that pair's directory)
5100
5564
  abg pairs prune --dry-run # Preview orphan pair dirs (no registry entry, not live)
@@ -5116,7 +5580,7 @@ var MARKETPLACE_NAME = "agentbridge", PLUGIN_NAME = "agentbridge", REFRESH_COMMA
5116
5580
  var init_cli = __esm(() => {
5117
5581
  REFRESH_COMMANDS = new Set(["claude", "codex", "resume"]);
5118
5582
  NOTIFY_COMMANDS = new Set(["claude", "codex", "init", "dev", "resume"]);
5119
- PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget", "resume"]);
5583
+ PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget", "resume", "logs"]);
5120
5584
  if (import.meta.main) {
5121
5585
  const { command, restArgs } = parseTopLevel(process.argv.slice(2));
5122
5586
  main(command, restArgs).catch((err) => {
@@ -5129,6 +5593,9 @@ init_cli();
5129
5593
 
5130
5594
  export {
5131
5595
  parseTopLevel,
5596
+ REFRESH_COMMANDS,
5132
5597
  PLUGIN_NAME,
5598
+ PAIR_AWARE_COMMANDS,
5599
+ NOTIFY_COMMANDS,
5133
5600
  MARKETPLACE_NAME
5134
5601
  };