@raysonmeng/agentbridge 0.1.6 → 0.1.8

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
@@ -17,9 +17,309 @@ var __export = (target, all) => {
17
17
  };
18
18
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
19
19
 
20
- // src/config-service.ts
21
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
20
+ // src/state-dir.ts
21
+ import { mkdirSync, existsSync } from "fs";
22
22
  import { join } from "path";
23
+ import { homedir, platform } from "os";
24
+
25
+ class StateDirResolver {
26
+ stateDir;
27
+ static platformBaseDir() {
28
+ if (platform() === "darwin") {
29
+ return join(homedir(), "Library", "Application Support", "AgentBridge");
30
+ }
31
+ const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
32
+ return join(xdgState, "agentbridge");
33
+ }
34
+ constructor(envOverride) {
35
+ const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
36
+ this.stateDir = override && override.length > 0 ? override : StateDirResolver.platformBaseDir();
37
+ }
38
+ ensure() {
39
+ if (!existsSync(this.stateDir)) {
40
+ mkdirSync(this.stateDir, { recursive: true });
41
+ }
42
+ }
43
+ get dir() {
44
+ return this.stateDir;
45
+ }
46
+ get pidFile() {
47
+ return join(this.stateDir, "daemon.pid");
48
+ }
49
+ get tuiPidFile() {
50
+ return join(this.stateDir, "codex-tui.pid");
51
+ }
52
+ get lockFile() {
53
+ return join(this.stateDir, "daemon.lock");
54
+ }
55
+ get statusFile() {
56
+ return join(this.stateDir, "status.json");
57
+ }
58
+ get portsFile() {
59
+ return join(this.stateDir, "ports.json");
60
+ }
61
+ get currentThreadFile() {
62
+ return join(this.stateDir, "current-thread.json");
63
+ }
64
+ get logFile() {
65
+ return join(this.stateDir, "agentbridge.log");
66
+ }
67
+ get codexWrapperLogFile() {
68
+ return join(this.stateDir, "codex-wrapper.log");
69
+ }
70
+ get killedFile() {
71
+ return join(this.stateDir, "killed");
72
+ }
73
+ get updateCheckFile() {
74
+ return join(this.stateDir, "update-check.json");
75
+ }
76
+ }
77
+ var init_state_dir = () => {};
78
+
79
+ // src/version-utils.ts
80
+ function isStableVersion(v) {
81
+ return STABLE_SEMVER_RE.test(v.trim());
82
+ }
83
+ function compareVersions(a, b) {
84
+ const pa = a.split(".").map(Number);
85
+ const pb = b.split(".").map(Number);
86
+ for (let i = 0;i < 3; i++) {
87
+ const va = pa[i] ?? 0;
88
+ const vb = pb[i] ?? 0;
89
+ if (va < vb)
90
+ return -1;
91
+ if (va > vb)
92
+ return 1;
93
+ }
94
+ return 0;
95
+ }
96
+ function isStableUpgrade(current, latest) {
97
+ if (!isStableVersion(current) || !isStableVersion(latest))
98
+ return false;
99
+ return compareVersions(latest, current) === 1;
100
+ }
101
+ var STABLE_SEMVER_RE;
102
+ var init_version_utils = __esm(() => {
103
+ STABLE_SEMVER_RE = /^\d+\.\d+\.\d+$/;
104
+ });
105
+
106
+ // src/env-utils.ts
107
+ function parsePositiveIntEnv(name, fallback, log = () => {}, env = process.env) {
108
+ const raw = env[name];
109
+ if (raw == null || raw === "")
110
+ return fallback;
111
+ const parsed = Number(raw);
112
+ if (!Number.isInteger(parsed) || parsed <= 0 || parsed > Number.MAX_SAFE_INTEGER) {
113
+ log(`Invalid ${name}=${JSON.stringify(raw)} (must be a positive integer within ` + `Number.MAX_SAFE_INTEGER); falling back to ${fallback}`);
114
+ return fallback;
115
+ }
116
+ return parsed;
117
+ }
118
+
119
+ // package.json
120
+ var require_package = __commonJS((exports, module) => {
121
+ module.exports = {
122
+ name: "@raysonmeng/agentbridge",
123
+ version: "0.1.8",
124
+ description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
125
+ type: "module",
126
+ packageManager: "bun@1.3.11",
127
+ engines: {
128
+ bun: ">=1.3.11"
129
+ },
130
+ bin: {
131
+ agentbridge: "dist/cli.js",
132
+ abg: "dist/cli.js"
133
+ },
134
+ files: [
135
+ "dist/",
136
+ "plugins/",
137
+ ".claude-plugin/",
138
+ "scripts/postinstall.cjs",
139
+ "scripts/install-safety.cjs",
140
+ "README.md",
141
+ "LICENSE"
142
+ ],
143
+ scripts: {
144
+ start: "bun run src/bridge.ts",
145
+ "build:cli": "node scripts/build-bundles.mjs cli daemon",
146
+ "build:plugin": "node scripts/build-bundles.mjs bridge-plugin daemon-plugin",
147
+ "smoke:built": "bun scripts/smoke-built-cli.mjs",
148
+ "smoke:pack": "bun scripts/smoke-pack.mjs",
149
+ "verify:plugin-sync": "node scripts/verify-plugin-sync.cjs",
150
+ postinstall: "node scripts/postinstall.cjs",
151
+ prepublishOnly: "bun run build:cli && bun run build:plugin && bun run verify:plugin-sync && bun scripts/check-plugin-versions.js",
152
+ "validate:plugin": "claude plugin validate plugins/agentbridge && claude plugin validate .claude-plugin/marketplace.json",
153
+ test: "bun test src",
154
+ "e2e:transport": "bun scripts/e2e-codex-transport.mjs",
155
+ "install:global": "node scripts/install-global.mjs local",
156
+ "install:global:local": "node scripts/install-global.mjs local",
157
+ "install:global:npm": "node scripts/install-global.mjs npm",
158
+ "release:bump": "node scripts/bump-version.mjs",
159
+ typecheck: "tsc --noEmit",
160
+ "validate:plugin-versions": "bun scripts/check-plugin-versions.js",
161
+ check: "tsc --noEmit && bun test src && bun run verify:plugin-sync && bun scripts/check-plugin-versions.js",
162
+ "ci:local": "bun run check && bun run smoke:built && bun run smoke:pack"
163
+ },
164
+ repository: {
165
+ type: "git",
166
+ url: "https://github.com/raysonmeng/agent-bridge.git"
167
+ },
168
+ homepage: "https://github.com/raysonmeng/agent-bridge#readme",
169
+ bugs: {
170
+ url: "https://github.com/raysonmeng/agent-bridge/issues"
171
+ },
172
+ keywords: [
173
+ "claude-code",
174
+ "codex",
175
+ "mcp",
176
+ "agent",
177
+ "bridge",
178
+ "multi-agent",
179
+ "channels"
180
+ ],
181
+ author: "AgentBridge Contributors",
182
+ license: "MIT",
183
+ devDependencies: {
184
+ "@modelcontextprotocol/sdk": "^1.27.1",
185
+ "@types/bun": "^1.3.11",
186
+ typescript: "^5.8.0"
187
+ }
188
+ };
189
+ });
190
+
191
+ // src/update-notifier.ts
192
+ var exports_update_notifier = {};
193
+ __export(exports_update_notifier, {
194
+ refreshUpdateCache: () => refreshUpdateCache,
195
+ parseLatestFromRegistry: () => parseLatestFromRegistry,
196
+ maybeNotifyUpdate: () => maybeNotifyUpdate,
197
+ isUpdateCheckSuppressed: () => isUpdateCheckSuppressed,
198
+ getCurrentVersion: () => getCurrentVersion,
199
+ buildUpdateNotice: () => buildUpdateNotice,
200
+ PACKAGE_NAME: () => PACKAGE_NAME
201
+ });
202
+ import { readFileSync, writeFileSync } from "fs";
203
+ function getCurrentVersion() {
204
+ try {
205
+ return require_package().version;
206
+ } catch {
207
+ return "0.0.0";
208
+ }
209
+ }
210
+ function isUpdateCheckSuppressed(env, isTTY) {
211
+ if (env.NO_UPDATE_NOTIFIER)
212
+ return true;
213
+ if (env.AGENTBRIDGE_NO_UPDATE_NOTIFIER)
214
+ return true;
215
+ if (env.CI)
216
+ return true;
217
+ if (env.NODE_ENV === "test")
218
+ return true;
219
+ if (!isTTY)
220
+ return true;
221
+ return false;
222
+ }
223
+ function readCache(stateDir) {
224
+ try {
225
+ const parsed = JSON.parse(readFileSync(stateDir.updateCheckFile, "utf-8"));
226
+ if (typeof parsed.lastCheckMs !== "number" || !Number.isFinite(parsed.lastCheckMs))
227
+ return null;
228
+ return {
229
+ lastCheckMs: parsed.lastCheckMs,
230
+ latest: typeof parsed.latest === "string" ? parsed.latest : null
231
+ };
232
+ } catch {
233
+ return null;
234
+ }
235
+ }
236
+ function writeCache(stateDir, cache) {
237
+ try {
238
+ stateDir.ensure();
239
+ writeFileSync(stateDir.updateCheckFile, JSON.stringify(cache, null, 2) + `
240
+ `, "utf-8");
241
+ } catch {}
242
+ }
243
+ function parseLatestFromRegistry(body) {
244
+ if (typeof body !== "object" || body === null)
245
+ return null;
246
+ const distTags = body["dist-tags"];
247
+ if (typeof distTags !== "object" || distTags === null)
248
+ return null;
249
+ const latest = distTags.latest;
250
+ if (typeof latest !== "string" || !isStableVersion(latest))
251
+ return null;
252
+ return latest;
253
+ }
254
+ async function fetchLatest(fetchImpl) {
255
+ try {
256
+ const res = await fetchImpl(REGISTRY_URL, {
257
+ headers: { Accept: ABBREVIATED_ACCEPT },
258
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
259
+ });
260
+ if (!res.ok)
261
+ return null;
262
+ return parseLatestFromRegistry(await res.json());
263
+ } catch {
264
+ return null;
265
+ }
266
+ }
267
+ async function refreshUpdateCache(deps = {}) {
268
+ const stateDir = deps.stateDir ?? new StateDirResolver;
269
+ const now = deps.now ?? Date.now;
270
+ const fetchImpl = deps.fetchImpl ?? fetch;
271
+ try {
272
+ const latest = await fetchLatest(fetchImpl);
273
+ const prev = readCache(stateDir);
274
+ writeCache(stateDir, { lastCheckMs: now(), latest: latest ?? prev?.latest ?? null });
275
+ } catch {}
276
+ }
277
+ function buildUpdateNotice(current, latest, isTTY) {
278
+ const yellow = isTTY ? "\x1B[33m" : "";
279
+ const bold = isTTY ? "\x1B[1m" : "";
280
+ const reset = isTTY ? "\x1B[0m" : "";
281
+ return [
282
+ `${yellow}\u26A0 AgentBridge update available: ${bold}${current}${reset}${yellow} \u2192 ${bold}${latest}${reset}`,
283
+ ` CLI: npm install -g ${PACKAGE_NAME}@latest`,
284
+ ` Plugin: /plugin marketplace update agentbridge (then /reload-plugins)`,
285
+ ` (silence with NO_UPDATE_NOTIFIER=1)`
286
+ ].join(`
287
+ `);
288
+ }
289
+ function checkIntervalMs(env) {
290
+ return parsePositiveIntEnv(CHECK_INTERVAL_ENV, DEFAULT_CHECK_INTERVAL_MS, undefined, env);
291
+ }
292
+ function maybeNotifyUpdate(deps = {}) {
293
+ try {
294
+ const env = deps.env ?? process.env;
295
+ const isTTY = deps.isTTY ?? Boolean(process.stderr.isTTY);
296
+ if (isUpdateCheckSuppressed(env, isTTY))
297
+ return;
298
+ const current = deps.current ?? getCurrentVersion();
299
+ const stateDir = deps.stateDir ?? new StateDirResolver;
300
+ const now = deps.now ?? Date.now;
301
+ const print = deps.print ?? ((m) => process.stderr.write(m + `
302
+ `));
303
+ const cache = readCache(stateDir);
304
+ if (cache?.latest && isStableUpgrade(current, cache.latest)) {
305
+ print(buildUpdateNotice(current, cache.latest, isTTY));
306
+ }
307
+ if (deps.refresh && (!cache || now() - cache.lastCheckMs >= checkIntervalMs(env))) {
308
+ refreshUpdateCache({ stateDir, now, fetchImpl: deps.fetchImpl }).catch(() => {});
309
+ }
310
+ } catch {}
311
+ }
312
+ var PACKAGE_NAME = "@raysonmeng/agentbridge", REGISTRY_URL, ABBREVIATED_ACCEPT = "application/vnd.npm.install-v1+json", DEFAULT_CHECK_INTERVAL_MS, FETCH_TIMEOUT_MS = 2500, CHECK_INTERVAL_ENV = "AGENTBRIDGE_UPDATE_CHECK_INTERVAL_MS";
313
+ var init_update_notifier = __esm(() => {
314
+ init_state_dir();
315
+ init_version_utils();
316
+ REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}`;
317
+ DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
318
+ });
319
+
320
+ // src/config-service.ts
321
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
322
+ import { join as join2 } from "path";
23
323
  function isRecord(value) {
24
324
  return typeof value === "object" && value !== null && !Array.isArray(value);
25
325
  }
@@ -33,6 +333,63 @@ function normalizeInteger(value, fallback) {
33
333
  }
34
334
  return fallback;
35
335
  }
336
+ function normalizeBoundedInteger(value, fallback, min, max) {
337
+ const parsed = normalizeInteger(value, fallback);
338
+ if (parsed < min || parsed > max)
339
+ return fallback;
340
+ return parsed;
341
+ }
342
+ function normalizeBoolean(value, fallback) {
343
+ if (typeof value === "boolean")
344
+ return value;
345
+ if (value === "true" || value === "1")
346
+ return true;
347
+ if (value === "false" || value === "0")
348
+ return false;
349
+ return fallback;
350
+ }
351
+ function normalizeCodexOverride(raw) {
352
+ if (!isRecord(raw))
353
+ return null;
354
+ const override = {};
355
+ if (typeof raw.model === "string" && raw.model.trim() !== "")
356
+ override.model = raw.model.trim();
357
+ if (typeof raw.effort === "string" && raw.effort.trim() !== "")
358
+ override.effort = raw.effort.trim();
359
+ return Object.keys(override).length > 0 ? override : null;
360
+ }
361
+ function normalizeCodexTiers(raw) {
362
+ const tiers = isRecord(raw) ? raw : {};
363
+ return {
364
+ full: normalizeCodexOverride(tiers.full),
365
+ balanced: normalizeCodexOverride(tiers.balanced) ?? DEFAULT_BUDGET_CONFIG.codexTiers.balanced,
366
+ eco: normalizeCodexOverride(tiers.eco) ?? DEFAULT_BUDGET_CONFIG.codexTiers.eco
367
+ };
368
+ }
369
+ function normalizeBudgetConfig(raw) {
370
+ const budget = isRecord(raw) ? raw : {};
371
+ 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);
375
+ if (pauseAt <= resumeBelow) {
376
+ pauseAt = DEFAULT_BUDGET_CONFIG.pauseAt;
377
+ resumeBelow = DEFAULT_BUDGET_CONFIG.resumeBelow;
378
+ }
379
+ return {
380
+ enabled: normalizeBoolean(budget.enabled, DEFAULT_BUDGET_CONFIG.enabled),
381
+ pollSeconds: normalizeBoundedInteger(budget.pollSeconds, DEFAULT_BUDGET_CONFIG.pollSeconds, 5, 3600),
382
+ pauseAt,
383
+ resumeBelow,
384
+ syncDriftPct: normalizeBoundedInteger(budget.syncDriftPct, DEFAULT_BUDGET_CONFIG.syncDriftPct, 1, 100),
385
+ 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)
388
+ },
389
+ codexTierControl: normalizeBoolean(budget.codexTierControl, DEFAULT_BUDGET_CONFIG.codexTierControl) && codexTiers.full !== null,
390
+ codexTiers
391
+ };
392
+ }
36
393
  function normalizeConfig(raw) {
37
394
  if (!isRecord(raw))
38
395
  return null;
@@ -49,7 +406,8 @@ function normalizeConfig(raw) {
49
406
  turnCoordination: {
50
407
  attentionWindowSeconds: normalizeInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds)
51
408
  },
52
- idleShutdownSeconds: normalizeInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds)
409
+ idleShutdownSeconds: normalizeInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds),
410
+ budget: normalizeBudgetConfig(config.budget)
53
411
  };
54
412
  }
55
413
 
@@ -58,15 +416,15 @@ class ConfigService {
58
416
  configPath;
59
417
  constructor(projectRoot) {
60
418
  const root = projectRoot ?? process.cwd();
61
- this.configDir = join(root, CONFIG_DIR);
62
- this.configPath = join(this.configDir, CONFIG_FILE);
419
+ this.configDir = join2(root, CONFIG_DIR);
420
+ this.configPath = join2(this.configDir, CONFIG_FILE);
63
421
  }
64
422
  hasConfig() {
65
- return existsSync(this.configPath);
423
+ return existsSync2(this.configPath);
66
424
  }
67
425
  load() {
68
426
  try {
69
- const raw = readFileSync(this.configPath, "utf-8");
427
+ const raw = readFileSync2(this.configPath, "utf-8");
70
428
  return normalizeConfig(JSON.parse(raw));
71
429
  } catch {
72
430
  return null;
@@ -77,13 +435,13 @@ class ConfigService {
77
435
  }
78
436
  save(config) {
79
437
  this.ensureConfigDir();
80
- writeFileSync(this.configPath, JSON.stringify(config, null, 2) + `
438
+ writeFileSync2(this.configPath, JSON.stringify(config, null, 2) + `
81
439
  `, "utf-8");
82
440
  }
83
441
  initDefaults() {
84
442
  this.ensureConfigDir();
85
443
  const created = [];
86
- if (!existsSync(this.configPath)) {
444
+ if (!existsSync2(this.configPath)) {
87
445
  this.save(DEFAULT_CONFIG);
88
446
  created.push(this.configPath);
89
447
  }
@@ -93,13 +451,30 @@ class ConfigService {
93
451
  return this.configPath;
94
452
  }
95
453
  ensureConfigDir() {
96
- if (!existsSync(this.configDir)) {
97
- mkdirSync(this.configDir, { recursive: true });
454
+ if (!existsSync2(this.configDir)) {
455
+ mkdirSync2(this.configDir, { recursive: true });
98
456
  }
99
457
  }
100
458
  }
101
- var DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json";
459
+ var DEFAULT_BUDGET_CONFIG, DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json";
102
460
  var init_config_service = __esm(() => {
461
+ DEFAULT_BUDGET_CONFIG = {
462
+ enabled: true,
463
+ pollSeconds: 60,
464
+ pauseAt: 90,
465
+ resumeBelow: 30,
466
+ syncDriftPct: 10,
467
+ parallel: {
468
+ minRemainingPct: 60,
469
+ timeWindowSec: 3600
470
+ },
471
+ codexTierControl: false,
472
+ codexTiers: {
473
+ full: null,
474
+ balanced: { effort: "medium" },
475
+ eco: { effort: "low" }
476
+ }
477
+ };
103
478
  DEFAULT_CONFIG = {
104
479
  version: "1.0",
105
480
  codex: {
@@ -109,18 +484,19 @@ var init_config_service = __esm(() => {
109
484
  turnCoordination: {
110
485
  attentionWindowSeconds: 15
111
486
  },
112
- idleShutdownSeconds: 30
487
+ idleShutdownSeconds: 30,
488
+ budget: DEFAULT_BUDGET_CONFIG
113
489
  };
114
490
  });
115
491
 
116
492
  // src/cli/pkg-root.ts
117
- import { dirname, join as join2 } from "path";
118
- import { existsSync as existsSync2 } from "fs";
493
+ import { dirname, join as join3 } from "path";
494
+ import { existsSync as existsSync3 } from "fs";
119
495
  import { execFileSync } from "child_process";
120
496
  function findPackageRoot() {
121
497
  let dir = import.meta.dir;
122
498
  while (true) {
123
- if (existsSync2(join2(dir, "package.json"))) {
499
+ if (existsSync3(join3(dir, "package.json"))) {
124
500
  return dir;
125
501
  }
126
502
  const parent = dirname(dir);
@@ -177,7 +553,7 @@ Another AI agent (Codex, by OpenAI) is available in a parallel session on this m
177
553
 
178
554
  ### Communication mechanism
179
555
  - **Claude \u2192 Codex**: Use the AgentBridge MCP tools (\`reply\` / \`get_messages\`) \u2014 these are yours only.
180
- - **Codex \u2192 Claude**: Codex has no symmetric tool. The bridge transparently intercepts Codex's normal output and forwards it to you. Messages arrive as push notifications (or via \`get_messages\` in pull mode).
556
+ - **Codex \u2192 Claude**: Codex has no symmetric tool. The bridge transparently intercepts Codex's normal output and forwards it to you as push notifications (if a push fails, drain the fallback queue with \`get_messages\`).
181
557
  - If Codex ever complains it can't find a "send-to-Claude" API, remind it that its side is transparent \u2014 it just writes a reply and you'll see it.
182
558
 
183
559
  ### When to collaborate vs. work solo
@@ -199,7 +575,16 @@ Another AI agent (Codex, by OpenAI) is available in a parallel session on this m
199
575
  1. When you receive a complex task, **proactively propose a division of labor** to Codex via the reply tool.
200
576
  2. State what you'll handle and what you'd like Codex to take on.
201
577
  3. Ask for Codex's agreement or counter-proposal before proceeding.
202
- 4. After task completion, **cross-review** each other's work.`, AGENTS_MD_SECTION = `## AgentBridge \u2014 Multi-Agent Collaboration
578
+ 4. After task completion, **cross-review** each other's work.
579
+
580
+ ### Budget awareness (active when agent-quota-guard is installed)
581
+ - Goal: **keep the task moving while fully using the subscription quota**. The bridge polls both agents' account-level 5h/weekly windows and may send \`system_budget_*\` notices: **balance** (route more work to the lighter side), **parallel** (quota surplus near the 5h reset \u2014 split more parallel subtasks), **pause/handoff/resume**.
582
+ - \`get_budget\` shows BOTH sides' quota \u2014 re-check it **before every task-allocation decision**. NEVER rely on quota numbers remembered from earlier in the conversation: the weekly window can refresh EARLY (resetting both 5h and weekly), so a side you remember as nearly exhausted may be fully restored.
583
+ - Side-aware pause semantics:
584
+ - **Codex exhausted** (\`system_budget_pause\`): the reply gate closes. Do not retry replies; continue solo on independent work, note the split point in a checkpoint.
585
+ - **You (Claude) exhausted** (\`system_budget_handoff\`): the gate stays OPEN \u2014 immediately send ONE handoff reply to Codex packaging the remaining task list, context, artifact locations and acceptance criteria, then stop working (your own quota-guard will hard-stop you at 92%). Codex relays the baton.
586
+ - **Both exhausted**: joint pause; checkpoint and wait for the resume notice.
587
+ - Save quota with model tiers: route mechanical subagent work to **haiku**, routine work to **sonnet**, reserve **opus** for architecture decisions; when your side is the heavier consumer, delegate more to Codex.`, AGENTS_MD_SECTION = `## AgentBridge \u2014 Multi-Agent Collaboration
203
588
 
204
589
  You are working in a **multi-agent environment** powered by AgentBridge.
205
590
  Another AI agent (Claude, by Anthropic) is available in a parallel session on this machine.
@@ -231,7 +616,33 @@ AgentBridge is a **transparent proxy** on your side. You do **not** have a tool
231
616
  1. When you receive a complex task, **proactively propose a division of labor** in your response (Claude will receive it).
232
617
  2. State what you'll handle and what you'd like Claude to take on.
233
618
  3. Ask for Claude's agreement or counter-proposal before proceeding.
234
- 4. After task completion, **cross-review** each other's work.`;
619
+ 4. After task completion, **cross-review** each other's work.
620
+
621
+ ### Message markers
622
+ Put a marker at the **very start** of each \`agentMessage\` (it must be the first text \u2014 e.g. \`[IMPORTANT] Task done\`, not \`Task done [IMPORTANT]\`):
623
+ - \`[IMPORTANT]\` \u2014 decisions, reviews, completions, blockers
624
+ - \`[STATUS]\` \u2014 progress updates
625
+ - \`[FYI]\` \u2014 background context
626
+
627
+ Keep \`agentMessage\` for high-value communication only.
628
+
629
+ ### Git operations \u2014 FORBIDDEN for you
630
+ You MUST NOT run git **write** commands: \`commit\`, \`push\`, \`pull\`, \`fetch\`, \`checkout -b\`, \`branch\`, \`merge\`, \`rebase\`, \`cherry-pick\`, \`tag\`, \`stash\`. They write the \`.git\` directory (blocked by your sandbox) and will hang your session. Read-only git (\`status\`, \`log\`, \`diff\`, \`show\`, \`rev-parse\`) is fine. Delegate **all** git writes to Claude: report what you changed and let Claude handle branching, committing, and pushing.
631
+
632
+ ### Role guidance
633
+ - Your default role: **Implementer, Executor, Verifier**.
634
+ - Analytical / review tasks: **Independent Analysis & Convergence**.
635
+ - Implementation tasks: **Architect \u2192 Builder \u2192 Critic**.
636
+ - Debugging tasks: **Hypothesis \u2192 Experiment \u2192 Interpretation**.
637
+ - Do not blindly follow Claude \u2014 challenge with evidence when you disagree.
638
+ - Use explicit collaboration phrases: "My independent view is:", "I agree on:", "I disagree on:", "Current consensus:".
639
+
640
+ ### Budget awareness (active when agent-quota-guard is installed)
641
+ - Goal: **keep the task moving while fully using the subscription quota**. You can check BOTH sides' quota yourself via your quota-guard MCP tool \`check_budget\` with \`agent: "claude"\` or \`"codex"\` \u2014 re-check **before negotiating task splits**, and NEVER rely on remembered numbers: the weekly window can refresh early (resetting both 5h and weekly windows).
642
+ - During a **budget pause** (your side exhausted) you simply stop receiving new turns \u2014 that IS the pause. Your own quota-guard hooks still apply; work resumes when Claude's next message arrives.
643
+ - **Handoff (Claude's side exhausted)**: you may receive a baton message packaging the remaining work. Push as far as possible within that single turn; write leftovers to a checkpoint file; do NOT expect Claude to respond until its quota refreshes.
644
+ - Claude may route more or less work to you based on quota drift \u2014 expected load balancing, not preference.
645
+ - When the user enabled tier control, the bridge may adjust your model/reasoning-effort via turn parameters under budget pressure; if asked to economize, prefer lower effort and concise outputs.`;
235
646
 
236
647
  // src/cli/init.ts
237
648
  var exports_init = {};
@@ -241,8 +652,8 @@ __export(exports_init, {
241
652
  compareVersions: () => compareVersions
242
653
  });
243
654
  import { execSync, execFileSync as execFileSync2 } from "child_process";
244
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
245
- import { join as join3 } from "path";
655
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
656
+ import { join as join4 } from "path";
246
657
  async function runInit() {
247
658
  console.log(`AgentBridge Init
248
659
  `);
@@ -331,29 +742,16 @@ function checkCodex() {
331
742
  process.exit(1);
332
743
  }
333
744
  }
334
- function compareVersions(a, b) {
335
- const pa = a.split(".").map(Number);
336
- const pb = b.split(".").map(Number);
337
- for (let i = 0;i < 3; i++) {
338
- const va = pa[i] ?? 0;
339
- const vb = pb[i] ?? 0;
340
- if (va < vb)
341
- return -1;
342
- if (va > vb)
343
- return 1;
344
- }
345
- return 0;
346
- }
347
745
  function writeCollaborationSections(projectRoot) {
348
746
  const results = [];
349
747
  const files = [
350
- { name: "CLAUDE.md", path: join3(projectRoot, "CLAUDE.md"), section: CLAUDE_MD_SECTION },
351
- { name: "AGENTS.md", path: join3(projectRoot, "AGENTS.md"), section: AGENTS_MD_SECTION }
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 }
352
750
  ];
353
751
  for (const { name, path, section } of files) {
354
752
  let existing = "";
355
753
  try {
356
- existing = readFileSync2(path, "utf-8");
754
+ existing = readFileSync3(path, "utf-8");
357
755
  } catch {}
358
756
  let updated;
359
757
  try {
@@ -367,7 +765,7 @@ function writeCollaborationSections(projectRoot) {
367
765
  results.push(`${name}: unchanged (section already up to date)`);
368
766
  continue;
369
767
  }
370
- writeFileSync2(path, updated, "utf-8");
768
+ writeFileSync3(path, updated, "utf-8");
371
769
  if (existing === "") {
372
770
  results.push(`${name}: created with collaboration section`);
373
771
  } else if (existing.includes(`<!-- ${MARKER_ID}:start -->`)) {
@@ -383,6 +781,7 @@ var init_init = __esm(() => {
383
781
  init_config_service();
384
782
  init_cli();
385
783
  init_pkg_root();
784
+ init_version_utils();
386
785
  });
387
786
 
388
787
  // src/cli/dev.ts
@@ -392,43 +791,60 @@ __export(exports_dev, {
392
791
  });
393
792
  import { execFileSync as execFileSync3, spawnSync } from "child_process";
394
793
  import { resolve } from "path";
395
- import { existsSync as existsSync3, cpSync, rmSync } from "fs";
396
- import { homedir } from "os";
397
- async function runDev() {
794
+ import { existsSync as existsSync4, cpSync, rmSync } from "fs";
795
+ import { homedir as homedir2 } from "os";
796
+ async function runDev(args = []) {
398
797
  console.log(`AgentBridge Dev Setup
399
798
  `);
799
+ const skipBuild = args.includes("--skip-build");
400
800
  const projectRoot = findPackageRoot();
401
801
  const marketplacePath = resolve(projectRoot, ".claude-plugin", "marketplace.json");
402
802
  const pluginDir = resolve(projectRoot, "plugins", "agentbridge");
403
803
  const pluginManifest = resolve(pluginDir, ".claude-plugin", "plugin.json");
404
- console.log("Building CLI from source...");
405
- const cliBuild = spawnSync("bun", ["run", "build:cli"], {
406
- cwd: projectRoot,
407
- stdio: "inherit"
408
- });
409
- if (cliBuild.status !== 0) {
410
- console.error(" ERROR: CLI build failed. Fix build errors and try again.");
804
+ const buildScript = resolve(projectRoot, "scripts", "build-bundles.mjs");
805
+ if (!existsSync4(buildScript)) {
806
+ console.error(" ERROR: 'agentbridge dev' must run inside an AgentBridge repository checkout \u2014");
807
+ console.error(" the published package does not ship the build scripts.");
808
+ console.error("");
809
+ console.error(" cd <agent_bridge repo> && bun src/cli.ts dev");
810
+ console.error("");
811
+ console.error(" Tip: from the repo, `bun run install:global` updates the global CLI");
812
+ console.error(" AND syncs the Claude Code plugin in one step.");
411
813
  process.exit(1);
412
814
  }
413
- console.log(` \u2713 CLI built successfully
815
+ if (skipBuild) {
816
+ console.log(`Skipping builds (--skip-build: caller already built CLI + plugin)
414
817
  `);
415
- console.log("Building plugin from source...");
416
- const buildResult = spawnSync("bun", ["run", "build:plugin"], {
417
- cwd: projectRoot,
418
- stdio: "inherit"
419
- });
420
- if (buildResult.status !== 0) {
421
- console.error(" ERROR: Plugin build failed. Fix build errors and try again.");
422
- process.exit(1);
423
- }
424
- console.log(` \u2713 Plugin built successfully
818
+ } else {
819
+ console.log("Building CLI from source...");
820
+ const cliBuild = spawnSync("bun", ["run", "build:cli"], {
821
+ cwd: projectRoot,
822
+ stdio: "inherit"
823
+ });
824
+ if (cliBuild.status !== 0) {
825
+ console.error(" ERROR: CLI build failed. Fix build errors and try again.");
826
+ process.exit(1);
827
+ }
828
+ console.log(` \u2713 CLI built successfully
829
+ `);
830
+ console.log("Building plugin from source...");
831
+ const buildResult = spawnSync("bun", ["run", "build:plugin"], {
832
+ cwd: projectRoot,
833
+ stdio: "inherit"
834
+ });
835
+ if (buildResult.status !== 0) {
836
+ console.error(" ERROR: Plugin build failed. Fix build errors and try again.");
837
+ process.exit(1);
838
+ }
839
+ console.log(` \u2713 Plugin built successfully
425
840
  `);
426
- if (!existsSync3(pluginManifest)) {
841
+ }
842
+ if (!existsSync4(pluginManifest)) {
427
843
  console.error(` ERROR: Plugin manifest not found at ${pluginManifest}`);
428
844
  console.error(" Run 'bun run build:plugin' first, or check your working tree.");
429
845
  process.exit(1);
430
846
  }
431
- if (!existsSync3(marketplacePath)) {
847
+ if (!existsSync4(marketplacePath)) {
432
848
  console.error(` ERROR: Marketplace manifest not found at ${marketplacePath}`);
433
849
  process.exit(1);
434
850
  }
@@ -457,8 +873,8 @@ Installing plugin...`);
457
873
  }
458
874
  console.log(`
459
875
  Syncing local plugin to cache...`);
460
- const cacheDir = resolve(homedir(), ".claude", "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME);
461
- if (existsSync3(cacheDir)) {
876
+ const cacheDir = resolve(homedir2(), ".claude", "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME);
877
+ if (existsSync4(cacheDir)) {
462
878
  const versionDirs = Bun.spawnSync(["ls", cacheDir]).stdout.toString().trim().split(`
463
879
  `).filter(Boolean);
464
880
  for (const ver of versionDirs) {
@@ -484,65 +900,415 @@ var init_dev = __esm(() => {
484
900
  init_pkg_root();
485
901
  });
486
902
 
487
- // src/daemon-lifecycle.ts
488
- import { spawn, execFileSync as execFileSync4 } from "child_process";
489
- import { existsSync as existsSync4, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3, openSync, closeSync, constants } from "fs";
490
- import { fileURLToPath } from "url";
903
+ // src/control-protocol.ts
904
+ var CLOSE_CODE_REPLACED = 4001, CLOSE_CODE_EVICTED_STALE = 4002, CLOSE_CODE_PROBE_IN_PROGRESS = 4003, CLOSE_CODE_PAIR_MISMATCH = 4004;
491
905
 
492
- class DaemonLifecycle {
493
- stateDir;
494
- controlPort;
495
- log;
496
- constructor(opts) {
497
- this.stateDir = opts.stateDir;
498
- this.controlPort = opts.controlPort;
499
- this.log = opts.log;
500
- }
501
- get healthUrl() {
502
- return `http://127.0.0.1:${this.controlPort}/healthz`;
503
- }
504
- get readyUrl() {
505
- return `http://127.0.0.1:${this.controlPort}/readyz`;
506
- }
507
- get controlWsUrl() {
508
- return `ws://127.0.0.1:${this.controlPort}/ws`;
509
- }
510
- async ensureRunning() {
511
- if (await this.isHealthy()) {
512
- await this.waitForReady();
513
- return;
906
+ // src/daemon-client.ts
907
+ import { EventEmitter } from "events";
908
+ var nextSocketId = 0, DaemonClient;
909
+ var init_daemon_client = __esm(() => {
910
+ DaemonClient = class DaemonClient extends EventEmitter {
911
+ url;
912
+ options;
913
+ ws = null;
914
+ wsId = 0;
915
+ nextRequestId = 1;
916
+ pendingReplies = new Map;
917
+ constructor(url, options = {}) {
918
+ super();
919
+ this.url = url;
920
+ this.options = options;
514
921
  }
515
- const existingPid = this.readPid();
516
- if (existingPid) {
517
- if (isProcessAlive(existingPid)) {
518
- if (this.isDaemonProcess(existingPid)) {
519
- try {
520
- await this.waitForReady(12, 250);
521
- return;
522
- } catch {
523
- throw new Error(`Found existing daemon process ${existingPid}, but control port ${this.controlPort} never became ready.`);
524
- }
525
- }
526
- this.log(`Pid ${existingPid} is alive but not an AgentBridge daemon, removing stale pid file`);
922
+ async connect() {
923
+ if (this.ws?.readyState === WebSocket.OPEN) {
924
+ this.log(`connect() skipped \u2014 ws#${this.wsId} already OPEN`);
925
+ return;
527
926
  }
528
- this.removeStalePidFile();
927
+ if (this.ws) {
928
+ const state = this.ws.readyState;
929
+ this.log(`connect() closing lingering ws#${this.wsId} (readyState=${state})`);
930
+ try {
931
+ this.ws.close();
932
+ } catch {}
933
+ this.ws = null;
934
+ }
935
+ const socketId = ++nextSocketId;
936
+ await new Promise((resolve2, reject) => {
937
+ const ws = new WebSocket(this.url);
938
+ let settled = false;
939
+ ws.onopen = () => {
940
+ settled = true;
941
+ this.ws = ws;
942
+ this.wsId = socketId;
943
+ this.attachSocketHandlers(ws, socketId);
944
+ this.log(`ws#${socketId} opened and attached`);
945
+ resolve2();
946
+ };
947
+ ws.onerror = () => {
948
+ if (settled)
949
+ return;
950
+ settled = true;
951
+ reject(new Error(`Failed to connect to AgentBridge daemon at ${this.url}`));
952
+ };
953
+ ws.onclose = () => {
954
+ if (settled)
955
+ return;
956
+ settled = true;
957
+ reject(new Error(`AgentBridge daemon closed the connection during startup (${this.url})`));
958
+ };
959
+ });
529
960
  }
530
- const lockAcquired = this.acquireLock();
531
- if (!lockAcquired) {
532
- this.log("Another process is starting the daemon, waiting for readiness...");
533
- await this.waitForReady();
534
- return;
961
+ attachClaude() {
962
+ this.send({
963
+ type: "claude_connect",
964
+ ...this.options.identity ? { identity: this.options.identity } : {}
965
+ });
966
+ }
967
+ async attachClaudeAndWaitForStatus(timeoutMs = 1000) {
968
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
969
+ return null;
970
+ }
971
+ return await new Promise((resolve2) => {
972
+ let settled = false;
973
+ let timer = null;
974
+ const cleanup = () => {
975
+ if (settled)
976
+ return;
977
+ settled = true;
978
+ if (timer) {
979
+ clearTimeout(timer);
980
+ timer = null;
981
+ }
982
+ this.off("status", onStatus);
983
+ this.off("rejected", onRejected);
984
+ this.off("disconnect", onDisconnect);
985
+ };
986
+ const finish = (value) => {
987
+ cleanup();
988
+ resolve2(value);
989
+ };
990
+ const onStatus = (status) => finish(status);
991
+ const onRejected = () => finish(null);
992
+ const onDisconnect = () => finish(null);
993
+ this.on("status", onStatus);
994
+ this.on("rejected", onRejected);
995
+ this.on("disconnect", onDisconnect);
996
+ timer = setTimeout(() => {
997
+ finish(null);
998
+ }, timeoutMs);
999
+ try {
1000
+ this.attachClaude();
1001
+ } catch {
1002
+ finish(null);
1003
+ }
1004
+ });
1005
+ }
1006
+ async probeIncumbent(timeoutMs = 3000) {
1007
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1008
+ return { connected: false, alive: false };
1009
+ }
1010
+ return await new Promise((resolve2) => {
1011
+ let settled = false;
1012
+ let timer = null;
1013
+ const finish = (value) => {
1014
+ if (settled)
1015
+ return;
1016
+ settled = true;
1017
+ if (timer)
1018
+ clearTimeout(timer);
1019
+ this.off("incumbentStatus", onStatus);
1020
+ this.off("disconnect", onDisconnect);
1021
+ this.off("rejected", onRejected);
1022
+ resolve2(value);
1023
+ };
1024
+ const onStatus = (s) => finish(s);
1025
+ const onDisconnect = () => finish({ connected: false, alive: false });
1026
+ const onRejected = () => finish({ connected: false, alive: false });
1027
+ this.on("incumbentStatus", onStatus);
1028
+ this.on("disconnect", onDisconnect);
1029
+ this.on("rejected", onRejected);
1030
+ timer = setTimeout(() => finish({ connected: false, alive: false }), timeoutMs);
1031
+ try {
1032
+ this.send({ type: "probe_incumbent" });
1033
+ } catch {
1034
+ finish({ connected: false, alive: false });
1035
+ }
1036
+ });
1037
+ }
1038
+ async disconnect() {
1039
+ if (!this.ws)
1040
+ return;
1041
+ try {
1042
+ this.send({ type: "claude_disconnect" });
1043
+ } catch {}
1044
+ try {
1045
+ this.ws.close();
1046
+ } catch {}
1047
+ this.ws = null;
1048
+ this.rejectPendingReplies("Daemon connection closed");
1049
+ }
1050
+ async sendReply(message, requireReply, onBusy) {
1051
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1052
+ return { success: false, error: "AgentBridge daemon is not connected." };
1053
+ }
1054
+ const requestId = `reply_${Date.now()}_${this.nextRequestId++}`;
1055
+ return new Promise((resolve2) => {
1056
+ const timer = setTimeout(() => {
1057
+ 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 });
1061
+ this.send({
1062
+ type: "claude_to_codex",
1063
+ requestId,
1064
+ message,
1065
+ ...requireReply ? { requireReply: true } : {},
1066
+ ...onBusy && onBusy !== "reject" ? { onBusy } : {}
1067
+ });
1068
+ });
1069
+ }
1070
+ attachSocketHandlers(ws, socketId) {
1071
+ ws.onmessage = (event) => {
1072
+ const raw = typeof event.data === "string" ? event.data : event.data.toString();
1073
+ let message;
1074
+ try {
1075
+ message = JSON.parse(raw);
1076
+ } catch {
1077
+ return;
1078
+ }
1079
+ switch (message.type) {
1080
+ case "codex_to_claude":
1081
+ this.emit("codexMessage", message.message);
1082
+ return;
1083
+ case "claude_to_codex_result": {
1084
+ const pending = this.pendingReplies.get(message.requestId);
1085
+ if (!pending)
1086
+ return;
1087
+ clearTimeout(pending.timer);
1088
+ this.pendingReplies.delete(message.requestId);
1089
+ pending.resolve({ success: message.success, error: message.error });
1090
+ return;
1091
+ }
1092
+ case "status":
1093
+ this.emit("status", message.status);
1094
+ return;
1095
+ case "incumbent_status":
1096
+ this.emit("incumbentStatus", { connected: message.connected, alive: message.alive });
1097
+ return;
1098
+ }
1099
+ };
1100
+ ws.onclose = (event) => {
1101
+ const isCurrent = this.ws === ws;
1102
+ this.log(`ws#${socketId} onclose (code=${event.code}, reason=${event.reason || "none"}, isCurrent=${isCurrent}, currentWsId=${this.wsId})`);
1103
+ if (isCurrent) {
1104
+ this.ws = null;
1105
+ this.rejectPendingReplies("AgentBridge daemon disconnected.");
1106
+ if (event.code === CLOSE_CODE_REPLACED || event.code === CLOSE_CODE_EVICTED_STALE || event.code === CLOSE_CODE_PROBE_IN_PROGRESS || event.code === CLOSE_CODE_PAIR_MISMATCH) {
1107
+ this.emit("rejected", event.code);
1108
+ } else {
1109
+ this.emit("disconnect");
1110
+ }
1111
+ }
1112
+ };
1113
+ ws.onerror = () => {};
1114
+ }
1115
+ rejectPendingReplies(error) {
1116
+ for (const [requestId, pending] of this.pendingReplies.entries()) {
1117
+ clearTimeout(pending.timer);
1118
+ pending.resolve({ success: false, error });
1119
+ this.pendingReplies.delete(requestId);
1120
+ }
1121
+ }
1122
+ send(message) {
1123
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1124
+ throw new Error("AgentBridge daemon socket is not open.");
1125
+ }
1126
+ this.ws.send(JSON.stringify(message));
1127
+ }
1128
+ log(msg) {
1129
+ process.stderr.write(`[${new Date().toISOString()}] [DaemonClient] ${msg}
1130
+ `);
535
1131
  }
1132
+ };
1133
+ });
1134
+
1135
+ // src/contract-version.ts
1136
+ var CONTRACT_VERSION = 1;
1137
+
1138
+ // src/build-info.ts
1139
+ function defineString(value, fallback) {
1140
+ return typeof value === "string" && value.length > 0 ? value : fallback;
1141
+ }
1142
+ function defineBundle(value) {
1143
+ if (value === "source" || value === "dist" || value === "plugin")
1144
+ return value;
1145
+ return import.meta.url.endsWith(".ts") ? "source" : "dist";
1146
+ }
1147
+ function defineNumber(value, fallback) {
1148
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1149
+ }
1150
+ function sameRuntimeContract(a, b) {
1151
+ if (!a || !b)
1152
+ return false;
1153
+ return a.version === b.version && a.commit === b.commit && a.contractVersion === b.contractVersion;
1154
+ }
1155
+ function compatibleContractVersion(a, b) {
1156
+ if (!a || !b)
1157
+ return false;
1158
+ return a.contractVersion === b.contractVersion;
1159
+ }
1160
+ function formatBuildInfo(build) {
1161
+ if (!build)
1162
+ return "<unknown>";
1163
+ return `${build.version}/${build.commit}/${build.bundle}/contract-v${build.contractVersion}`;
1164
+ }
1165
+ var BUILD_INFO;
1166
+ var init_build_info = __esm(() => {
1167
+ BUILD_INFO = Object.freeze({
1168
+ version: defineString("0.1.8", "0.0.0-source"),
1169
+ commit: defineString("c80a7fd", "source"),
1170
+ bundle: defineBundle("dist"),
1171
+ contractVersion: defineNumber(1, CONTRACT_VERSION)
1172
+ });
1173
+ });
1174
+
1175
+ // src/daemon-lifecycle.ts
1176
+ import { spawn, execFileSync as execFileSync4 } from "child_process";
1177
+ import { existsSync as existsSync5, readFileSync as readFileSync4, statSync, unlinkSync, writeFileSync as writeFileSync4, openSync, closeSync, constants } from "fs";
1178
+ import { fileURLToPath } from "url";
1179
+
1180
+ class DaemonLifecycle {
1181
+ stateDir;
1182
+ controlPort;
1183
+ log;
1184
+ constructor(opts) {
1185
+ this.stateDir = opts.stateDir;
1186
+ this.controlPort = opts.controlPort;
1187
+ this.log = opts.log;
1188
+ }
1189
+ get healthUrl() {
1190
+ return `http://127.0.0.1:${this.controlPort}/healthz`;
1191
+ }
1192
+ get readyUrl() {
1193
+ return `http://127.0.0.1:${this.controlPort}/readyz`;
1194
+ }
1195
+ get controlWsUrl() {
1196
+ return `ws://127.0.0.1:${this.controlPort}/ws`;
1197
+ }
1198
+ get expectedPairId() {
1199
+ return process.env.AGENTBRIDGE_PAIR_ID || null;
1200
+ }
1201
+ async fetchStatus() {
536
1202
  try {
1203
+ const response = await fetchWithTimeout(this.healthUrl);
1204
+ if (!response.ok)
1205
+ return null;
1206
+ return await response.json();
1207
+ } catch {
1208
+ return null;
1209
+ }
1210
+ }
1211
+ isForeignDaemon(status) {
1212
+ const expected = this.expectedPairId;
1213
+ if (!expected)
1214
+ return false;
1215
+ if (!status)
1216
+ return false;
1217
+ const reported = status.pairId;
1218
+ if (reported == null)
1219
+ return true;
1220
+ return reported !== expected;
1221
+ }
1222
+ isRegisteredPairDaemonInManualMode(status) {
1223
+ return !this.expectedPairId && status?.pairId != null;
1224
+ }
1225
+ isBuildDrifted(status) {
1226
+ if (process.env.AGENTBRIDGE_ALLOW_BUILD_DRIFT === "1")
1227
+ return false;
1228
+ const runtime = status?.build;
1229
+ if (!runtime)
1230
+ return true;
1231
+ return !sameRuntimeContract(runtime, BUILD_INFO);
1232
+ }
1233
+ canReuseDespiteDrift(status) {
1234
+ if (!compatibleContractVersion(status?.build, BUILD_INFO))
1235
+ return false;
1236
+ return status?.tuiConnected === true;
1237
+ }
1238
+ async ensureRunning() {
1239
+ if (await this.isHealthy()) {
1240
+ const status = await this.fetchStatus();
1241
+ if (this.isRegisteredPairDaemonInManualMode(status)) {
1242
+ 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.`);
1243
+ }
1244
+ if (this.isForeignDaemon(status)) {
1245
+ 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`);
1246
+ await this.replaceUnhealthyDaemon(status?.pid);
1247
+ return;
1248
+ }
1249
+ if (this.isBuildDrifted(status)) {
1250
+ if (this.canReuseDespiteDrift(status)) {
1251
+ 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)`);
1252
+ } else {
1253
+ this.log(`Daemon on control port ${this.controlPort} is running build ${formatBuildInfo(status?.build)} ` + `but launcher is ${formatBuildInfo(BUILD_INFO)} \u2014 replacing drifted daemon`);
1254
+ await this.replaceUnhealthyDaemon(status?.pid);
1255
+ return;
1256
+ }
1257
+ }
1258
+ try {
1259
+ await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1260
+ return;
1261
+ } catch {
1262
+ this.log(`Daemon on control port ${this.controlPort} is healthy but not ready within reuse window \u2014 replacing`);
1263
+ await this.replaceUnhealthyDaemon(status?.pid);
1264
+ return;
1265
+ }
1266
+ }
1267
+ const existingPid = this.readPid();
1268
+ if (existingPid) {
1269
+ if (isProcessAlive(existingPid)) {
1270
+ if (this.isDaemonProcess(existingPid)) {
1271
+ try {
1272
+ await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1273
+ return;
1274
+ } catch {
1275
+ this.log(`Existing daemon process ${existingPid} never became ready \u2014 replacing`);
1276
+ await this.replaceUnhealthyDaemon(existingPid);
1277
+ return;
1278
+ }
1279
+ }
1280
+ this.log(`Pid ${existingPid} is alive but not an AgentBridge daemon, removing stale pid file`);
1281
+ }
1282
+ this.removeStalePidFile();
1283
+ }
1284
+ await this.withStartupLockStrict(async (locked) => {
1285
+ if (!locked) {
1286
+ this.log("Another process holds the startup lock, waiting for readiness+identity...");
1287
+ await this.waitForReadyAndOurs();
1288
+ return;
1289
+ }
1290
+ if (await this.isHealthy()) {
1291
+ const status = await this.fetchStatus();
1292
+ if (this.isForeignDaemon(status) || this.isBuildDrifted(status) && !this.canReuseDespiteDrift(status)) {
1293
+ this.log(`Daemon on control port ${this.controlPort} is not reusable under startup lock ` + `(pair=${status?.pairId ?? "<none>"}, build=${formatBuildInfo(status?.build)}) \u2014 replacing`);
1294
+ await this.kill(3000, status?.pid);
1295
+ } else {
1296
+ try {
1297
+ await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1298
+ return;
1299
+ } catch {
1300
+ this.log(`Daemon on control port ${this.controlPort} is healthy but not ready under startup lock \u2014 replacing`);
1301
+ await this.kill(3000, status?.pid);
1302
+ }
1303
+ }
1304
+ }
537
1305
  this.launch();
538
1306
  await this.waitForReady();
539
- } finally {
540
- this.releaseLock();
541
- }
1307
+ });
542
1308
  }
543
1309
  async isHealthy() {
544
1310
  try {
545
- const response = await fetch(this.healthUrl);
1311
+ const response = await fetchWithTimeout(this.healthUrl);
546
1312
  return response.ok;
547
1313
  } catch {
548
1314
  return false;
@@ -558,7 +1324,7 @@ class DaemonLifecycle {
558
1324
  }
559
1325
  async isReady() {
560
1326
  try {
561
- const response = await fetch(this.readyUrl);
1327
+ const response = await fetchWithTimeout(this.readyUrl);
562
1328
  return response.ok;
563
1329
  } catch {
564
1330
  return false;
@@ -572,9 +1338,21 @@ class DaemonLifecycle {
572
1338
  }
573
1339
  throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
574
1340
  }
1341
+ async waitForReadyAndOurs(maxRetries = 40, delayMs = 250) {
1342
+ for (let attempt = 0;attempt < maxRetries; attempt++) {
1343
+ if (await this.isReady()) {
1344
+ const status = await this.fetchStatus();
1345
+ if (!this.isForeignDaemon(status) && (!this.isBuildDrifted(status) || this.canReuseDespiteDrift(status))) {
1346
+ return;
1347
+ }
1348
+ }
1349
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
1350
+ }
1351
+ throw new Error(`Timed out waiting for AgentBridge daemon readiness+identity on ${this.readyUrl} (control port ${this.controlPort})`);
1352
+ }
575
1353
  readStatus() {
576
1354
  try {
577
- const raw = readFileSync3(this.stateDir.statusFile, "utf-8");
1355
+ const raw = readFileSync4(this.stateDir.statusFile, "utf-8");
578
1356
  return JSON.parse(raw);
579
1357
  } catch {
580
1358
  return null;
@@ -582,12 +1360,12 @@ class DaemonLifecycle {
582
1360
  }
583
1361
  writeStatus(status) {
584
1362
  this.stateDir.ensure();
585
- writeFileSync3(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
1363
+ writeFileSync4(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
586
1364
  `, "utf-8");
587
1365
  }
588
1366
  readPid() {
589
1367
  try {
590
- const raw = readFileSync3(this.stateDir.pidFile, "utf-8").trim();
1368
+ const raw = readFileSync4(this.stateDir.pidFile, "utf-8").trim();
591
1369
  if (!raw)
592
1370
  return null;
593
1371
  const pid = Number.parseInt(raw, 10);
@@ -598,7 +1376,7 @@ class DaemonLifecycle {
598
1376
  }
599
1377
  writePid(pid) {
600
1378
  this.stateDir.ensure();
601
- writeFileSync3(this.stateDir.pidFile, `${pid ?? process.pid}
1379
+ writeFileSync4(this.stateDir.pidFile, `${pid ?? process.pid}
602
1380
  `, "utf-8");
603
1381
  }
604
1382
  removePidFile() {
@@ -613,7 +1391,7 @@ class DaemonLifecycle {
613
1391
  }
614
1392
  markKilled() {
615
1393
  this.stateDir.ensure();
616
- writeFileSync3(this.stateDir.killedFile, `${Date.now()}
1394
+ writeFileSync4(this.stateDir.killedFile, `${Date.now()}
617
1395
  `, "utf-8");
618
1396
  }
619
1397
  clearKilled() {
@@ -622,7 +1400,7 @@ class DaemonLifecycle {
622
1400
  } catch {}
623
1401
  }
624
1402
  wasKilled() {
625
- return existsSync4(this.stateDir.killedFile);
1403
+ return existsSync5(this.stateDir.killedFile);
626
1404
  }
627
1405
  launch() {
628
1406
  this.stateDir.ensure();
@@ -643,36 +1421,90 @@ class DaemonLifecycle {
643
1421
  this.log("Removing stale pid file");
644
1422
  this.removePidFile();
645
1423
  }
646
- acquireLock(depth = 0) {
647
- if (depth > 1) {
648
- this.log("Lock acquisition failed after retry, proceeding without lock");
649
- return true;
1424
+ async replaceUnhealthyDaemon(statusPid) {
1425
+ await this.withStartupLockStrict(async (locked) => {
1426
+ if (!locked) {
1427
+ this.log("Another process holds the startup lock, waiting for readiness+identity...");
1428
+ await this.waitForReadyAndOurs();
1429
+ return;
1430
+ }
1431
+ if (await this.isHealthy()) {
1432
+ const status = await this.fetchStatus();
1433
+ if (!this.isForeignDaemon(status) && (!this.isBuildDrifted(status) || this.canReuseDespiteDrift(status))) {
1434
+ try {
1435
+ await this.waitForReady(REUSE_READY_RETRIES, REUSE_READY_DELAY_MS);
1436
+ return;
1437
+ } catch {}
1438
+ }
1439
+ }
1440
+ this.log(`Killing unhealthy daemon on control port ${this.controlPort} and relaunching`);
1441
+ await this.kill(3000, statusPid);
1442
+ this.launch();
1443
+ await this.waitForReady();
1444
+ });
1445
+ }
1446
+ async withStartupLockStrict(fn) {
1447
+ const locked = this.acquireLockStrict();
1448
+ try {
1449
+ return await fn(locked);
1450
+ } finally {
1451
+ if (locked)
1452
+ this.releaseLock();
650
1453
  }
1454
+ }
1455
+ acquireLockStrict(reclaimed = false) {
651
1456
  this.stateDir.ensure();
1457
+ let fd = null;
652
1458
  try {
653
- const fd = openSync(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
654
- writeFileSync3(fd, `${process.pid}
1459
+ fd = openSync(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
1460
+ writeFileSync4(fd, `${process.pid}
655
1461
  `);
656
1462
  closeSync(fd);
657
1463
  return true;
658
1464
  } catch (err) {
1465
+ if (fd !== null && err.code !== "EEXIST") {
1466
+ try {
1467
+ closeSync(fd);
1468
+ } catch {}
1469
+ this.releaseLock();
1470
+ }
659
1471
  if (err.code === "EEXIST") {
1472
+ if (reclaimed)
1473
+ return false;
660
1474
  try {
661
- const holderPid = Number.parseInt(readFileSync3(this.stateDir.lockFile, "utf-8").trim(), 10);
1475
+ const holderPid = Number.parseInt(readFileSync4(this.stateDir.lockFile, "utf-8").trim(), 10);
662
1476
  if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
663
- this.log(`Stale lock file from dead process ${holderPid}, removing`);
1477
+ this.log(`Stale startup lock from dead process ${holderPid}, reclaiming`);
1478
+ this.releaseLock();
1479
+ return this.acquireLockStrict(true);
1480
+ }
1481
+ if (Number.isFinite(holderPid) && this.lockAgeMs() > LOCK_IDENTITY_GRACE_MS && !this.isAgentBridgeProcess(holderPid)) {
1482
+ this.log(`Startup lock is ${Math.round(this.lockAgeMs() / 1000)}s old and holder pid ${holderPid} ` + `is an unrelated process (pid recycled), reclaiming`);
664
1483
  this.releaseLock();
665
- return this.acquireLock(depth + 1);
1484
+ return this.acquireLockStrict(true);
666
1485
  }
667
1486
  } catch {
668
- this.log("Cannot read lock file, removing stale lock");
669
- this.releaseLock();
670
- return this.acquireLock(depth + 1);
1487
+ return false;
671
1488
  }
672
1489
  return false;
673
1490
  }
674
- this.log(`Warning: could not acquire startup lock: ${err.message}`);
675
- return true;
1491
+ this.log(`Could not acquire strict startup lock: ${err.message}`);
1492
+ return false;
1493
+ }
1494
+ }
1495
+ lockAgeMs() {
1496
+ try {
1497
+ return Date.now() - statSync(this.stateDir.lockFile).mtimeMs;
1498
+ } catch {
1499
+ return 0;
1500
+ }
1501
+ }
1502
+ isAgentBridgeProcess(pid) {
1503
+ try {
1504
+ const cmd = execFileSync4("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
1505
+ return cmd.includes("agentbridge") || cmd.includes("agent_bridge");
1506
+ } catch {
1507
+ return false;
676
1508
  }
677
1509
  }
678
1510
  releaseLock() {
@@ -680,8 +1512,8 @@ class DaemonLifecycle {
680
1512
  unlinkSync(this.stateDir.lockFile);
681
1513
  } catch {}
682
1514
  }
683
- async kill(gracefulTimeoutMs = 3000) {
684
- const pid = this.readPid();
1515
+ async kill(gracefulTimeoutMs = 3000, pidOverride) {
1516
+ const pid = pidOverride ?? this.readPid();
685
1517
  if (!pid) {
686
1518
  this.log("No daemon pid file found");
687
1519
  this.cleanup();
@@ -723,7 +1555,9 @@ class DaemonLifecycle {
723
1555
  isDaemonProcess(pid) {
724
1556
  try {
725
1557
  const cmd = execFileSync4("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
726
- return cmd.includes("daemon") && (cmd.includes("agentbridge") || cmd.includes("agent_bridge"));
1558
+ const hasDaemonEntry = /(?:^|[\s/\\])[\w.-]*-?daemon\.(?:ts|js)(?:\s|$)/.test(cmd);
1559
+ const hasAgentbridge = cmd.includes("agentbridge") || cmd.includes("agent_bridge");
1560
+ return hasDaemonEntry && hasAgentbridge;
727
1561
  } catch {
728
1562
  return false;
729
1563
  }
@@ -731,7 +1565,15 @@ class DaemonLifecycle {
731
1565
  cleanup() {
732
1566
  this.removePidFile();
733
1567
  this.removeStatusFile();
734
- this.releaseLock();
1568
+ }
1569
+ }
1570
+ async function fetchWithTimeout(url, timeoutMs = HEALTH_FETCH_TIMEOUT_MS) {
1571
+ const controller = new AbortController;
1572
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1573
+ try {
1574
+ return await fetch(url, { signal: controller.signal });
1575
+ } finally {
1576
+ clearTimeout(timer);
735
1577
  }
736
1578
  }
737
1579
  function isProcessAlive(pid) {
@@ -742,87 +1584,910 @@ function isProcessAlive(pid) {
742
1584
  return false;
743
1585
  }
744
1586
  }
745
- var DAEMON_ENTRY, DAEMON_PATH;
1587
+ var DEFAULT_DAEMON_ENTRY, DAEMON_ENTRY, DAEMON_PATH, REUSE_READY_RETRIES, REUSE_READY_DELAY_MS = 250, HEALTH_FETCH_TIMEOUT_MS = 500, LOCK_IDENTITY_GRACE_MS;
746
1588
  var init_daemon_lifecycle = __esm(() => {
747
- DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY ?? "./daemon.ts";
1589
+ init_build_info();
1590
+ DEFAULT_DAEMON_ENTRY = import.meta.url.endsWith(".ts") ? "./daemon.ts" : "./daemon.js";
1591
+ DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY || DEFAULT_DAEMON_ENTRY;
748
1592
  DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
1593
+ REUSE_READY_RETRIES = parsePositiveIntEnv("AGENTBRIDGE_REUSE_READY_RETRIES", 12);
1594
+ LOCK_IDENTITY_GRACE_MS = parsePositiveIntEnv("AGENTBRIDGE_LOCK_IDENTITY_GRACE_MS", 120000);
749
1595
  });
750
1596
 
751
- // src/state-dir.ts
752
- import { mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
753
- import { join as join4 } from "path";
754
- import { homedir as homedir2, platform } from "os";
755
-
756
- class StateDirResolver {
757
- stateDir;
758
- constructor(envOverride) {
759
- const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
760
- if (override) {
761
- this.stateDir = override;
762
- } else if (platform() === "darwin") {
763
- this.stateDir = join4(homedir2(), "Library", "Application Support", "AgentBridge");
764
- } else {
765
- const xdgState = process.env.XDG_STATE_HOME ?? join4(homedir2(), ".local", "state");
766
- this.stateDir = join4(xdgState, "agentbridge");
767
- }
1597
+ // src/pair-registry.ts
1598
+ import { execFileSync as execFileSync5 } from "child_process";
1599
+ import {
1600
+ closeSync as closeSync2,
1601
+ existsSync as existsSync6,
1602
+ fsyncSync,
1603
+ linkSync,
1604
+ lstatSync,
1605
+ mkdirSync as mkdirSync3,
1606
+ openSync as openSync2,
1607
+ readdirSync,
1608
+ readFileSync as readFileSync5,
1609
+ realpathSync,
1610
+ renameSync,
1611
+ rmSync as rmSync2,
1612
+ statSync as statSync2,
1613
+ unlinkSync as unlinkSync2,
1614
+ writeFileSync as writeFileSync5
1615
+ } from "fs";
1616
+ import { createServer } from "net";
1617
+ import { createHash, randomUUID } from "crypto";
1618
+ import { hostname, userInfo } from "os";
1619
+ import { basename, join as join5, resolve as resolve2, sep } from "path";
1620
+ function portsForSlot(slot) {
1621
+ if (!Number.isInteger(slot) || slot < 0) {
1622
+ throw new PairError("PAIR_ID_INVALID", `Invalid slot: ${slot}`);
768
1623
  }
769
- ensure() {
770
- if (!existsSync5(this.stateDir)) {
771
- mkdirSync2(this.stateDir, { recursive: true });
772
- }
1624
+ if (slot > MAX_PAIR_SLOT) {
1625
+ throw new PairError("PAIR_ID_INVALID", `Slot ${slot} exceeds the maximum (${MAX_PAIR_SLOT}); ports would overflow 65535.`, { slot, maxSlot: MAX_PAIR_SLOT });
773
1626
  }
774
- get dir() {
775
- return this.stateDir;
1627
+ const base = PAIR_BASE_PORT + slot * PAIR_SLOT_STRIDE;
1628
+ return { appPort: base, proxyPort: base + 1, controlPort: base + 2 };
1629
+ }
1630
+ function validatePairId(raw) {
1631
+ const id = raw.trim();
1632
+ const deviceBase = id.split(".")[0] ?? "";
1633
+ if (id === "." || id === ".." || !PAIR_ID_REGEX.test(id) || id.endsWith(".") || WINDOWS_RESERVED_RE.test(deviceBase)) {
1634
+ throw new PairError("PAIR_ID_INVALID", `Invalid --pair name: ${JSON.stringify(raw)}. Allowed: letters, digits, "." "_" "-", 1-64 chars ` + `(not "." / ".." / a trailing dot / a reserved name like CON, NUL, COM1).`, { raw });
776
1635
  }
777
- get pidFile() {
778
- return join4(this.stateDir, "daemon.pid");
1636
+ return id;
1637
+ }
1638
+ function derivePairId(cwd, name) {
1639
+ let real;
1640
+ try {
1641
+ real = realpathSync(cwd);
1642
+ } catch {
1643
+ real = cwd;
779
1644
  }
780
- get tuiPidFile() {
781
- return join4(this.stateDir, "codex-tui.pid");
1645
+ const hash = createHash("sha256").update(real).update("\x00").update(name.toLowerCase()).digest("hex").slice(0, 8);
1646
+ const slug = name.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "pair";
1647
+ return `${slug}-${hash}`;
1648
+ }
1649
+ function pickLowestFreeSlot(entries) {
1650
+ const used = new Set(entries.map((e) => e.slot));
1651
+ let slot = 0;
1652
+ while (used.has(slot))
1653
+ slot++;
1654
+ return slot;
1655
+ }
1656
+ function pairsDir(base) {
1657
+ return join5(base, "pairs");
1658
+ }
1659
+ function registryPath(base) {
1660
+ return join5(pairsDir(base), REGISTRY_FILE_NAME);
1661
+ }
1662
+ function readRegistry(base) {
1663
+ const path = registryPath(base);
1664
+ if (!existsSync6(path))
1665
+ return { version: 1, pairs: [] };
1666
+ let parsed;
1667
+ try {
1668
+ parsed = JSON.parse(readFileSync5(path, "utf-8"));
1669
+ } catch (err) {
1670
+ throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry JSON is not parseable at ${path}: ${err.message}`, {
1671
+ path
1672
+ });
782
1673
  }
783
- get lockFile() {
784
- return join4(this.stateDir, "daemon.lock");
1674
+ if (!parsed || typeof parsed !== "object" || parsed.version !== 1 || !Array.isArray(parsed.pairs)) {
1675
+ throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry shape is invalid at ${path}`, { path });
785
1676
  }
786
- get statusFile() {
787
- return join4(this.stateDir, "status.json");
1677
+ const entries = parsed.pairs;
1678
+ const seenSlots = new Set;
1679
+ const seenIds = new Set;
1680
+ for (const e of entries) {
1681
+ const idValid = e && typeof e.pairId === "string" && e.pairId !== "." && e.pairId !== ".." && PAIR_ID_REGEX.test(e.pairId);
1682
+ if (!idValid || !Number.isInteger(e.slot) || e.slot < 0) {
1683
+ throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry has a malformed entry at ${path}`, { path, entry: e });
1684
+ }
1685
+ const lower = e.pairId.toLowerCase();
1686
+ if (seenSlots.has(e.slot) || seenIds.has(lower)) {
1687
+ throw new PairError("PAIR_REGISTRY_CORRUPT", `Registry has duplicate slot/pairId at ${path}`, {
1688
+ path,
1689
+ pairId: e.pairId,
1690
+ slot: e.slot
1691
+ });
1692
+ }
1693
+ seenSlots.add(e.slot);
1694
+ seenIds.add(lower);
788
1695
  }
789
- get portsFile() {
790
- return join4(this.stateDir, "ports.json");
1696
+ return parsed;
1697
+ }
1698
+ function writeRegistry(base, reg) {
1699
+ mkdirSync3(pairsDir(base), { recursive: true });
1700
+ const target = registryPath(base);
1701
+ const tmp = `${target}.tmp.${process.pid}`;
1702
+ const data = JSON.stringify(reg, null, 2) + `
1703
+ `;
1704
+ const fd = openSync2(tmp, "w");
1705
+ try {
1706
+ writeFileSync5(fd, data);
1707
+ fsyncSync(fd);
1708
+ } finally {
1709
+ closeSync2(fd);
791
1710
  }
792
- get logFile() {
793
- return join4(this.stateDir, "agentbridge.log");
1711
+ renameSync(tmp, target);
1712
+ }
1713
+ function lockFilePath(base) {
1714
+ return join5(pairsDir(base), LOCK_FILE_NAME);
1715
+ }
1716
+ function readLockOwner(lockFile) {
1717
+ try {
1718
+ const parsed = JSON.parse(readFileSync5(lockFile, "utf-8"));
1719
+ if (typeof parsed.pid === "number" && typeof parsed.nonce === "string")
1720
+ return parsed;
1721
+ return null;
1722
+ } catch {
1723
+ return null;
794
1724
  }
795
- get codexWrapperLogFile() {
796
- return join4(this.stateDir, "codex-wrapper.log");
1725
+ }
1726
+ function pidLooksAlive(pid) {
1727
+ if (!Number.isInteger(pid) || pid <= 0)
1728
+ return false;
1729
+ try {
1730
+ process.kill(pid, 0);
1731
+ return true;
1732
+ } catch (err) {
1733
+ return err?.code === "EPERM";
797
1734
  }
798
- get killedFile() {
799
- return join4(this.stateDir, "killed");
1735
+ }
1736
+ function lockFileAgeMs(lockFile) {
1737
+ try {
1738
+ return Date.now() - statSync2(lockFile).mtimeMs;
1739
+ } catch {
1740
+ return Number.POSITIVE_INFINITY;
800
1741
  }
801
1742
  }
802
- var init_state_dir = () => {};
803
-
804
- // src/cli/claude.ts
805
- var exports_claude = {};
806
- __export(exports_claude, {
807
- runClaude: () => runClaude,
808
- checkOwnedFlagConflicts: () => checkOwnedFlagConflicts
809
- });
810
- import { spawn as spawn2 } from "child_process";
811
- async function runClaude(args) {
812
- checkOwnedFlagConflicts(args, "agentbridge claude", OWNED_FLAGS);
813
- const stateDir = new StateDirResolver;
814
- const controlPort = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
815
- const lifecycle = new DaemonLifecycle({
816
- stateDir,
817
- controlPort,
818
- log: (msg) => console.error(`[agentbridge] ${msg}`)
819
- });
820
- lifecycle.clearKilled();
821
- const channelEntry = `plugin:${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
1743
+ function safeHostname() {
1744
+ try {
1745
+ return hostname();
1746
+ } catch {
1747
+ return;
1748
+ }
1749
+ }
1750
+ function safeUid() {
1751
+ try {
1752
+ return userInfo().uid;
1753
+ } catch {
1754
+ return;
1755
+ }
1756
+ }
1757
+ function sleep(ms) {
1758
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
1759
+ }
1760
+ function lockIsStale(lockFile) {
1761
+ const owner = readLockOwner(lockFile);
1762
+ if (owner)
1763
+ return !pidLooksAlive(owner.pid);
1764
+ return lockFileAgeMs(lockFile) > ORPHAN_GRACE_MS;
1765
+ }
1766
+ function attemptReclaim(lockFile) {
1767
+ const reclaimLock = `${lockFile}.reclaim`;
1768
+ const myNonce = randomUUID();
1769
+ const ownerJson = JSON.stringify({
1770
+ pid: process.pid,
1771
+ createdAt: Date.now(),
1772
+ nonce: myNonce,
1773
+ hostname: safeHostname(),
1774
+ uid: safeUid()
1775
+ });
1776
+ const tmp = `${reclaimLock}.acq.${process.pid}.${randomUUID()}`;
1777
+ let held = false;
1778
+ try {
1779
+ writeFileSync5(tmp, ownerJson);
1780
+ try {
1781
+ linkSync(tmp, reclaimLock);
1782
+ held = true;
1783
+ } catch (err) {
1784
+ if (err?.code === "EEXIST") {
1785
+ if (lockIsStale(reclaimLock)) {
1786
+ try {
1787
+ unlinkSync2(reclaimLock);
1788
+ } catch {}
1789
+ }
1790
+ return;
1791
+ }
1792
+ throw err;
1793
+ }
1794
+ } finally {
1795
+ try {
1796
+ unlinkSync2(tmp);
1797
+ } catch {}
1798
+ }
1799
+ if (!held)
1800
+ return;
1801
+ try {
1802
+ if (readLockOwner(reclaimLock)?.nonce !== myNonce)
1803
+ return;
1804
+ if (lockIsStale(lockFile)) {
1805
+ try {
1806
+ unlinkSync2(lockFile);
1807
+ } catch {}
1808
+ }
1809
+ } finally {
1810
+ if (readLockOwner(reclaimLock)?.nonce === myNonce) {
1811
+ try {
1812
+ unlinkSync2(reclaimLock);
1813
+ } catch {}
1814
+ }
1815
+ }
1816
+ }
1817
+ async function withRegistryLock(base, fn) {
1818
+ mkdirSync3(pairsDir(base), { recursive: true });
1819
+ const lockFile = lockFilePath(base);
1820
+ const deadline = Date.now() + LOCK_DEADLINE_MS;
1821
+ const myNonce = randomUUID();
1822
+ const ownerJson = JSON.stringify({
1823
+ pid: process.pid,
1824
+ createdAt: Date.now(),
1825
+ nonce: myNonce,
1826
+ hostname: safeHostname(),
1827
+ uid: safeUid()
1828
+ });
1829
+ for (;; ) {
1830
+ const tmp = `${lockFile}.acq.${process.pid}.${randomUUID()}`;
1831
+ let acquired = false;
1832
+ try {
1833
+ writeFileSync5(tmp, ownerJson);
1834
+ try {
1835
+ linkSync(tmp, lockFile);
1836
+ acquired = true;
1837
+ } catch (err) {
1838
+ if (err?.code !== "EEXIST")
1839
+ throw err;
1840
+ }
1841
+ } finally {
1842
+ try {
1843
+ unlinkSync2(tmp);
1844
+ } catch {}
1845
+ }
1846
+ if (acquired) {
1847
+ try {
1848
+ return await fn();
1849
+ } finally {
1850
+ const current = readLockOwner(lockFile);
1851
+ if (!current || current.nonce === myNonce) {
1852
+ try {
1853
+ unlinkSync2(lockFile);
1854
+ } catch {}
1855
+ }
1856
+ }
1857
+ }
1858
+ if (lockIsStale(lockFile)) {
1859
+ attemptReclaim(lockFile);
1860
+ }
1861
+ if (Date.now() >= deadline) {
1862
+ throw new PairError("PAIR_LOCK_TIMEOUT", `Timed out acquiring registry lock at ${lockFile}`, {
1863
+ holderPid: readLockOwner(lockFile)?.pid
1864
+ });
1865
+ }
1866
+ await sleep(25 + Math.floor(Math.random() * 50));
1867
+ }
1868
+ }
1869
+ function isDaemonProcess(pid) {
1870
+ try {
1871
+ const cmd = execFileSync5("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
1872
+ return cmd.includes("daemon") && (cmd.includes("agentbridge") || cmd.includes("agent_bridge"));
1873
+ } catch {
1874
+ return false;
1875
+ }
1876
+ }
1877
+ function detectLegacyRootDaemon(base) {
1878
+ const rootPidFile = join5(base, "daemon.pid");
1879
+ if (!existsSync6(rootPidFile))
1880
+ return null;
1881
+ let pid;
1882
+ try {
1883
+ const raw = readFileSync5(rootPidFile, "utf-8").trim();
1884
+ pid = Number.parseInt(raw, 10);
1885
+ } catch {
1886
+ return null;
1887
+ }
1888
+ if (!Number.isFinite(pid) || !pidLooksAlive(pid) || !isDaemonProcess(pid))
1889
+ return null;
1890
+ return { pid, controlPort: LEGACY_ROOT_CONTROL_PORT };
1891
+ }
1892
+ function probePortFree(port) {
1893
+ return new Promise((resolve3) => {
1894
+ const server = createServer();
1895
+ server.once("error", () => resolve3(false));
1896
+ server.once("listening", () => {
1897
+ server.close(() => resolve3(true));
1898
+ });
1899
+ server.listen(port, "127.0.0.1");
1900
+ });
1901
+ }
1902
+ function pidOnPort(port) {
1903
+ try {
1904
+ const out = execFileSync5("lsof", ["-ti", `:${port}`, "-sTCP:LISTEN"], { encoding: "utf-8" }).trim();
1905
+ const first = out.split(/\s+/)[0];
1906
+ const pid = Number.parseInt(first ?? "", 10);
1907
+ return Number.isFinite(pid) ? pid : undefined;
1908
+ } catch {
1909
+ return;
1910
+ }
1911
+ }
1912
+ async function resolvePair(base, opts) {
1913
+ const hasFlag = opts.pairFlag != null;
1914
+ const name = hasFlag ? validatePairId(opts.pairFlag) : DEFAULT_PAIR_NAME;
1915
+ const pairId = derivePairId(opts.cwd, name);
1916
+ const source = hasFlag ? "flag" : "cwd";
1917
+ const lower = pairId.toLowerCase();
1918
+ const flagLower = name.toLowerCase();
1919
+ const allocation = await withRegistryLock(base, () => {
1920
+ const reg = readRegistry(base);
1921
+ const scoped = reg.pairs.find((p) => p.pairId.toLowerCase() === lower);
1922
+ if (scoped)
1923
+ return { slot: scoped.slot, entry: scoped, isNew: false, matchedRaw: false };
1924
+ if (hasFlag) {
1925
+ const raw = reg.pairs.find((p) => p.pairId.toLowerCase() === flagLower);
1926
+ if (raw) {
1927
+ if (raw.cwd === opts.cwd) {
1928
+ return { slot: raw.slot, entry: raw, isNew: false, matchedRaw: true };
1929
+ }
1930
+ return { crossCwd: raw };
1931
+ }
1932
+ }
1933
+ const newSlot = pickLowestFreeSlot(reg.pairs);
1934
+ if (newSlot === 0) {
1935
+ const legacy = detectLegacyRootDaemon(base);
1936
+ if (legacy) {
1937
+ throw new PairError("PAIR_LEGACY_ROOT_DAEMON", `A pre-multi-pair AgentBridge daemon is running at the legacy location ` + `(pid ${legacy.pid}, control port ${legacy.controlPort}). Run "abg kill" to stop it, then retry \u2014 ` + `your new session would otherwise collide on port ${legacy.controlPort}.`, { pid: legacy.pid, controlPort: legacy.controlPort });
1938
+ }
1939
+ }
1940
+ portsForSlot(newSlot);
1941
+ const newEntry = {
1942
+ pairId,
1943
+ slot: newSlot,
1944
+ cwd: opts.cwd,
1945
+ name,
1946
+ source,
1947
+ createdAt: new Date().toISOString()
1948
+ };
1949
+ writeRegistry(base, { version: 1, pairs: [...reg.pairs, newEntry] });
1950
+ return { slot: newSlot, entry: newEntry, isNew: true, matchedRaw: false };
1951
+ });
1952
+ if ("crossCwd" in allocation) {
1953
+ const raw = allocation.crossCwd;
1954
+ throw new PairError("PAIR_CROSS_CWD", `--pair ${opts.pairFlag ?? name} refers to pair "${raw.pairId}" registered for ${raw.cwd}, ` + `but you are in ${opts.cwd}. A pair is scoped to its directory \u2014 cd into that directory ` + `to use it, or pass a short name to create/use a pair here.`, { pairId: raw.pairId, registeredCwd: raw.cwd, cwd: opts.cwd });
1955
+ }
1956
+ const { slot, entry, isNew, matchedRaw } = allocation;
1957
+ const ports = portsForSlot(slot);
1958
+ if (isNew && opts.probePorts !== false) {
1959
+ for (const port of [ports.appPort, ports.proxyPort, ports.controlPort]) {
1960
+ if (!await probePortFree(port)) {
1961
+ await removeAllocatedPairIfUnchanged(base, pairId, slot);
1962
+ throw new PairError("PAIR_PORTS_BUSY", `Port ${port} (pair "${pairId}", slot ${slot}) is already in use by another process. ` + `Free it or remove the conflicting pair; AgentBridge will not silently move slots.`, { port, slot, pairId, pid: pidOnPort(port) });
1963
+ }
1964
+ }
1965
+ }
1966
+ let warning;
1967
+ if (isNew && hasFlag && /-[0-9a-f]{8}$/i.test(name)) {
1968
+ warning = `--pair ${opts.pairFlag ?? name} looks like a full pair id, but no registered pair matched; ` + `creating a NEW pair named "${name}". Pass a short name (e.g. "main") or run \`abg pairs\` ` + `to see existing pairs.`;
1969
+ }
1970
+ return {
1971
+ pairId: entry.pairId,
1972
+ slot,
1973
+ ports,
1974
+ stateDir: join5(pairsDir(base), entry.pairId),
1975
+ name: entry.name ?? name,
1976
+ entry,
1977
+ warning
1978
+ };
1979
+ }
1980
+ async function removeAllocatedPairIfUnchanged(base, pairId, slot) {
1981
+ await withRegistryLock(base, () => {
1982
+ if (existsSync6(pairDirPath(base, pairId)) || pairDirDaemonAlive(base, pairId))
1983
+ return;
1984
+ const reg = readRegistry(base);
1985
+ const nextPairs = reg.pairs.filter((pair) => !(pair.pairId === pairId && pair.slot === slot));
1986
+ if (nextPairs.length === reg.pairs.length)
1987
+ return;
1988
+ writeRegistry(base, { version: 1, pairs: nextPairs });
1989
+ });
1990
+ }
1991
+ function pairDirPath(base, pairId) {
1992
+ const id = validatePairId(pairId);
1993
+ return join5(pairsDir(base), id);
1994
+ }
1995
+ function removePairDir(base, pairId) {
1996
+ const id = validatePairId(pairId);
1997
+ const root = pairsDir(base);
1998
+ const dir = join5(root, id);
1999
+ const canonicalRoot = resolve2(root);
2000
+ const canonicalDir = resolve2(dir);
2001
+ if (canonicalDir === canonicalRoot || !canonicalDir.startsWith(canonicalRoot + sep)) {
2002
+ throw new PairError("PAIR_ID_INVALID", `Refusing to remove a pair dir outside ${canonicalRoot}: ${canonicalDir}`, { pairId });
2003
+ }
2004
+ assertPairsRootNotSymlinked(root);
2005
+ if (!existsSync6(canonicalDir))
2006
+ return false;
2007
+ rmSync2(canonicalDir, { recursive: true, force: true });
2008
+ return true;
2009
+ }
2010
+ function assertPairsRootNotSymlinked(root) {
2011
+ let stat;
2012
+ try {
2013
+ stat = lstatSync(root);
2014
+ } catch {
2015
+ return;
2016
+ }
2017
+ if (stat.isSymbolicLink()) {
2018
+ throw new PairError("PAIR_ID_INVALID", `Refusing to operate through a symlinked pairs root: ${root}`, { root });
2019
+ }
2020
+ }
2021
+ function listPairDirs(base) {
2022
+ const root = pairsDir(base);
2023
+ if (!existsSync6(root))
2024
+ return [];
2025
+ if (lstatSync(root).isSymbolicLink())
2026
+ return [];
2027
+ return readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
2028
+ }
2029
+ function pairDirDaemonAlive(base, pairId) {
2030
+ const dir = join5(pairsDir(base), pairId);
2031
+ const pids = [];
2032
+ try {
2033
+ const pid = Number.parseInt(readFileSync5(join5(dir, "daemon.pid"), "utf-8").trim(), 10);
2034
+ if (Number.isFinite(pid))
2035
+ pids.push(pid);
2036
+ } catch {}
2037
+ try {
2038
+ const status = JSON.parse(readFileSync5(join5(dir, "status.json"), "utf-8"));
2039
+ if (typeof status?.pid === "number")
2040
+ pids.push(status.pid);
2041
+ } catch {}
2042
+ return pids.some((pid) => pidLooksAlive(pid));
2043
+ }
2044
+ async function removePairEntryAndDir(base, pairId) {
2045
+ const lower = pairId.toLowerCase();
2046
+ return withRegistryLock(base, () => {
2047
+ const reg = readRegistry(base);
2048
+ const found = reg.pairs.find((p) => p.pairId.toLowerCase() === lower) ?? null;
2049
+ if (pairDirDaemonAlive(base, pairId)) {
2050
+ return { entry: found, dirRemoved: false, keptLive: true };
2051
+ }
2052
+ const dirRemoved = removePairDir(base, pairId);
2053
+ if (found) {
2054
+ writeRegistry(base, { version: 1, pairs: reg.pairs.filter((p) => p.pairId.toLowerCase() !== lower) });
2055
+ }
2056
+ return { entry: found, dirRemoved, keptLive: false };
2057
+ });
2058
+ }
2059
+ async function removeUnregisteredPairDir(base, pairId) {
2060
+ const lower = pairId.toLowerCase();
2061
+ return withRegistryLock(base, () => {
2062
+ const reg = readRegistry(base);
2063
+ if (reg.pairs.some((p) => p.pairId.toLowerCase() === lower)) {
2064
+ return { removed: false, reason: "registered" };
2065
+ }
2066
+ if (pairDirDaemonAlive(base, pairId)) {
2067
+ return { removed: false, reason: "live" };
2068
+ }
2069
+ return { removed: removePairDir(base, pairId) };
2070
+ });
2071
+ }
2072
+ var PAIR_BASE_PORT = 4500, PAIR_SLOT_STRIDE = 10, PAIR_ID_REGEX, DEFAULT_PAIR_NAME = "main", LOCK_FILE_NAME = ".registry.lock", REGISTRY_FILE_NAME = "registry.json", LOCK_DEADLINE_MS = 1e4, ORPHAN_GRACE_MS = 3000, LEGACY_ROOT_CONTROL_PORT = 4502, WINDOWS_RESERVED_RE, PairError, MAX_PAIR_SLOT;
2073
+ var init_pair_registry = __esm(() => {
2074
+ PAIR_ID_REGEX = /^[A-Za-z0-9._-]{1,64}$/;
2075
+ WINDOWS_RESERVED_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
2076
+ PairError = class PairError extends Error {
2077
+ code;
2078
+ details;
2079
+ constructor(code, message, details) {
2080
+ super(message);
2081
+ this.name = "PairError";
2082
+ this.code = code;
2083
+ this.details = details;
2084
+ }
2085
+ };
2086
+ MAX_PAIR_SLOT = Math.floor((65535 - 2 - PAIR_BASE_PORT) / PAIR_SLOT_STRIDE);
2087
+ });
2088
+
2089
+ // src/env-guard.ts
2090
+ function normalizeEnvGuardMode(raw, fallback = "fix") {
2091
+ if (raw === "off" || raw === "warn" || raw === "fix" || raw === "strict")
2092
+ return raw;
2093
+ return fallback;
2094
+ }
2095
+ function inspectAgentBridgeEnv(opts) {
2096
+ const env = opts.env ?? process.env;
2097
+ const actualPairId = nonEmpty(env.AGENTBRIDGE_PAIR_ID);
2098
+ const pairName = nonEmpty(env.AGENTBRIDGE_PAIR_NAME) ?? "main";
2099
+ const stateDir = nonEmpty(env.AGENTBRIDGE_STATE_DIR);
2100
+ const baseDir = nonEmpty(env.AGENTBRIDGE_BASE_DIR);
2101
+ const manualOptIn = env.AGENTBRIDGE_MANUAL === "1";
2102
+ const manualRuntimeEnv = !!stateDir || !!nonEmpty(env.AGENTBRIDGE_CONTROL_PORT) || !!nonEmpty(env.CODEX_WS_PORT) || !!nonEmpty(env.CODEX_PROXY_PORT);
2103
+ const expectedPairId = actualPairId ? derivePairId(opts.cwd, pairName) : null;
2104
+ const reasons = [];
2105
+ if (!actualPairId && manualRuntimeEnv && !manualOptIn) {
2106
+ reasons.push("AgentBridge runtime env is set without AGENTBRIDGE_PAIR_ID or AGENTBRIDGE_MANUAL=1");
2107
+ }
2108
+ if (actualPairId && expectedPairId && actualPairId !== expectedPairId) {
2109
+ reasons.push(`AGENTBRIDGE_PAIR_ID=${actualPairId} does not match cwd-derived ${expectedPairId}`);
2110
+ }
2111
+ if (actualPairId && stateDir && !stateDir.endsWith(`/pairs/${actualPairId}`)) {
2112
+ reasons.push(`AGENTBRIDGE_STATE_DIR does not end with /pairs/${actualPairId}`);
2113
+ }
2114
+ if (actualPairId && baseDir && stateDir && !stateDir.startsWith(`${baseDir}/`)) {
2115
+ reasons.push("AGENTBRIDGE_BASE_DIR and AGENTBRIDGE_STATE_DIR disagree");
2116
+ }
2117
+ return {
2118
+ ok: reasons.length === 0,
2119
+ expectedPairId,
2120
+ actualPairId,
2121
+ pairName,
2122
+ reasons
2123
+ };
2124
+ }
2125
+ function guardAgentBridgeEnv(opts) {
2126
+ const env = opts.env ?? process.env;
2127
+ const mode = normalizeEnvGuardMode(opts.mode, "fix");
2128
+ const effectiveMode = mode === "strict" && opts.allowStrict === false ? "fix" : mode;
2129
+ const inspection = inspectAgentBridgeEnv({ cwd: opts.cwd, env });
2130
+ if (effectiveMode === "off" || inspection.ok) {
2131
+ return { ...inspection, action: "none" };
2132
+ }
2133
+ const message = `stale AgentBridge environment detected for ${opts.cwd}: ${inspection.reasons.join("; ")}`;
2134
+ if (effectiveMode === "strict") {
2135
+ throw new Error(message);
2136
+ }
2137
+ opts.log?.(`[agentbridge] ${message}`);
2138
+ if (effectiveMode === "warn") {
2139
+ return { ...inspection, action: "warned" };
2140
+ }
2141
+ for (const key of GENERATED_ENV_KEYS) {
2142
+ delete env[key];
2143
+ }
2144
+ opts.log?.("[agentbridge] cleared stale AgentBridge environment variables");
2145
+ return { ...inspection, action: "fixed" };
2146
+ }
2147
+ function nonEmpty(value) {
2148
+ return value && value.length > 0 ? value : null;
2149
+ }
2150
+ var GENERATED_ENV_KEYS;
2151
+ var init_env_guard = __esm(() => {
2152
+ init_pair_registry();
2153
+ GENERATED_ENV_KEYS = [
2154
+ "AGENTBRIDGE_BASE_DIR",
2155
+ "AGENTBRIDGE_PAIR_ID",
2156
+ "AGENTBRIDGE_PAIR_NAME",
2157
+ "AGENTBRIDGE_STATE_DIR",
2158
+ "AGENTBRIDGE_CONTROL_PORT",
2159
+ "AGENTBRIDGE_MODE",
2160
+ "AGENTBRIDGE_FILTER_MODE",
2161
+ "AGENTBRIDGE_MAX_BUFFERED_MESSAGES",
2162
+ "AGENTBRIDGE_CODEX_TRANSPORT",
2163
+ "CODEX_WS_PORT",
2164
+ "CODEX_PROXY_PORT"
2165
+ ];
2166
+ });
2167
+
2168
+ // src/pair-resolver.ts
2169
+ import { realpathSync as realpathSync2 } from "fs";
2170
+ import { join as join6, resolve as resolve3 } from "path";
2171
+ function computeBaseDir() {
2172
+ return process.env.AGENTBRIDGE_BASE_DIR || process.env.AGENTBRIDGE_STATE_DIR || StateDirResolver.platformBaseDir();
2173
+ }
2174
+ function parsePairFlag(args) {
2175
+ const rest = [];
2176
+ let pairFlag;
2177
+ for (let i = 0;i < args.length; i++) {
2178
+ const a = args[i];
2179
+ if (a === "--pair") {
2180
+ const next = args[i + 1];
2181
+ if (next !== undefined && !next.startsWith("-")) {
2182
+ pairFlag = next;
2183
+ i++;
2184
+ } else {
2185
+ pairFlag = "";
2186
+ }
2187
+ continue;
2188
+ }
2189
+ if (a.startsWith("--pair=")) {
2190
+ pairFlag = a.slice("--pair=".length);
2191
+ continue;
2192
+ }
2193
+ rest.push(a);
2194
+ }
2195
+ return { pairFlag, rest };
2196
+ }
2197
+ function parseKillArgs(args) {
2198
+ let all = false;
2199
+ let pairFlag;
2200
+ for (let i = 0;i < args.length; i++) {
2201
+ const a = args[i];
2202
+ if (a === "all") {
2203
+ all = true;
2204
+ continue;
2205
+ }
2206
+ if (a === "--all") {
2207
+ all = true;
2208
+ continue;
2209
+ }
2210
+ if (a === "--pair") {
2211
+ const next = args[i + 1];
2212
+ if (next !== undefined && !next.startsWith("-")) {
2213
+ pairFlag = next;
2214
+ i++;
2215
+ } else {
2216
+ pairFlag = "";
2217
+ }
2218
+ continue;
2219
+ }
2220
+ if (a.startsWith("--pair=")) {
2221
+ pairFlag = a.slice("--pair=".length);
2222
+ }
2223
+ }
2224
+ return { all, pairFlag };
2225
+ }
2226
+ async function applyPairEnv(opts) {
2227
+ const explicitEnv = !!process.env.AGENTBRIDGE_STATE_DIR || !!process.env.AGENTBRIDGE_CONTROL_PORT || !!process.env.CODEX_WS_PORT || !!process.env.CODEX_PROXY_PORT;
2228
+ if (opts.pairFlag === undefined && explicitEnv && process.env.AGENTBRIDGE_MANUAL === "1") {
2229
+ const stateDir = new StateDirResolver;
2230
+ const controlPort = Number.parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
2231
+ const appPort = Number.parseInt(process.env.CODEX_WS_PORT ?? "4500", 10);
2232
+ const proxyPort = Number.parseInt(process.env.CODEX_PROXY_PORT ?? "4501", 10);
2233
+ return {
2234
+ pairId: "(manual)",
2235
+ slot: null,
2236
+ ports: { appPort, proxyPort, controlPort },
2237
+ stateDir,
2238
+ name: "(manual)",
2239
+ manual: true
2240
+ };
2241
+ }
2242
+ const base = computeBaseDir();
2243
+ const resolved = await resolvePair(base, { pairFlag: opts.pairFlag, cwd: process.cwd() });
2244
+ process.env.AGENTBRIDGE_BASE_DIR = base;
2245
+ process.env.AGENTBRIDGE_PAIR_ID = resolved.pairId;
2246
+ process.env.AGENTBRIDGE_PAIR_NAME = resolved.name;
2247
+ process.env.AGENTBRIDGE_STATE_DIR = resolved.stateDir;
2248
+ process.env.AGENTBRIDGE_CONTROL_PORT = String(resolved.ports.controlPort);
2249
+ process.env.CODEX_WS_PORT = String(resolved.ports.appPort);
2250
+ process.env.CODEX_PROXY_PORT = String(resolved.ports.proxyPort);
2251
+ return {
2252
+ pairId: resolved.pairId,
2253
+ slot: resolved.slot,
2254
+ ports: resolved.ports,
2255
+ stateDir: new StateDirResolver(resolved.stateDir),
2256
+ name: resolved.name,
2257
+ manual: false,
2258
+ warning: resolved.warning
2259
+ };
2260
+ }
2261
+ function resolvePairReadOnly(pairFlag) {
2262
+ const explicitEnv = !!process.env.AGENTBRIDGE_STATE_DIR || !!process.env.AGENTBRIDGE_CONTROL_PORT || !!process.env.CODEX_WS_PORT || !!process.env.CODEX_PROXY_PORT;
2263
+ if (pairFlag === undefined && explicitEnv && process.env.AGENTBRIDGE_MANUAL === "1") {
2264
+ return {
2265
+ registered: true,
2266
+ pair: {
2267
+ pairId: "(manual)",
2268
+ slot: null,
2269
+ ports: {
2270
+ appPort: Number.parseInt(process.env.CODEX_WS_PORT ?? "4500", 10),
2271
+ proxyPort: Number.parseInt(process.env.CODEX_PROXY_PORT ?? "4501", 10),
2272
+ controlPort: Number.parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10)
2273
+ },
2274
+ stateDir: new StateDirResolver,
2275
+ name: "(manual)",
2276
+ manual: true
2277
+ }
2278
+ };
2279
+ }
2280
+ const base = computeBaseDir();
2281
+ const cwd = process.cwd();
2282
+ const name = pairFlag ?? "main";
2283
+ let entry = null;
2284
+ try {
2285
+ entry = findPairForFlag(base, cwd, name);
2286
+ } catch (err) {
2287
+ if (err instanceof PairError && err.code === "PAIR_ID_INVALID")
2288
+ throw err;
2289
+ }
2290
+ if (entry) {
2291
+ return {
2292
+ registered: true,
2293
+ pair: {
2294
+ pairId: entry.pairId,
2295
+ slot: entry.slot,
2296
+ ports: portsForEntry(entry),
2297
+ stateDir: new StateDirResolver(join6(base, "pairs", entry.pairId)),
2298
+ name: entry.name ?? name,
2299
+ manual: false
2300
+ }
2301
+ };
2302
+ }
2303
+ const pairId = derivePairId(cwd, name);
2304
+ return {
2305
+ registered: false,
2306
+ pair: {
2307
+ pairId,
2308
+ slot: null,
2309
+ ports: { appPort: 0, proxyPort: 0, controlPort: 0 },
2310
+ stateDir: new StateDirResolver(join6(base, "pairs", pairId)),
2311
+ name,
2312
+ manual: false
2313
+ }
2314
+ };
2315
+ }
2316
+ function listPairs(base) {
2317
+ return readRegistry(base).pairs;
2318
+ }
2319
+ function listPairsForCwd(base, cwd) {
2320
+ const canonicalCwd = canonicalizeCwd(cwd);
2321
+ return listPairs(base).filter((pair) => canonicalizeCwd(pair.cwd) === canonicalCwd);
2322
+ }
2323
+ function findPair(base, pairId) {
2324
+ const lower = pairId.toLowerCase();
2325
+ return readRegistry(base).pairs.find((p) => p.pairId.toLowerCase() === lower) ?? null;
2326
+ }
2327
+ function findPairForFlag(base, cwd, flag) {
2328
+ const name = validatePairId(flag);
2329
+ const scopedId = derivePairId(cwd, name);
2330
+ const scoped = findPair(base, scopedId);
2331
+ if (scoped)
2332
+ return scoped;
2333
+ const raw = findPair(base, name);
2334
+ return raw && raw.cwd === cwd ? raw : null;
2335
+ }
2336
+ function portsForEntry(entry) {
2337
+ return portsForSlot(entry.slot);
2338
+ }
2339
+ function canonicalizeCwd(cwd) {
2340
+ const absolute = resolve3(cwd);
2341
+ try {
2342
+ return realpathSync2.native(absolute);
2343
+ } catch {
2344
+ try {
2345
+ return realpathSync2(absolute);
2346
+ } catch {
2347
+ return absolute;
2348
+ }
2349
+ }
2350
+ }
2351
+ var init_pair_resolver = __esm(() => {
2352
+ init_state_dir();
2353
+ init_pair_registry();
2354
+ });
2355
+
2356
+ // src/trace-log.ts
2357
+ import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
2358
+ import { join as join7 } from "path";
2359
+ function pickRelevantEnv(env) {
2360
+ const picked = {};
2361
+ for (const [key, value] of Object.entries(env)) {
2362
+ if (!RELEVANT_ENV_RE.test(key))
2363
+ continue;
2364
+ picked[key] = SECRET_KEY_RE.test(key) && value !== undefined ? "<redacted>" : value;
2365
+ }
2366
+ return picked;
2367
+ }
2368
+ function redactArgv(argv) {
2369
+ const redacted = [];
2370
+ let redactNext = false;
2371
+ for (const arg of argv) {
2372
+ if (redactNext) {
2373
+ redacted.push("<redacted>");
2374
+ redactNext = false;
2375
+ continue;
2376
+ }
2377
+ if (SECRET_ARG_RE.test(arg)) {
2378
+ if (arg.includes("=")) {
2379
+ const [key] = arg.split("=", 1);
2380
+ redacted.push(`${key}=<redacted>`);
2381
+ } else {
2382
+ redacted.push(arg);
2383
+ redactNext = true;
2384
+ }
2385
+ continue;
2386
+ }
2387
+ redacted.push(arg);
2388
+ }
2389
+ return redacted;
2390
+ }
2391
+ function traceLogPath(cwd, timestamp) {
2392
+ const day = timestamp.slice(0, 10);
2393
+ return join7(cwd, ".agentbridge", "logs", `trace-${day}.jsonl`);
2394
+ }
2395
+ function appendTraceEvent(input) {
2396
+ const timestamp = input.timestamp ?? new Date().toISOString();
2397
+ const path = traceLogPath(input.cwd, timestamp);
2398
+ const event = {
2399
+ timestamp,
2400
+ event: input.event,
2401
+ cwd: input.cwd,
2402
+ pid: input.pid ?? process.pid,
2403
+ ...input.argv ? { argv: redactArgv(input.argv) } : {},
2404
+ ...input.env ? { env: pickRelevantEnv(input.env) } : {},
2405
+ ...input.data ? { data: redactData(input.data) } : {}
2406
+ };
2407
+ mkdirSync4(join7(input.cwd, ".agentbridge", "logs"), { recursive: true });
2408
+ appendFileSync(path, JSON.stringify(event) + `
2409
+ `, "utf-8");
2410
+ return path;
2411
+ }
2412
+ function isEnvSnapshot(key, value) {
2413
+ return /env$/i.test(key) && !!value && typeof value === "object" && !Array.isArray(value);
2414
+ }
2415
+ function redactData(value, key = "") {
2416
+ if (typeof value === "string") {
2417
+ return SECRET_KEY_RE.test(key) ? "<redacted>" : value;
2418
+ }
2419
+ if (Array.isArray(value)) {
2420
+ return value.map((item) => redactData(item, key));
2421
+ }
2422
+ if (value && typeof value === "object") {
2423
+ const redacted = {};
2424
+ for (const [childKey, childValue] of Object.entries(value)) {
2425
+ if (SECRET_KEY_RE.test(childKey)) {
2426
+ redacted[childKey] = "<redacted>";
2427
+ } else if (isEnvSnapshot(childKey, childValue)) {
2428
+ redacted[childKey] = pickRelevantEnv(childValue);
2429
+ } else {
2430
+ redacted[childKey] = redactData(childValue, childKey);
2431
+ }
2432
+ }
2433
+ return redacted;
2434
+ }
2435
+ return value;
2436
+ }
2437
+ var SECRET_KEY_RE, SECRET_ARG_RE, RELEVANT_ENV_RE;
2438
+ var init_trace_log = __esm(() => {
2439
+ SECRET_KEY_RE = /(token|secret|password|passwd|api[_-]?key|auth|cookie|session)/i;
2440
+ SECRET_ARG_RE = /^--?(?:token|secret|password|passwd|apikey|api-key|api_key|auth|cookie|session)(?:=.*)?$/i;
2441
+ RELEVANT_ENV_RE = /^(AGENTBRIDGE_|CODEX_)/;
2442
+ });
2443
+
2444
+ // src/cli/claude.ts
2445
+ var exports_claude = {};
2446
+ __export(exports_claude, {
2447
+ runClaude: () => runClaude,
2448
+ checkOwnedFlagConflicts: () => checkOwnedFlagConflicts
2449
+ });
2450
+ import { spawn as spawn2 } from "child_process";
2451
+ async function runClaude(args) {
2452
+ const originalEnv = { ...process.env };
2453
+ const envGuardResult = guardAgentBridgeEnv({
2454
+ cwd: process.cwd(),
2455
+ env: process.env,
2456
+ mode: normalizeEnvGuardMode(process.env.AGENTBRIDGE_ENV_GUARD),
2457
+ allowStrict: true,
2458
+ log: (msg) => console.error(msg)
2459
+ });
2460
+ const { pairFlag, rest } = parsePairFlag(args);
2461
+ checkOwnedFlagConflicts(rest, "agentbridge claude", OWNED_FLAGS);
2462
+ let pair;
2463
+ try {
2464
+ pair = await applyPairEnv({ pairFlag });
2465
+ } catch (err) {
2466
+ console.error(`[agentbridge] ${err.message}`);
2467
+ process.exit(1);
2468
+ }
2469
+ if (pair.warning)
2470
+ console.error(`[agentbridge] \u26A0\uFE0F ${pair.warning}`);
2471
+ if (process.env.AGENTBRIDGE_TRACE === "1") {
2472
+ traceCliStart("cli.claude.start", args, originalEnv, envGuardResult.action, pair);
2473
+ }
2474
+ const stateDir = pair.stateDir;
2475
+ const controlPort = pair.ports.controlPort;
2476
+ const lifecycle = new DaemonLifecycle({
2477
+ stateDir,
2478
+ controlPort,
2479
+ log: (msg) => console.error(`[agentbridge] ${msg}`)
2480
+ });
2481
+ if (!pair.manual) {
2482
+ console.error(`[agentbridge] pair "${pair.pairId}" (slot ${pair.slot}) \u2014 control :${controlPort}, ` + `codex :${pair.ports.appPort}/:${pair.ports.proxyPort}`);
2483
+ }
2484
+ await assertPairNotLive(lifecycle, pair);
2485
+ lifecycle.clearKilled();
2486
+ const channelEntry = `plugin:${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
822
2487
  const fullArgs = [
823
2488
  "--dangerously-load-development-channels",
824
2489
  channelEntry,
825
- ...args
2490
+ ...rest
826
2491
  ];
827
2492
  const child = spawn2("claude", fullArgs, {
828
2493
  stdio: "inherit",
@@ -841,6 +2506,62 @@ async function runClaude(args) {
841
2506
  process.exit(1);
842
2507
  });
843
2508
  }
2509
+ function traceCliStart(event, args, originalEnv, envGuardAction, pair) {
2510
+ try {
2511
+ appendTraceEvent({
2512
+ cwd: process.cwd(),
2513
+ event,
2514
+ pid: process.pid,
2515
+ argv: ["agentbridge", "claude", ...args],
2516
+ env: process.env,
2517
+ data: {
2518
+ originalEnv: pickRelevantEnv(originalEnv),
2519
+ effectiveEnv: pickRelevantEnv(process.env),
2520
+ envGuardAction,
2521
+ pairId: pair.pairId,
2522
+ pairName: pair.name,
2523
+ manual: pair.manual,
2524
+ slot: pair.slot,
2525
+ stateDir: pair.stateDir.dir,
2526
+ ports: pair.ports,
2527
+ build: BUILD_INFO
2528
+ }
2529
+ });
2530
+ } catch {}
2531
+ }
2532
+ async function assertPairNotLive(lifecycle, pair) {
2533
+ let healthy = false;
2534
+ try {
2535
+ healthy = await lifecycle.isHealthy();
2536
+ } catch {
2537
+ return;
2538
+ }
2539
+ if (!healthy)
2540
+ return;
2541
+ const client = new DaemonClient(lifecycle.controlWsUrl);
2542
+ let incumbent;
2543
+ try {
2544
+ await client.connect();
2545
+ const daemonProbeMs = parsePositiveIntEnv("AGENTBRIDGE_LIVENESS_PROBE_TIMEOUT_MS", 3000);
2546
+ incumbent = await client.probeIncumbent(daemonProbeMs + 2500);
2547
+ } catch {
2548
+ return;
2549
+ } finally {
2550
+ try {
2551
+ await client.disconnect();
2552
+ } catch {}
2553
+ }
2554
+ if (incumbent.connected && incumbent.alive) {
2555
+ const name = pair.name;
2556
+ console.error(`[agentbridge] Pair "${name}" in ${process.cwd()} already has an active Claude session.`);
2557
+ console.error(`[agentbridge] Refusing to open a second one in the same pair.`);
2558
+ console.error(`[agentbridge]`);
2559
+ console.error(`[agentbridge] \u2022 Use that existing session, or`);
2560
+ console.error(`[agentbridge] \u2022 Start a different pair: abg --pair <other-name> claude`);
2561
+ console.error(`[agentbridge] \u2022 If that session is actually dead, take it over with: abg --pair ${name} kill`);
2562
+ process.exit(1);
2563
+ }
2564
+ }
844
2565
  function checkOwnedFlagConflicts(args, commandName, ownedFlags) {
845
2566
  for (const flag of ownedFlags) {
846
2567
  if (args.some((a) => a === flag || a.startsWith(`${flag}=`))) {
@@ -861,17 +2582,201 @@ function checkOwnedFlagConflicts(args, commandName, ownedFlags) {
861
2582
  var OWNED_FLAGS;
862
2583
  var init_claude = __esm(() => {
863
2584
  init_cli();
2585
+ init_daemon_client();
864
2586
  init_daemon_lifecycle();
865
- init_state_dir();
2587
+ init_build_info();
2588
+ init_env_guard();
2589
+ init_pair_resolver();
2590
+ init_trace_log();
866
2591
  OWNED_FLAGS = ["--channels", "--dangerously-load-development-channels"];
867
2592
  });
868
2593
 
869
- // src/stderr-ring-buffer.ts
870
- class StderrRingBuffer {
871
- maxBytes;
872
- chunks = [];
2594
+ // src/agents-contract.ts
2595
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
2596
+ import { join as join8 } from "path";
2597
+ function checkAgentsMdContract(cwd) {
2598
+ const path = join8(cwd, "AGENTS.md");
2599
+ const exists = existsSync7(path);
2600
+ let content = "";
2601
+ if (exists) {
2602
+ try {
2603
+ content = readFileSync6(path, "utf-8");
2604
+ } catch {
2605
+ return {
2606
+ fresh: false,
2607
+ exists,
2608
+ message: "AGENTS.md could not be read; re-run `abg init` to refresh the AgentBridge contract."
2609
+ };
2610
+ }
2611
+ }
2612
+ const fresh = isFreshAgentsMdContract(content);
2613
+ if (fresh) {
2614
+ return { fresh: true, exists, message: "AGENTS.md AgentBridge contract is up to date" };
2615
+ }
2616
+ return {
2617
+ fresh: false,
2618
+ exists,
2619
+ message: exists ? "AGENTS.md is missing the current AgentBridge contract; re-run `abg init` to refresh it." : "AGENTS.md not found; re-run `abg init` to write the AgentBridge contract."
2620
+ };
2621
+ }
2622
+ function isFreshAgentsMdContract(content) {
2623
+ if (!content.includes(`<!-- ${MARKER_ID}:start -->`))
2624
+ return false;
2625
+ return content.includes("transparent proxy") && content.includes("Do not") && content.includes("sendToClaude") && content.includes("Git operations") && content.includes("Implementer, Executor, Verifier");
2626
+ }
2627
+ var init_agents_contract = () => {};
2628
+
2629
+ // src/wrapper-exit-observability.ts
2630
+ import { readFileSync as readFileSync7, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
2631
+ import { join as join9 } from "path";
2632
+ function discoverNativeChildPid(launcherPid, run) {
2633
+ try {
2634
+ const out = run("pgrep", ["-P", String(launcherPid)]);
2635
+ const first = out.split(/\r?\n/).map((line) => line.trim()).find((line) => /^\d+$/.test(line));
2636
+ return first ? Number(first) : null;
2637
+ } catch {
2638
+ return null;
2639
+ }
2640
+ }
2641
+ function readTurnInProgress(statusFilePath, read = (p) => readFileSync7(p, "utf-8"), isPidAlive = defaultIsPidAlive) {
2642
+ try {
2643
+ const status = JSON.parse(read(statusFilePath));
2644
+ if (typeof status.turnInProgress !== "boolean")
2645
+ return null;
2646
+ if (typeof status.pid === "number" && !isPidAlive(status.pid))
2647
+ return null;
2648
+ return status.turnInProgress;
2649
+ } catch {
2650
+ return null;
2651
+ }
2652
+ }
2653
+ function defaultIsPidAlive(pid) {
2654
+ if (pid <= 0)
2655
+ return false;
2656
+ try {
2657
+ process.kill(pid, 0);
2658
+ return true;
2659
+ } catch (err) {
2660
+ return err.code === "EPERM";
2661
+ }
2662
+ }
2663
+ function refineCleanExitClassification(turnInProgress) {
2664
+ if (turnInProgress === true)
2665
+ return "exit_0_during_turn";
2666
+ if (turnInProgress === false)
2667
+ return "exit_0_idle";
2668
+ return "exit_0_turn_unknown";
2669
+ }
2670
+ function findCodexSqliteLog(codexHome, fs = { readdir: readdirSync2, stat: statSync3 }) {
2671
+ try {
2672
+ const entries = fs.readdir(codexHome).filter((name) => /^logs.*\.sqlite$/.test(String(name)));
2673
+ let best = null;
2674
+ for (const name of entries) {
2675
+ const path = join9(codexHome, String(name));
2676
+ try {
2677
+ const mtime = fs.stat(path).mtimeMs;
2678
+ if (!best || mtime > best.mtime)
2679
+ best = { path, mtime };
2680
+ } catch {}
2681
+ }
2682
+ return best?.path ?? null;
2683
+ } catch {
2684
+ return null;
2685
+ }
2686
+ }
2687
+ function codexSqliteTailCommand(dbPath, nativePid, limit = 80) {
2688
+ const rows = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : 80;
2689
+ const pid = Math.max(0, Math.floor(nativePid));
2690
+ const sql = `select ts, level, target, substr(feedback_log_body,1,300) from logs ` + `where process_uuid like 'pid:${pid}:%' order by id desc limit ${rows};`;
2691
+ return { cmd: "sqlite3", args: ["-readonly", dbPath, sql] };
2692
+ }
2693
+ function captureTuiLogTail(options) {
2694
+ if (options.nativePid === null) {
2695
+ return "(native child pid unknown \u2014 tui log tail unavailable)";
2696
+ }
2697
+ const db = findCodexSqliteLog(options.codexHome);
2698
+ if (!db) {
2699
+ return "(no codex sqlite log database found)";
2700
+ }
2701
+ try {
2702
+ const { cmd, args } = codexSqliteTailCommand(db, options.nativePid);
2703
+ const out = options.run(cmd, args).trim();
2704
+ return out.length > 0 ? out : `(no log rows for pid ${options.nativePid} in ${db})`;
2705
+ } catch (err) {
2706
+ return `(tui log tail capture failed: ${err instanceof Error ? err.message : String(err)})`;
2707
+ }
2708
+ }
2709
+ var init_wrapper_exit_observability = () => {};
2710
+
2711
+ // src/pair-command.ts
2712
+ function pairScopedCommand(cmd) {
2713
+ const pairId = process.env.AGENTBRIDGE_PAIR_ID;
2714
+ if (!pairId)
2715
+ return `agentbridge ${cmd}`;
2716
+ let selector = process.env.AGENTBRIDGE_PAIR_NAME;
2717
+ if (!selector) {
2718
+ try {
2719
+ selector = findPair(computeBaseDir(), pairId)?.name || pairId;
2720
+ } catch {
2721
+ selector = pairId;
2722
+ }
2723
+ }
2724
+ return `agentbridge --pair ${selector} ${cmd}`;
2725
+ }
2726
+ var init_pair_command = __esm(() => {
2727
+ init_pair_resolver();
2728
+ });
2729
+
2730
+ // src/rotating-log.ts
2731
+ import { appendFileSync as appendFileSync2, existsSync as existsSync8, renameSync as renameSync2, statSync as statSync4, unlinkSync as unlinkSync3 } from "fs";
2732
+ import { dirname as dirname2 } from "path";
2733
+ function appendRotatingLog(path, content, options = {}) {
2734
+ const maxBytes = options.maxBytes ?? positiveIntFromEnv("AGENTBRIDGE_LOG_MAX_BYTES", DEFAULT_MAX_BYTES);
2735
+ const keep = options.keep ?? positiveIntFromEnv("AGENTBRIDGE_LOG_ROTATE_KEEP", DEFAULT_KEEP);
2736
+ if (!existsSync8(dirname2(path)))
2737
+ return;
2738
+ rotateIfNeeded(path, Buffer.byteLength(content), maxBytes, keep);
2739
+ appendFileSync2(path, content, "utf-8");
2740
+ }
2741
+ function positiveIntFromEnv(name, fallback) {
2742
+ const value = process.env[name];
2743
+ if (!value)
2744
+ return fallback;
2745
+ const parsed = Number(value);
2746
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
2747
+ }
2748
+ function rotateIfNeeded(path, incomingBytes, maxBytes, keep) {
2749
+ if (!Number.isFinite(maxBytes) || maxBytes <= 0 || keep <= 0)
2750
+ return;
2751
+ if (!existsSync8(path))
2752
+ return;
2753
+ const size = statSync4(path).size;
2754
+ if (size + incomingBytes <= maxBytes)
2755
+ return;
2756
+ for (let index = keep;index >= 1; index--) {
2757
+ const current = `${path}.${index}`;
2758
+ const next = `${path}.${index + 1}`;
2759
+ if (!existsSync8(current))
2760
+ continue;
2761
+ if (index === keep) {
2762
+ unlinkSync3(current);
2763
+ } else {
2764
+ renameSync2(current, next);
2765
+ }
2766
+ }
2767
+ renameSync2(path, `${path}.1`);
2768
+ }
2769
+ var DEFAULT_MAX_BYTES, DEFAULT_KEEP = 3;
2770
+ var init_rotating_log = __esm(() => {
2771
+ DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
2772
+ });
2773
+
2774
+ // src/stderr-ring-buffer.ts
2775
+ class StderrRingBuffer {
2776
+ maxBytes;
2777
+ chunks = [];
873
2778
  bytes = 0;
874
- constructor(maxBytes = DEFAULT_MAX_BYTES) {
2779
+ constructor(maxBytes = DEFAULT_MAX_BYTES2) {
875
2780
  this.maxBytes = maxBytes;
876
2781
  if (maxBytes <= 0) {
877
2782
  throw new Error("StderrRingBuffer maxBytes must be positive");
@@ -909,36 +2814,270 @@ class StderrRingBuffer {
909
2814
  return this.bytes;
910
2815
  }
911
2816
  }
912
- var DEFAULT_MAX_BYTES;
2817
+ var DEFAULT_MAX_BYTES2;
913
2818
  var init_stderr_ring_buffer = __esm(() => {
914
- DEFAULT_MAX_BYTES = 64 * 1024;
2819
+ DEFAULT_MAX_BYTES2 = 64 * 1024;
915
2820
  });
916
2821
 
2822
+ // src/thread-state.ts
2823
+ import {
2824
+ existsSync as existsSync9,
2825
+ mkdirSync as mkdirSync5,
2826
+ readdirSync as readdirSync3,
2827
+ readFileSync as readFileSync8,
2828
+ renameSync as renameSync3,
2829
+ writeFileSync as writeFileSync6
2830
+ } from "fs";
2831
+ import { homedir as homedir3 } from "os";
2832
+ import { basename as basename2, dirname as dirname3, join as join10 } from "path";
2833
+ function nowIso() {
2834
+ return new Date().toISOString();
2835
+ }
2836
+ function codexHome(env = process.env) {
2837
+ return env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : join10(homedir3(), ".codex");
2838
+ }
2839
+ function atomicWriteJson(path, value) {
2840
+ mkdirSync5(dirname3(path), { recursive: true });
2841
+ const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
2842
+ writeFileSync6(tmp, JSON.stringify(value, null, 2) + `
2843
+ `, "utf-8");
2844
+ renameSync3(tmp, path);
2845
+ }
2846
+ function readRawCurrentThread(stateDir) {
2847
+ try {
2848
+ const parsed = JSON.parse(readFileSync8(stateDir.currentThreadFile, "utf-8"));
2849
+ if (parsed?.version === 1 && typeof parsed.threadId === "string" && parsed.threadId.length > 0 && (parsed.status === "pending" || parsed.status === "current") && typeof parsed.cwd === "string") {
2850
+ return parsed;
2851
+ }
2852
+ } catch {}
2853
+ return null;
2854
+ }
2855
+ function findCodexRolloutFile(threadId, env = process.env, maxEntries = 20000) {
2856
+ const sessionsDir = join10(codexHome(env), "sessions");
2857
+ if (!threadId || !existsSync9(sessionsDir))
2858
+ return null;
2859
+ const exactName = `rollout-${threadId}.jsonl`;
2860
+ const stack = [sessionsDir];
2861
+ let visited = 0;
2862
+ while (stack.length > 0 && visited < maxEntries) {
2863
+ const dir = stack.pop();
2864
+ let entries;
2865
+ try {
2866
+ entries = readdirSync3(dir, { withFileTypes: true });
2867
+ } catch {
2868
+ continue;
2869
+ }
2870
+ for (const entry of entries) {
2871
+ visited++;
2872
+ const path = join10(dir, entry.name);
2873
+ if (entry.isDirectory()) {
2874
+ stack.push(path);
2875
+ continue;
2876
+ }
2877
+ if (!entry.isFile())
2878
+ continue;
2879
+ const name = basename2(entry.name);
2880
+ if (name === exactName || name.startsWith("rollout-") && name.endsWith(".jsonl") && name.includes(threadId)) {
2881
+ return path;
2882
+ }
2883
+ }
2884
+ }
2885
+ return null;
2886
+ }
2887
+ function readUsableCurrentThread(identity, env = process.env) {
2888
+ const state = readRawCurrentThread(identity.stateDir);
2889
+ if (!state)
2890
+ return null;
2891
+ if (state.status !== "current")
2892
+ return null;
2893
+ if (state.pairId !== identity.pairId)
2894
+ return null;
2895
+ if (state.cwd !== identity.cwd)
2896
+ return null;
2897
+ if (state.rolloutPath && existsSync9(state.rolloutPath))
2898
+ return state;
2899
+ const rolloutPath = findCodexRolloutFile(state.threadId, env);
2900
+ if (!rolloutPath)
2901
+ return null;
2902
+ const repaired = {
2903
+ ...state,
2904
+ rolloutPath,
2905
+ rolloutVerifiedAt: nowIso(),
2906
+ updatedAt: nowIso()
2907
+ };
2908
+ atomicWriteJson(identity.stateDir.currentThreadFile, repaired);
2909
+ return repaired;
2910
+ }
2911
+ var init_thread_state = () => {};
2912
+
2913
+ // src/process-lifecycle.ts
2914
+ import { execFileSync as execFileSync6 } from "child_process";
2915
+ import { basename as basename3 } from "path";
2916
+ function parsePsProcessList(output) {
2917
+ const entries = [];
2918
+ for (const line of output.split(/\r?\n/)) {
2919
+ const match = line.match(/^\s*(\d+)\s+(.+?)\s*$/);
2920
+ if (!match)
2921
+ continue;
2922
+ const pid = Number.parseInt(match[1], 10);
2923
+ if (!Number.isFinite(pid))
2924
+ continue;
2925
+ entries.push({ pid, command: match[2] });
2926
+ }
2927
+ return entries;
2928
+ }
2929
+ function invokesCodexBinary(command) {
2930
+ const tokens = command.trim().split(/\s+/);
2931
+ const exe = tokens[0] ? basename3(tokens[0]) : "";
2932
+ if (exe === "codex")
2933
+ return true;
2934
+ if ((exe === "node" || exe === "bun") && tokens[1]) {
2935
+ return basename3(tokens[1]) === "codex";
2936
+ }
2937
+ return false;
2938
+ }
2939
+ function commandMatchesManagedCodexTui(command, proxyUrl) {
2940
+ if (!invokesCodexBinary(command))
2941
+ return false;
2942
+ if (!command.includes("tui_app_server"))
2943
+ return false;
2944
+ const remoteUrl = extractRemoteUrl(command);
2945
+ if (!remoteUrl)
2946
+ return false;
2947
+ if (!proxyUrl)
2948
+ return true;
2949
+ return remoteTargetsProxy(remoteUrl, proxyUrl);
2950
+ }
2951
+ function findManagedCodexTuiProcessesFromList(processes, proxyUrl) {
2952
+ return processes.filter((entry) => commandMatchesManagedCodexTui(entry.command, proxyUrl));
2953
+ }
2954
+ function findManagedCodexTuiProcesses(proxyUrl) {
2955
+ try {
2956
+ const output = execFileSync6("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
2957
+ return findManagedCodexTuiProcessesFromList(parsePsProcessList(output), proxyUrl).filter((entry) => entry.pid !== process.pid);
2958
+ } catch {
2959
+ return [];
2960
+ }
2961
+ }
2962
+ function listManagedCodexTuiProcessesFromList(processes) {
2963
+ return processes.filter((entry) => commandMatchesManagedCodexTui(entry.command)).map((entry) => ({ ...entry, remoteUrl: extractRemoteUrl(entry.command) }));
2964
+ }
2965
+ function listManagedCodexTuiProcesses() {
2966
+ try {
2967
+ const output = execFileSync6("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
2968
+ return listManagedCodexTuiProcessesFromList(parsePsProcessList(output)).filter((entry) => entry.pid !== process.pid);
2969
+ } catch {
2970
+ return [];
2971
+ }
2972
+ }
2973
+ function listBridgeFrontendProcessesFromList(processes) {
2974
+ return processes.filter((entry) => /(?:^|[\s/\\])bridge-server\.js(?:\s|$)/.test(entry.command) && (entry.command.includes("agentbridge") || entry.command.includes("agent_bridge")));
2975
+ }
2976
+ function listBridgeFrontendProcesses() {
2977
+ try {
2978
+ const output = execFileSync6("ps", ["-axo", "pid=,command="], { encoding: "utf-8" });
2979
+ return listBridgeFrontendProcessesFromList(parsePsProcessList(output)).filter((entry) => entry.pid !== process.pid);
2980
+ } catch {
2981
+ return [];
2982
+ }
2983
+ }
2984
+ function isProcessAlive2(pid) {
2985
+ try {
2986
+ process.kill(pid, 0);
2987
+ return true;
2988
+ } catch {
2989
+ return false;
2990
+ }
2991
+ }
2992
+ function commandForPid(pid) {
2993
+ try {
2994
+ return execFileSync6("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
2995
+ } catch {
2996
+ return null;
2997
+ }
2998
+ }
2999
+ function terminateProcessSync(pid, options = {}) {
3000
+ const gracefulTimeoutMs = options.gracefulTimeoutMs ?? 2000;
3001
+ const target = options.processGroup && process.platform !== "win32" ? -pid : pid;
3002
+ const label = options.processGroup && process.platform !== "win32" ? `process group ${pid}` : `pid ${pid}`;
3003
+ try {
3004
+ process.kill(target, "SIGTERM");
3005
+ options.log?.(`Sent SIGTERM to ${label}`);
3006
+ } catch {
3007
+ return !isProcessAlive2(pid);
3008
+ }
3009
+ if (waitForExitSync(pid, gracefulTimeoutMs))
3010
+ return true;
3011
+ try {
3012
+ process.kill(target, "SIGKILL");
3013
+ options.log?.(`Sent SIGKILL to ${label}`);
3014
+ } catch {}
3015
+ return waitForExitSync(pid, 500);
3016
+ }
3017
+ function waitForExitSync(pid, timeoutMs) {
3018
+ const deadline = Date.now() + timeoutMs;
3019
+ while (Date.now() < deadline) {
3020
+ if (!isProcessAlive2(pid))
3021
+ return true;
3022
+ sleepSync(50);
3023
+ }
3024
+ return !isProcessAlive2(pid);
3025
+ }
3026
+ function sleepSync(ms) {
3027
+ const buffer = new SharedArrayBuffer(4);
3028
+ const view = new Int32Array(buffer);
3029
+ Atomics.wait(view, 0, 0, ms);
3030
+ }
3031
+ function extractRemoteUrl(command) {
3032
+ const equals = command.match(/(?:^|\s)--remote=([^\s]+)/);
3033
+ if (equals)
3034
+ return equals[1];
3035
+ const separate = command.match(/(?:^|\s)--remote\s+([^\s]+)/);
3036
+ return separate?.[1] ?? null;
3037
+ }
3038
+ function remoteTargetsProxy(remoteUrl, proxyUrl) {
3039
+ try {
3040
+ const remote = new URL(remoteUrl);
3041
+ const proxy = new URL(proxyUrl);
3042
+ return remote.protocol === proxy.protocol && remote.hostname === proxy.hostname && remote.port === proxy.port && normalizePath(remote.pathname) === normalizePath(proxy.pathname);
3043
+ } catch {
3044
+ return remoteUrl === proxyUrl;
3045
+ }
3046
+ }
3047
+ function normalizePath(pathname) {
3048
+ return pathname === "" ? "/" : pathname;
3049
+ }
3050
+ var init_process_lifecycle = () => {};
3051
+
917
3052
  // src/cli/codex.ts
918
3053
  var exports_codex = {};
919
3054
  __export(exports_codex, {
920
- runCodex: () => runCodex
3055
+ runCodex: () => runCodex,
3056
+ resolveCodexResumeArgs: () => resolveCodexResumeArgs,
3057
+ parseAgentBridgeCodexArgs: () => parseAgentBridgeCodexArgs,
3058
+ buildCodexArgs: () => buildCodexArgs
921
3059
  });
922
- import { spawn as spawn3, execSync as execSync2 } from "child_process";
3060
+ import { spawn as spawn3, execSync as execSync2, execFileSync as execFileSync7 } from "child_process";
923
3061
  import {
924
- openSync as openSync2,
3062
+ openSync as openSync3,
925
3063
  writeSync,
926
- closeSync as closeSync2,
927
- writeFileSync as writeFileSync4,
928
- unlinkSync as unlinkSync2,
929
- appendFileSync,
930
- existsSync as existsSync6,
931
- mkdirSync as mkdirSync3
3064
+ closeSync as closeSync3,
3065
+ writeFileSync as writeFileSync7,
3066
+ readFileSync as readFileSync9,
3067
+ unlinkSync as unlinkSync4,
3068
+ existsSync as existsSync10,
3069
+ mkdirSync as mkdirSync6
932
3070
  } from "fs";
933
- import { dirname as dirname2 } from "path";
3071
+ import { homedir as homedir4 } from "os";
3072
+ import { dirname as dirname4, join as join11 } from "path";
934
3073
  function appendWrapperLog(path, entry) {
935
3074
  try {
936
- const dir = dirname2(path);
937
- if (!existsSync6(dir)) {
938
- mkdirSync3(dir, { recursive: true });
3075
+ const dir = dirname4(path);
3076
+ if (!existsSync10(dir)) {
3077
+ mkdirSync6(dir, { recursive: true });
939
3078
  }
940
- appendFileSync(path, `[${new Date().toISOString()}] ${entry}
941
- `, "utf-8");
3079
+ appendRotatingLog(path, `[${new Date().toISOString()}] ${entry}
3080
+ `);
942
3081
  } catch {}
943
3082
  }
944
3083
  function buildChildEnv() {
@@ -948,17 +3087,106 @@ function buildChildEnv() {
948
3087
  RUST_LOG: process.env.RUST_LOG ?? "info,codex_core=debug,codex_tui=debug,codex_app_server=debug"
949
3088
  };
950
3089
  }
3090
+ function parseAgentBridgeCodexArgs(args) {
3091
+ const rest = [];
3092
+ let forceNew = false;
3093
+ let resumeCurrent = false;
3094
+ for (const arg of args) {
3095
+ if (arg === "--new") {
3096
+ forceNew = true;
3097
+ continue;
3098
+ }
3099
+ if (arg === "resume-current") {
3100
+ resumeCurrent = true;
3101
+ continue;
3102
+ }
3103
+ rest.push(arg);
3104
+ }
3105
+ return { rest, forceNew, resumeCurrent };
3106
+ }
3107
+ function resolveCodexResumeArgs(parsed, pair, env = process.env) {
3108
+ if (parsed.forceNew && parsed.resumeCurrent) {
3109
+ return {
3110
+ rest: parsed.rest,
3111
+ mode: "new",
3112
+ error: "`--new` cannot be combined with `resume-current`."
3113
+ };
3114
+ }
3115
+ if (parsed.forceNew) {
3116
+ return { rest: parsed.rest, mode: "new" };
3117
+ }
3118
+ const identity = {
3119
+ stateDir: pair.stateDir,
3120
+ pairId: pair.manual ? null : pair.pairId,
3121
+ pairName: pair.name,
3122
+ cwd: process.cwd()
3123
+ };
3124
+ const current = readUsableCurrentThread(identity, env);
3125
+ if (parsed.resumeCurrent) {
3126
+ if (!current) {
3127
+ return {
3128
+ rest: parsed.rest,
3129
+ mode: "resume-current",
3130
+ error: "No verified current Codex thread for this pair. Start a new one with `abg codex --new`, or resume a specific thread with `abg codex resume <threadId>`."
3131
+ };
3132
+ }
3133
+ return {
3134
+ rest: ["resume", current.threadId, ...parsed.rest],
3135
+ mode: "resume-current",
3136
+ thread: current
3137
+ };
3138
+ }
3139
+ if (parsed.rest.length === 0 && current) {
3140
+ return {
3141
+ rest: ["resume", current.threadId],
3142
+ mode: "auto-resume",
3143
+ thread: current
3144
+ };
3145
+ }
3146
+ return { rest: parsed.rest, mode: "passthrough" };
3147
+ }
3148
+ function buildCodexArgs(userArgs, proxyUrl) {
3149
+ const bridgeFlags = ["--enable", "tui_app_server", "--remote", proxyUrl];
3150
+ const first = userArgs[0];
3151
+ if (!first || first.startsWith("-")) {
3152
+ return { fullArgs: [...bridgeFlags, ...userArgs], injectedBridgeFlags: true };
3153
+ }
3154
+ if (TUI_SUBCOMMANDS.has(first)) {
3155
+ return {
3156
+ fullArgs: [first, ...bridgeFlags, ...userArgs.slice(1)],
3157
+ injectedBridgeFlags: true
3158
+ };
3159
+ }
3160
+ if (NON_TUI_SUBCOMMANDS.has(first)) {
3161
+ return { fullArgs: userArgs, injectedBridgeFlags: false };
3162
+ }
3163
+ return { fullArgs: [...bridgeFlags, ...userArgs], injectedBridgeFlags: true };
3164
+ }
951
3165
  async function runCodex(args) {
952
- checkOwnedFlagConflicts(args, "agentbridge codex", OWNED_FLAGS2);
953
- for (let i = 0;i < args.length; i++) {
954
- if (args[i] === "--enable" && args[i + 1] === "tui_app_server") {
3166
+ const originalEnv = { ...process.env };
3167
+ const envGuardResult = guardAgentBridgeEnv({
3168
+ cwd: process.cwd(),
3169
+ env: process.env,
3170
+ mode: normalizeEnvGuardMode(process.env.AGENTBRIDGE_ENV_GUARD),
3171
+ allowStrict: true,
3172
+ log: (msg) => console.error(msg)
3173
+ });
3174
+ const { pairFlag, rest } = parsePairFlag(args);
3175
+ const wrapperArgs = parseAgentBridgeCodexArgs(rest);
3176
+ const agentsContract = checkAgentsMdContract(process.cwd());
3177
+ if (!agentsContract.fresh) {
3178
+ console.error(`[agentbridge] ${agentsContract.message}`);
3179
+ }
3180
+ checkOwnedFlagConflicts(wrapperArgs.rest, "agentbridge codex", OWNED_FLAGS2);
3181
+ for (let i = 0;i < wrapperArgs.rest.length; i++) {
3182
+ if (wrapperArgs.rest[i] === "--enable" && wrapperArgs.rest[i + 1] === "tui_app_server") {
955
3183
  console.error(`Error: "--enable tui_app_server" is automatically set by agentbridge codex.`);
956
3184
  console.error("");
957
3185
  console.error("If you need full control over these flags, use the native command directly:");
958
3186
  console.error(" codex [your flags here]");
959
3187
  process.exit(1);
960
3188
  }
961
- if (args[i] === "--enable=tui_app_server") {
3189
+ if (wrapperArgs.rest[i] === "--enable=tui_app_server") {
962
3190
  console.error(`Error: "--enable=tui_app_server" is automatically set by agentbridge codex.`);
963
3191
  console.error("");
964
3192
  console.error("If you need full control over these flags, use the native command directly:");
@@ -966,15 +3194,30 @@ async function runCodex(args) {
966
3194
  process.exit(1);
967
3195
  }
968
3196
  }
969
- const stateDir = new StateDirResolver;
970
- const configService = new ConfigService;
971
- const config = configService.loadOrDefault();
972
- const controlPort = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
3197
+ let pair;
3198
+ try {
3199
+ pair = await applyPairEnv({ pairFlag });
3200
+ } catch (err) {
3201
+ console.error(`[agentbridge] ${err.message}`);
3202
+ process.exit(1);
3203
+ }
3204
+ if (pair.warning)
3205
+ console.error(`[agentbridge] \u26A0\uFE0F ${pair.warning}`);
3206
+ if (process.env.AGENTBRIDGE_TRACE === "1") {
3207
+ traceCliStart2("cli.codex.start", args, originalEnv, envGuardResult.action, pair);
3208
+ }
3209
+ const stateDir = pair.stateDir;
3210
+ const controlPort = pair.ports.controlPort;
3211
+ const pairProxyUrl = `ws://127.0.0.1:${pair.ports.proxyPort}`;
3212
+ guardNoLiveManagedTui(stateDir, pairProxyUrl);
973
3213
  const lifecycle = new DaemonLifecycle({
974
3214
  stateDir,
975
3215
  controlPort,
976
3216
  log: (msg) => console.error(`[agentbridge] ${msg}`)
977
3217
  });
3218
+ if (!pair.manual) {
3219
+ console.error(`[agentbridge] pair "${pair.pairId}" (slot ${pair.slot}) \u2014 control :${controlPort}, ` + `codex :${pair.ports.appPort}/:${pair.ports.proxyPort}`);
3220
+ }
978
3221
  console.error("[agentbridge] Ensuring daemon is running...");
979
3222
  try {
980
3223
  lifecycle.clearKilled();
@@ -982,7 +3225,7 @@ async function runCodex(args) {
982
3225
  console.error("[agentbridge] Daemon is ready.");
983
3226
  } catch (err) {
984
3227
  console.error(`[agentbridge] Failed to start daemon: ${err.message}`);
985
- console.error("[agentbridge] Try: agentbridge kill && agentbridge claude");
3228
+ console.error(`[agentbridge] Try: ${pairScopedCommand("kill")} && ${pairScopedCommand("claude")}`);
986
3229
  process.exit(1);
987
3230
  }
988
3231
  let proxyUrl;
@@ -990,8 +3233,9 @@ async function runCodex(args) {
990
3233
  if (status?.proxyUrl) {
991
3234
  proxyUrl = status.proxyUrl;
992
3235
  } else {
993
- proxyUrl = `ws://127.0.0.1:${config.codex.proxyPort}`;
994
- console.error(`[agentbridge] No daemon status found, using config default: ${proxyUrl}`);
3236
+ const fallbackProxyPort = process.env.CODEX_PROXY_PORT ?? String(new ConfigService().loadOrDefault().codex.proxyPort);
3237
+ proxyUrl = `ws://127.0.0.1:${fallbackProxyPort}`;
3238
+ console.error(`[agentbridge] No daemon status found, using fallback proxy port: ${proxyUrl}`);
995
3239
  }
996
3240
  try {
997
3241
  await waitForProxyReady(proxyUrl);
@@ -1018,7 +3262,7 @@ async function runCodex(args) {
1018
3262
  }
1019
3263
  let ttyFd = null;
1020
3264
  try {
1021
- ttyFd = openSync2("/dev/tty", "w");
3265
+ ttyFd = openSync3("/dev/tty", "w");
1022
3266
  } catch {
1023
3267
  if (process.stdout.isTTY) {
1024
3268
  ttyFd = 1;
@@ -1040,32 +3284,53 @@ async function runCodex(args) {
1040
3284
  }
1041
3285
  if (ttyFd !== 1) {
1042
3286
  try {
1043
- closeSync2(ttyFd);
3287
+ closeSync3(ttyFd);
1044
3288
  } catch {}
1045
3289
  }
1046
3290
  }
1047
3291
  }
1048
- const fullArgs = [
1049
- "--enable",
1050
- "tui_app_server",
1051
- "--remote",
1052
- proxyUrl,
1053
- ...args
1054
- ];
3292
+ const resumeArgs = resolveCodexResumeArgs(wrapperArgs, pair);
3293
+ if (resumeArgs.error) {
3294
+ console.error(`[agentbridge] ${resumeArgs.error}`);
3295
+ process.exit(1);
3296
+ }
3297
+ if (resumeArgs.mode === "auto-resume" || resumeArgs.mode === "resume-current") {
3298
+ console.error(`[agentbridge] Resuming current Codex thread ${resumeArgs.thread.threadId}`);
3299
+ }
3300
+ const { fullArgs } = buildCodexArgs(resumeArgs.rest, proxyUrl);
1055
3301
  const stderrTail = new StderrRingBuffer;
1056
3302
  const wrapperLogPath = stateDir.codexWrapperLogFile;
1057
3303
  const startedAt = Date.now();
1058
3304
  stateDir.ensure();
1059
- appendWrapperLog(wrapperLogPath, `spawn: codex ${fullArgs.map((a) => a.includes(" ") ? JSON.stringify(a) : a).join(" ")}`);
3305
+ appendWrapperLog(wrapperLogPath, `spawn: codex ${redactArgv(fullArgs).map((a) => a.includes(" ") ? JSON.stringify(a) : a).join(" ")}`);
1060
3306
  const child = spawn3("codex", fullArgs, {
1061
3307
  stdio: ["inherit", "inherit", "pipe"],
1062
3308
  env: buildChildEnv()
1063
3309
  });
1064
3310
  if (typeof child.pid === "number") {
1065
- writeFileSync4(stateDir.tuiPidFile, `${child.pid}
3311
+ writeFileSync7(stateDir.tuiPidFile, `${child.pid}
1066
3312
  `, "utf-8");
1067
3313
  appendWrapperLog(wrapperLogPath, `child pid=${child.pid}`);
1068
3314
  }
3315
+ let nativeChildPid = null;
3316
+ if (typeof child.pid === "number") {
3317
+ const launcherPid = child.pid;
3318
+ let attempts = 0;
3319
+ const discover = () => {
3320
+ attempts += 1;
3321
+ nativeChildPid = discoverNativeChildPid(launcherPid, (cmd, args2) => execFileSync7(cmd, args2, { encoding: "utf-8", timeout: 2000 }));
3322
+ if (nativeChildPid !== null) {
3323
+ appendWrapperLog(wrapperLogPath, `native child pid=${nativeChildPid} (launcher pid=${launcherPid})`);
3324
+ return;
3325
+ }
3326
+ if (attempts < 5 && !childExited) {
3327
+ const retry = setTimeout(discover, 500);
3328
+ retry.unref();
3329
+ }
3330
+ };
3331
+ const first = setTimeout(discover, 300);
3332
+ first.unref();
3333
+ }
1069
3334
  if (child.stderr) {
1070
3335
  child.stderr.on("data", (chunk) => {
1071
3336
  try {
@@ -1075,29 +3340,69 @@ async function runCodex(args) {
1075
3340
  });
1076
3341
  }
1077
3342
  let cleanedTuiPid = false;
3343
+ let childExited = false;
3344
+ let wrapperShuttingDown = false;
3345
+ let signalExitCode = null;
1078
3346
  function cleanupTuiPidFile() {
1079
3347
  if (cleanedTuiPid)
1080
3348
  return;
1081
3349
  cleanedTuiPid = true;
1082
3350
  try {
1083
- unlinkSync2(stateDir.tuiPidFile);
3351
+ unlinkSync4(stateDir.tuiPidFile);
1084
3352
  } catch {}
1085
3353
  }
1086
- process.on("exit", () => {
1087
- restoreTerminal();
1088
- cleanupTuiPidFile();
1089
- });
1090
- process.on("SIGINT", () => {
3354
+ function requestChildTermination(reason) {
3355
+ if (childExited)
3356
+ return;
3357
+ const pid = child.pid;
3358
+ if (typeof pid !== "number")
3359
+ return;
3360
+ appendWrapperLog(wrapperLogPath, `terminating child pid=${pid} reason=${reason}`);
3361
+ try {
3362
+ child.kill("SIGTERM");
3363
+ } catch {}
3364
+ const killTimer = setTimeout(() => {
3365
+ if (childExited)
3366
+ return;
3367
+ appendWrapperLog(wrapperLogPath, `child pid=${pid} still alive after SIGTERM; sending SIGKILL`);
3368
+ try {
3369
+ child.kill("SIGKILL");
3370
+ } catch {}
3371
+ }, 1500);
3372
+ killTimer.unref();
3373
+ }
3374
+ function shutdownWrapper(reason, exitCode) {
3375
+ if (wrapperShuttingDown)
3376
+ return;
3377
+ wrapperShuttingDown = true;
3378
+ signalExitCode = exitCode;
1091
3379
  restoreTerminal();
1092
- cleanupTuiPidFile();
1093
- process.exit(130);
1094
- });
1095
- process.on("SIGTERM", () => {
3380
+ requestChildTermination(reason);
3381
+ if (childExited) {
3382
+ cleanupTuiPidFile();
3383
+ process.exit(exitCode);
3384
+ return;
3385
+ }
3386
+ const forceTimer = setTimeout(() => {
3387
+ cleanupTuiPidFile();
3388
+ process.exit(exitCode);
3389
+ }, 3000);
3390
+ forceTimer.unref();
3391
+ }
3392
+ process.on("exit", () => {
1096
3393
  restoreTerminal();
3394
+ if (!childExited && typeof child.pid === "number") {
3395
+ try {
3396
+ child.kill("SIGKILL");
3397
+ } catch {}
3398
+ }
1097
3399
  cleanupTuiPidFile();
1098
- process.exit(143);
1099
3400
  });
3401
+ process.on("SIGHUP", () => shutdownWrapper("SIGHUP", 129));
3402
+ process.on("SIGINT", () => shutdownWrapper("SIGINT", 130));
3403
+ process.on("SIGTERM", () => shutdownWrapper("SIGTERM", 143));
1100
3404
  child.on("exit", (code, signal) => {
3405
+ childExited = true;
1101
3406
  cleanupTuiPidFile();
1102
3407
  const runtimeMs = Date.now() - startedAt;
1103
3408
  const tail = stderrTail.toString();
@@ -1113,16 +3418,25 @@ async function runCodex(args) {
1113
3418
  classification = `signal:${signal}`;
1114
3419
  else if (typeof code === "number" && code !== 0)
1115
3420
  classification = `nonzero_exit:${code}`;
1116
- else if (code === 0 && tail.trim().length === 0)
1117
- classification = "exit_0_empty_stderr";
3421
+ else if (code === 0 && tail.trim().length === 0) {
3422
+ classification = refineCleanExitClassification(readTurnInProgress(stateDir.statusFile));
3423
+ }
3424
+ const tuiLogTail = captureTuiLogTail({
3425
+ codexHome: join11(homedir4(), ".codex"),
3426
+ nativePid: nativeChildPid,
3427
+ run: (cmd, args2) => execFileSync7(cmd, args2, { encoding: "utf-8", timeout: 2000 })
3428
+ });
1118
3429
  appendWrapperLog(wrapperLogPath, [
1119
- `exit: code=${code ?? "null"} signal=${signal ?? "null"} runtime_ms=${runtimeMs} pid=${child.pid ?? "unknown"} classification=${classification}`,
3430
+ `exit: code=${code ?? "null"} signal=${signal ?? "null"} runtime_ms=${runtimeMs} pid=${child.pid ?? "unknown"} native_pid=${nativeChildPid ?? "unknown"} classification=${classification}`,
1120
3431
  `--- last stderr (${stderrTail.byteLength} bytes) ---`,
1121
3432
  tailLines,
1122
- `--- end stderr ---`
3433
+ `--- end stderr ---`,
3434
+ `--- last tui log (native pid ${nativeChildPid ?? "unknown"}) ---`,
3435
+ tuiLogTail,
3436
+ `--- end tui log ---`
1123
3437
  ].join(`
1124
3438
  `));
1125
- process.exit(code ?? 0);
3439
+ process.exit(signalExitCode ?? code ?? 0);
1126
3440
  });
1127
3441
  child.on("error", (err) => {
1128
3442
  cleanupTuiPidFile();
@@ -1136,197 +3450,1383 @@ async function runCodex(args) {
1136
3450
  process.exit(1);
1137
3451
  });
1138
3452
  }
1139
- function proxyHealthUrl(proxyUrl) {
1140
- const url = new URL(proxyUrl);
1141
- url.protocol = url.protocol === "wss:" ? "https:" : "http:";
1142
- url.pathname = "/healthz";
1143
- url.search = "";
1144
- url.hash = "";
1145
- return url.toString();
3453
+ function traceCliStart2(event, args, originalEnv, envGuardAction, pair) {
3454
+ try {
3455
+ appendTraceEvent({
3456
+ cwd: process.cwd(),
3457
+ event,
3458
+ pid: process.pid,
3459
+ argv: ["agentbridge", "codex", ...args],
3460
+ env: process.env,
3461
+ data: {
3462
+ originalEnv: pickRelevantEnv(originalEnv),
3463
+ effectiveEnv: pickRelevantEnv(process.env),
3464
+ envGuardAction,
3465
+ pairId: pair.pairId,
3466
+ pairName: pair.name,
3467
+ manual: pair.manual,
3468
+ slot: pair.slot,
3469
+ stateDir: pair.stateDir.dir,
3470
+ ports: pair.ports,
3471
+ build: BUILD_INFO
3472
+ }
3473
+ });
3474
+ } catch {}
3475
+ }
3476
+ function guardNoLiveManagedTui(stateDir, proxyUrl) {
3477
+ const pid = readTuiPid(stateDir);
3478
+ if (pid) {
3479
+ if (!isProcessAlive2(pid)) {
3480
+ try {
3481
+ unlinkSync4(stateDir.tuiPidFile);
3482
+ } catch {}
3483
+ } else if (!isManagedCodexTuiProcess(pid, proxyUrl)) {
3484
+ appendWrapperLog(stateDir.codexWrapperLogFile, `stale tui pid file pointed at unmanaged live pid=${pid}; removing`);
3485
+ try {
3486
+ unlinkSync4(stateDir.tuiPidFile);
3487
+ } catch {}
3488
+ } else {
3489
+ console.error(`[agentbridge] This pair already has a managed Codex TUI running (pid ${pid}).`);
3490
+ console.error(`[agentbridge] Use that terminal, or stop it with: ${pairScopedCommand("kill")}`);
3491
+ process.exit(1);
3492
+ }
3493
+ }
3494
+ const orphan = findManagedCodexTuiProcesses(proxyUrl)[0];
3495
+ if (!orphan)
3496
+ return;
3497
+ console.error(`[agentbridge] This pair already has a managed Codex TUI running (pid ${orphan.pid}).`);
3498
+ console.error(`[agentbridge] Use that terminal, or stop it with: ${pairScopedCommand("kill")}`);
3499
+ process.exit(1);
3500
+ }
3501
+ function readTuiPid(stateDir) {
3502
+ try {
3503
+ const raw = readFileSync9(stateDir.tuiPidFile, "utf-8").trim();
3504
+ if (!raw)
3505
+ return null;
3506
+ const pid = Number.parseInt(raw, 10);
3507
+ return Number.isFinite(pid) ? pid : null;
3508
+ } catch {
3509
+ return null;
3510
+ }
3511
+ }
3512
+ function isManagedCodexTuiProcess(pid, proxyUrl) {
3513
+ const cmd = commandForPid(pid);
3514
+ return cmd !== null && commandMatchesManagedCodexTui(cmd, proxyUrl);
3515
+ }
3516
+ function proxyHealthUrl(proxyUrl) {
3517
+ const url = new URL(proxyUrl);
3518
+ url.protocol = url.protocol === "wss:" ? "https:" : "http:";
3519
+ url.pathname = "/healthz";
3520
+ url.search = "";
3521
+ url.hash = "";
3522
+ return url.toString();
3523
+ }
3524
+ async function waitForProxyReady(proxyUrl, maxRetries = 20, delayMs = 100) {
3525
+ const healthUrl = proxyHealthUrl(proxyUrl);
3526
+ for (let attempt = 0;attempt < maxRetries; attempt++) {
3527
+ try {
3528
+ const response = await fetch(healthUrl);
3529
+ if (response.ok) {
3530
+ return;
3531
+ }
3532
+ } catch {}
3533
+ await new Promise((resolve4) => setTimeout(resolve4, delayMs));
3534
+ }
3535
+ throw new Error(`Timed out waiting for Codex proxy readiness on ${healthUrl}`);
3536
+ }
3537
+ var OWNED_FLAGS2, TUI_SUBCOMMANDS, NON_TUI_SUBCOMMANDS;
3538
+ var init_codex = __esm(() => {
3539
+ init_agents_contract();
3540
+ init_wrapper_exit_observability();
3541
+ init_config_service();
3542
+ init_build_info();
3543
+ init_daemon_lifecycle();
3544
+ init_env_guard();
3545
+ init_pair_command();
3546
+ init_rotating_log();
3547
+ init_pair_resolver();
3548
+ init_stderr_ring_buffer();
3549
+ init_thread_state();
3550
+ init_trace_log();
3551
+ init_process_lifecycle();
3552
+ init_claude();
3553
+ OWNED_FLAGS2 = ["--remote"];
3554
+ TUI_SUBCOMMANDS = new Set(["resume", "fork"]);
3555
+ NON_TUI_SUBCOMMANDS = new Set([
3556
+ "exec",
3557
+ "e",
3558
+ "review",
3559
+ "login",
3560
+ "logout",
3561
+ "mcp",
3562
+ "mcp-server",
3563
+ "plugin",
3564
+ "remote-control",
3565
+ "update",
3566
+ "app-server",
3567
+ "exec-server",
3568
+ "app",
3569
+ "completion",
3570
+ "sandbox",
3571
+ "debug",
3572
+ "apply",
3573
+ "a",
3574
+ "cloud",
3575
+ "features",
3576
+ "help"
3577
+ ]);
3578
+ });
3579
+
3580
+ // src/cli/kill.ts
3581
+ var exports_kill = {};
3582
+ __export(exports_kill, {
3583
+ stopPairEntry: () => stopPairEntry,
3584
+ runKill: () => runKill,
3585
+ formatKillReport: () => formatKillReport
3586
+ });
3587
+ import { readFileSync as readFileSync10, unlinkSync as unlinkSync5 } from "fs";
3588
+ import { join as join12 } from "path";
3589
+ async function runKill(args = []) {
3590
+ const argError = validateKillArgs(args);
3591
+ if (argError === "help") {
3592
+ printKillUsage();
3593
+ return;
3594
+ }
3595
+ if (argError) {
3596
+ console.error(`Error: ${argError}`);
3597
+ printKillUsage();
3598
+ process.exit(1);
3599
+ }
3600
+ const parsed = parseKillArgs(args);
3601
+ if (parsed.pairFlag !== undefined && parsed.all) {
3602
+ console.error('Error: use either "--pair <name>" or "--all", not both.');
3603
+ process.exit(1);
3604
+ }
3605
+ const base = computeBaseDir();
3606
+ console.log(`AgentBridge Kill \u2014 stopping AgentBridge pair processes
3607
+ `);
3608
+ const results = [];
3609
+ let restartCommand = "agentbridge claude";
3610
+ if (parsed.pairFlag !== undefined) {
3611
+ let pair;
3612
+ try {
3613
+ pair = findPairForFlag(base, process.cwd(), parsed.pairFlag);
3614
+ } catch (err) {
3615
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
3616
+ process.exit(1);
3617
+ }
3618
+ if (!pair) {
3619
+ console.log(`No such pair: "${parsed.pairFlag}" in ${process.cwd()}`);
3620
+ printKnownPairs(base);
3621
+ return;
3622
+ }
3623
+ restartCommand = `agentbridge --pair ${pair.name ?? parsed.pairFlag} claude`;
3624
+ results.push(await stopPairEntry(base, pair));
3625
+ } else if (parsed.all) {
3626
+ let registered = [];
3627
+ try {
3628
+ registered = listPairs(base);
3629
+ } catch (error) {
3630
+ const message = error instanceof Error ? error.message : String(error);
3631
+ console.log(`\u26A0\uFE0F pair registry \u4E0D\u53EF\u8BFB\uFF08${message}\uFF09\u2014\u2014\u964D\u7EA7\u4E3A\u72B6\u6001\u76EE\u5F55\u626B\u63CF\uFF0C\u4ECD\u4F1A\u505C\u6B62\u80FD\u627E\u5230\u7684\u5168\u90E8 pair\u3002`);
3632
+ }
3633
+ for (const pair of registered) {
3634
+ results.push(await stopPairEntry(base, pair));
3635
+ }
3636
+ const registeredIds = new Set(registered.map((pair) => pair.pairId));
3637
+ for (const dirName of listPairDirsSafe(base)) {
3638
+ if (registeredIds.has(dirName))
3639
+ continue;
3640
+ const stateDir = new StateDirResolver(join12(base, "pairs", dirName));
3641
+ results.push(await stopStateDir(`${dirName} (unregistered)`, stateDir, portsFromStateDir(stateDir)));
3642
+ }
3643
+ const legacy = detectLegacyRootDaemon(base);
3644
+ if (legacy) {
3645
+ results.push(await stopStateDir("(legacy-root)", new StateDirResolver(base), {
3646
+ appPort: 4500,
3647
+ proxyPort: 4501,
3648
+ controlPort: legacy.controlPort
3649
+ }));
3650
+ }
3651
+ } else {
3652
+ let cwdPairs = [];
3653
+ try {
3654
+ cwdPairs = listPairsForCwd(base, process.cwd());
3655
+ } catch (error) {
3656
+ const message = error instanceof Error ? error.message : String(error);
3657
+ console.log(`\u26A0\uFE0F pair registry \u4E0D\u53EF\u8BFB\uFF08${message}\uFF09\u2014\u2014\u65E0\u6CD5\u6309\u76EE\u5F55\u5B9A\u4F4D pair\u3002` + "\u8FD0\u884C `abg kill --all` \u53EF\u964D\u7EA7\u4E3A\u5168\u76D8\u72B6\u6001\u76EE\u5F55\u626B\u63CF\uFF0C\u505C\u6B62\u6240\u6709\u80FD\u627E\u5230\u7684 pair\u3002");
3658
+ process.exitCode = 2;
3659
+ }
3660
+ for (const pair of cwdPairs) {
3661
+ results.push(await stopPairEntry(base, pair));
3662
+ }
3663
+ const legacy = detectLegacyRootDaemon(base);
3664
+ if (legacy) {
3665
+ results.push(await stopStateDir("(legacy-root)", new StateDirResolver(base), {
3666
+ appPort: 4500,
3667
+ proxyPort: 4501,
3668
+ controlPort: legacy.controlPort
3669
+ }));
3670
+ }
3671
+ if (results.length === 0) {
3672
+ console.log(`No AgentBridge pairs registered for current directory: ${process.cwd()}`);
3673
+ console.log("Use `abg kill all` or `abg kill --all` to stop pairs from every directory.");
3674
+ return;
3675
+ }
3676
+ }
3677
+ printSummary(results, restartCommand);
3678
+ }
3679
+ function validateKillArgs(args) {
3680
+ for (let i = 0;i < args.length; i++) {
3681
+ const arg = args[i];
3682
+ if (arg === "--help" || arg === "-h")
3683
+ return "help";
3684
+ if (arg === "all")
3685
+ continue;
3686
+ if (arg === "--all")
3687
+ continue;
3688
+ if (arg === "--pair") {
3689
+ const value = args[i + 1];
3690
+ if (value === undefined || value.startsWith("-")) {
3691
+ return 'Missing value for "--pair".';
3692
+ }
3693
+ i++;
3694
+ continue;
3695
+ }
3696
+ if (arg.startsWith("--pair=")) {
3697
+ if (arg.slice("--pair=".length).length === 0) {
3698
+ return 'Missing value for "--pair".';
3699
+ }
3700
+ continue;
3701
+ }
3702
+ return `Unknown kill argument: ${arg}`;
3703
+ }
3704
+ return null;
3705
+ }
3706
+ function printKillUsage() {
3707
+ console.log(`
3708
+ Usage: abg kill [--all]
3709
+ abg kill all
3710
+ abg [--pair <name|id>] kill
3711
+
3712
+ Stops AgentBridge daemon/TUI processes.
3713
+
3714
+ Options:
3715
+ --pair <name|id> Stop only one pair \u2014 a cwd-scoped name (e.g. "main") or the
3716
+ same pair id when run from that directory.
3717
+ all, --all Stop all registered pairs and any legacy-root daemon.
3718
+ --help, -h Show this help message.
3719
+
3720
+ No arguments stop this directory's registered pairs and any legacy-root daemon.
3721
+ `.trim());
3722
+ }
3723
+ async function stopPairEntry(base, pair) {
3724
+ const ports = portsForEntry(pair);
3725
+ const stateDir = new StateDirResolver(join12(base, "pairs", pair.pairId));
3726
+ return stopStateDir(pair.pairId, stateDir, ports);
3727
+ }
3728
+ function listPairDirsSafe(base) {
3729
+ try {
3730
+ return listPairDirs(base);
3731
+ } catch {
3732
+ return [];
3733
+ }
3734
+ }
3735
+ function portsFromStateDir(stateDir) {
3736
+ try {
3737
+ const raw = JSON.parse(readFileSync10(stateDir.statusFile, "utf-8"));
3738
+ return {
3739
+ appPort: portFromUrl(raw?.appServerUrl) ?? 0,
3740
+ proxyPort: portFromUrl(raw?.proxyUrl) ?? 0,
3741
+ controlPort: typeof raw?.controlPort === "number" ? raw.controlPort : 0
3742
+ };
3743
+ } catch {
3744
+ return { appPort: 0, proxyPort: 0, controlPort: 0 };
3745
+ }
3746
+ }
3747
+ function portFromUrl(url) {
3748
+ if (typeof url !== "string")
3749
+ return null;
3750
+ const match = url.match(/:(\d+)(?:[/?]|$)/);
3751
+ return match ? Number.parseInt(match[1], 10) : null;
3752
+ }
3753
+ async function stopStateDir(label, stateDir, ports) {
3754
+ const portsLabel = `${ports.appPort}/${ports.proxyPort}/${ports.controlPort}`;
3755
+ const details = [];
3756
+ const log = (msg) => details.push(msg);
3757
+ try {
3758
+ const lifecycle = new DaemonLifecycle({
3759
+ stateDir,
3760
+ controlPort: ports.controlPort,
3761
+ log
3762
+ });
3763
+ lifecycle.markKilled();
3764
+ const status = lifecycle.readStatus();
3765
+ const proxyUrl = typeof status?.proxyUrl === "string" && status.proxyUrl.length > 0 ? status.proxyUrl : `ws://127.0.0.1:${ports.proxyPort}`;
3766
+ const tuiKilled = await killManagedCodexTui(stateDir, proxyUrl, log);
3767
+ const daemonKilled = await lifecycle.kill();
3768
+ return { label, portsLabel, daemonKilled, tuiKilled, details };
3769
+ } catch (error) {
3770
+ log(`ERROR: ${error instanceof Error ? error.message : String(error)}`);
3771
+ return { label, portsLabel, daemonKilled: false, tuiKilled: false, details, error };
3772
+ }
3773
+ }
3774
+ function printKnownPairs(base) {
3775
+ try {
3776
+ const pairs = listPairs(base);
3777
+ if (pairs.length === 0) {
3778
+ console.log("No pairs registered.");
3779
+ return;
3780
+ }
3781
+ console.log("Known pairs:");
3782
+ for (const pair of pairs) {
3783
+ const ports = portsForEntry(pair);
3784
+ console.log(` ${pair.pairId} (name=${pair.name ?? "main"}, cwd=${pair.cwd}, slot ${pair.slot}, ports ${ports.appPort}/${ports.proxyPort}/${ports.controlPort})`);
3785
+ }
3786
+ } catch (error) {
3787
+ if (error instanceof PairError) {
3788
+ console.log(`Could not read pair registry: ${error.message}`);
3789
+ return;
3790
+ }
3791
+ throw error;
3792
+ }
3793
+ }
3794
+ function describeStopped(result) {
3795
+ const parts = [];
3796
+ if (result.daemonKilled)
3797
+ parts.push("daemon");
3798
+ if (result.tuiKilled)
3799
+ parts.push("Codex TUI");
3800
+ return `${result.label}\uFF08${parts.join(" + ")}\uFF09`;
3801
+ }
3802
+ function formatKillReport(results, frontends, restartCommand) {
3803
+ const lines = [];
3804
+ if (results.length === 0) {
3805
+ lines.push("No pairs registered.");
3806
+ return lines;
3807
+ }
3808
+ const stopped = results.filter((r) => (r.daemonKilled || r.tuiKilled) && !r.error);
3809
+ const failed = results.filter((r) => r.error);
3810
+ const idle = results.filter((r) => !r.daemonKilled && !r.tuiKilled && !r.error);
3811
+ for (const result of [...stopped, ...failed]) {
3812
+ lines.push(` [${result.label} ${result.portsLabel}]`);
3813
+ for (const detail of result.details)
3814
+ lines.push(` ${detail}`);
3815
+ }
3816
+ if (stopped.length > 0 || failed.length > 0)
3817
+ lines.push("");
3818
+ lines.push(`\u603B\u7ED3\uFF08\u5171 ${results.length} \u4E2A\u76EE\u6807\uFF09:`);
3819
+ if (stopped.length > 0) {
3820
+ lines.push(` \u2705 \u5DF2\u505C\u6B62 ${stopped.length} \u4E2A: ${stopped.map(describeStopped).join(", ")}`);
3821
+ }
3822
+ if (idle.length > 0) {
3823
+ lines.push(` \u26AA \u672C\u6765\u5C31\u6CA1\u5728\u8FD0\u884C ${idle.length} \u4E2A: ${idle.map((r) => r.label).join(", ")}`);
3824
+ }
3825
+ if (failed.length > 0) {
3826
+ lines.push(` \u274C \u5931\u8D25 ${failed.length} \u4E2A: ${failed.map((r) => r.label).join(", ")}\uFF08\u8BE6\u89C1\u4E0A\u65B9\u65E5\u5FD7\uFF09`);
3827
+ }
3828
+ lines.push("");
3829
+ if (stopped.length > 0) {
3830
+ lines.push("AgentBridge stopped.");
3831
+ lines.push(`Please restart Claude Code (\`${restartCommand}\`), switch to a new conversation, or run \`/resume\` to fully disconnect.`);
3832
+ lines.push("\u2139\uFE0F \u5DF2\u5199\u5165 killed \u54E8\u5175\uFF1A\u88AB\u505C\u6B62\u7684 pair \u4E0D\u4F1A\u88AB\u81EA\u52A8\u590D\u6D3B\uFF1B" + `\u4E0B\u6B21 \`${restartCommand}\` / \`agentbridge codex\` \u4F1A\u6E05\u9664\u54E8\u5175\u5E76\u7528\u5F53\u524D\u5B89\u88C5\u7248\u672C\u542F\u52A8\u5168\u65B0 daemon\u3002`);
3833
+ } else {
3834
+ lines.push("No running AgentBridge daemon or managed Codex TUI found.");
3835
+ lines.push("\u2139\uFE0F \u76EE\u6807 pair \u90FD\u6CA1\u6709\u5728\u8FD0\u884C\u7684\u8FDB\u7A0B\u2014\u2014\u5982\u679C\u4F60\u4ECD\u770B\u5230 AgentBridge \u6D3B\u52A8\uFF0C\u89C1\u4E0B\u65B9\u524D\u7AEF\u63D0\u793A\u3002");
3836
+ }
3837
+ if (frontends.length > 0) {
3838
+ lines.push(`\u26A0\uFE0F \u68C0\u6D4B\u5230 ${frontends.length} \u4E2A\u4ECD\u5728\u8FD0\u884C\u7684 Claude Code \u6865\u63A5\u524D\u7AEF (pid ${frontends.map((f) => f.pid).join(", ")})\uFF1A`);
3839
+ lines.push(" \u5B83\u4EEC\u73B0\u5728\u5904\u4E8E\u5F85\u673A\u72B6\u6001\u3001\u4E0D\u4F1A\u590D\u6D3B\u5DF2\u505C\u6B62\u7684 daemon\uFF1B\u4F46\u65E7\u7A97\u53E3\u91CC\u52A0\u8F7D\u7684\u63D2\u4EF6\u4EE3\u7801");
3840
+ lines.push(" \u4E0D\u4F1A\u81EA\u52A8\u66F4\u65B0\u2014\u2014\u5347\u7EA7\u540E\u9700\u8981\u65B0\u7248\u672C\u65F6\uFF0C\u8BF7\u5173\u95ED\u5E76\u91CD\u5F00\u5BF9\u5E94\u7684 Claude Code \u7A97\u53E3\u3002");
3841
+ }
3842
+ lines.push("Registry entries were preserved. Use `abg pairs rm <name|id>` to stop and release a slot.");
3843
+ return lines;
3844
+ }
3845
+ function printSummary(results, restartCommand) {
3846
+ const frontends = listBridgeFrontendProcesses();
3847
+ for (const line of formatKillReport(results, frontends, restartCommand)) {
3848
+ console.log(line);
3849
+ }
3850
+ if (results.some((r) => r.error)) {
3851
+ process.exitCode = 2;
3852
+ }
3853
+ }
3854
+ async function killManagedCodexTui(stateDir, proxyUrl, log, gracefulTimeoutMs = 3000) {
3855
+ const pid = readTuiPid2(stateDir);
3856
+ let killed = false;
3857
+ if (!pid) {
3858
+ log("No Codex TUI pid file found");
3859
+ removeTuiPidFile(stateDir);
3860
+ } else if (!isProcessAlive2(pid)) {
3861
+ log(`Codex TUI pid ${pid} is not alive, cleaning up stale pid file`);
3862
+ removeTuiPidFile(stateDir);
3863
+ } else if (!isManagedCodexTuiProcess2(pid, proxyUrl)) {
3864
+ log(`Pid ${pid} is alive but is NOT a managed AgentBridge Codex TUI \u2014 refusing to kill. Cleaning up stale pid file.`);
3865
+ removeTuiPidFile(stateDir);
3866
+ } else {
3867
+ log(`Stopping Codex TUI pid ${pid}`);
3868
+ terminateProcessSync(pid, { gracefulTimeoutMs, log });
3869
+ removeTuiPidFile(stateDir);
3870
+ killed = true;
3871
+ }
3872
+ const orphanCandidates = findManagedCodexTuiProcesses(proxyUrl).filter((entry) => entry.pid !== pid);
3873
+ for (const candidate of orphanCandidates) {
3874
+ log(`Stopping orphan Codex TUI pid ${candidate.pid} attached to ${proxyUrl}`);
3875
+ terminateProcessSync(candidate.pid, { gracefulTimeoutMs, log });
3876
+ killed = true;
3877
+ }
3878
+ removeTuiPidFile(stateDir);
3879
+ return killed;
3880
+ }
3881
+ function readTuiPid2(stateDir) {
3882
+ try {
3883
+ const raw = readFileSync10(stateDir.tuiPidFile, "utf-8").trim();
3884
+ if (!raw)
3885
+ return null;
3886
+ const pid = Number.parseInt(raw, 10);
3887
+ return Number.isFinite(pid) ? pid : null;
3888
+ } catch {
3889
+ return null;
3890
+ }
3891
+ }
3892
+ function removeTuiPidFile(stateDir) {
3893
+ try {
3894
+ unlinkSync5(stateDir.tuiPidFile);
3895
+ } catch {}
3896
+ }
3897
+ function isManagedCodexTuiProcess2(pid, proxyUrl) {
3898
+ const cmd = commandForPid(pid);
3899
+ return cmd !== null && commandMatchesManagedCodexTui(cmd, proxyUrl);
3900
+ }
3901
+ var init_kill = __esm(() => {
3902
+ init_daemon_lifecycle();
3903
+ init_pair_registry();
3904
+ init_pair_resolver();
3905
+ init_process_lifecycle();
3906
+ init_state_dir();
3907
+ });
3908
+
3909
+ // src/cli/pairs.ts
3910
+ var exports_pairs = {};
3911
+ __export(exports_pairs, {
3912
+ runPairs: () => runPairs
3913
+ });
3914
+ import { join as join13 } from "path";
3915
+ async function runPairs(args = []) {
3916
+ const [command, ...rest] = args;
3917
+ if (command === "rm") {
3918
+ await runRemove(rest);
3919
+ return;
3920
+ }
3921
+ if (command === "prune") {
3922
+ await runPrune(rest);
3923
+ return;
3924
+ }
3925
+ if (command && command !== "list" && command !== "--json" && command !== "--threads") {
3926
+ console.error(`Unknown pairs command: ${command}`);
3927
+ console.error("Usage: abg pairs [--json] [--threads] | abg pairs rm <name|id> | abg pairs prune [--dry-run]");
3928
+ process.exit(1);
3929
+ }
3930
+ const json = command === "--json" || rest.includes("--json");
3931
+ const includeThreads = rest.includes("--threads") || args.includes("--threads");
3932
+ const rows = await collectRows();
3933
+ if (json) {
3934
+ console.log(JSON.stringify(rows, null, 2));
3935
+ return;
3936
+ }
3937
+ printTable(rows, { includeThreads });
3938
+ }
3939
+ async function runRemove(args) {
3940
+ const flag = args[0];
3941
+ if (!flag) {
3942
+ console.error("Error: `abg pairs rm <name|id>` requires a pair name or id.");
3943
+ process.exit(1);
3944
+ }
3945
+ const base = computeBaseDir();
3946
+ let pair;
3947
+ try {
3948
+ pair = findPairForFlag(base, process.cwd(), flag);
3949
+ } catch (err) {
3950
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
3951
+ process.exit(1);
3952
+ }
3953
+ if (!pair) {
3954
+ console.log(`No such pair: "${flag}" in ${process.cwd()}`);
3955
+ printKnownPairs2(base);
3956
+ return;
3957
+ }
3958
+ const stop = await stopPairEntry(base, pair);
3959
+ if (stop.error) {
3960
+ console.error(`Error: failed to stop pair ${pair.pairId}; leaving it registered. ` + `${stop.error instanceof Error ? stop.error.message : String(stop.error)}`);
3961
+ process.exit(1);
3962
+ }
3963
+ let result;
3964
+ try {
3965
+ result = await removePairEntryAndDir(base, pair.pairId);
3966
+ } catch (err) {
3967
+ console.error(`Error: could not delete state dir for ${pair.pairId}; registry entry kept \u2014 retry or run \`abg pairs prune\`. ` + `${err instanceof Error ? err.message : String(err)}`);
3968
+ process.exit(1);
3969
+ }
3970
+ if (result.keptLive) {
3971
+ console.log(`Pair ${pair.pairId} is live again (relaunched concurrently); not removed. Stop it first, then retry.`);
3972
+ return;
3973
+ }
3974
+ const dirNote = result.dirRemoved ? " State directory deleted." : "";
3975
+ if (result.entry) {
3976
+ console.log(`Removed pair ${result.entry.pairId}; slot ${result.entry.slot} is now available.${dirNote}`);
3977
+ } else {
3978
+ console.log(`Pair ${pair.pairId} was already absent from the registry.${dirNote}`);
3979
+ }
3980
+ }
3981
+ async function runPrune(args) {
3982
+ const dryRun = args.includes("--dry-run");
3983
+ for (const arg of args) {
3984
+ if (arg !== "--dry-run") {
3985
+ console.error(`Unknown prune argument: ${arg}`);
3986
+ console.error("Usage: abg pairs prune [--dry-run]");
3987
+ process.exit(1);
3988
+ }
3989
+ }
3990
+ const base = computeBaseDir();
3991
+ const registered = new Set(listPairs(base).map((pair) => pair.pairId.toLowerCase()));
3992
+ const removed = [];
3993
+ const kept = [];
3994
+ for (const name of listPairDirs(base)) {
3995
+ let id;
3996
+ try {
3997
+ id = validatePairId(name);
3998
+ } catch {
3999
+ kept.push({ name, reason: "not a managed pair-id directory" });
4000
+ continue;
4001
+ }
4002
+ if (id !== name) {
4003
+ kept.push({ name, reason: "directory name is not a canonical pair id" });
4004
+ continue;
4005
+ }
4006
+ if (registered.has(id.toLowerCase())) {
4007
+ kept.push({ name, reason: "registered \u2014 use `abg pairs rm`" });
4008
+ continue;
4009
+ }
4010
+ if (pairDirDaemonAlive(base, id)) {
4011
+ kept.push({ name, reason: "daemon still alive" });
4012
+ continue;
4013
+ }
4014
+ if (dryRun) {
4015
+ removed.push(name);
4016
+ continue;
4017
+ }
4018
+ try {
4019
+ const outcome = await removeUnregisteredPairDir(base, id);
4020
+ if (outcome.removed) {
4021
+ removed.push(name);
4022
+ } else if (outcome.reason === "registered") {
4023
+ kept.push({ name, reason: "registered during prune \u2014 use `abg pairs rm`" });
4024
+ } else if (outcome.reason === "live") {
4025
+ kept.push({ name, reason: "daemon became live during prune" });
4026
+ } else {
4027
+ kept.push({ name, reason: "already gone" });
4028
+ }
4029
+ } catch (err) {
4030
+ kept.push({ name, reason: `error: ${err instanceof Error ? err.message : String(err)}` });
4031
+ }
4032
+ }
4033
+ printPruneSummary(removed, kept, dryRun);
4034
+ }
4035
+ function printPruneSummary(removed, kept, dryRun) {
4036
+ if (removed.length === 0 && kept.length === 0) {
4037
+ console.log("No pair directories found.");
4038
+ return;
4039
+ }
4040
+ if (removed.length > 0) {
4041
+ console.log(dryRun ? "Would remove orphan pair directories:" : "Removed orphan pair directories:");
4042
+ for (const name of removed)
4043
+ console.log(` ${name}`);
4044
+ } else {
4045
+ console.log(dryRun ? "No orphan pair directories to remove." : "No orphan pair directories removed.");
4046
+ }
4047
+ if (kept.length > 0) {
4048
+ console.log("Kept:");
4049
+ for (const { name, reason } of kept)
4050
+ console.log(` ${name} (${reason})`);
4051
+ }
4052
+ if (dryRun) {
4053
+ console.log(`
4054
+ (dry run \u2014 nothing was deleted. Re-run without --dry-run to apply.)`);
4055
+ }
4056
+ }
4057
+ async function collectRows() {
4058
+ const base = computeBaseDir();
4059
+ const rows = await Promise.all(listPairs(base).map((pair) => rowForPair(base, pair)));
4060
+ const legacy = detectLegacyRootDaemon(base);
4061
+ if (legacy) {
4062
+ rows.push({
4063
+ pairId: "(legacy-root)",
4064
+ name: "-",
4065
+ slot: null,
4066
+ ports: { appPort: 4500, proxyPort: 4501, controlPort: legacy.controlPort },
4067
+ source: "legacy",
4068
+ cwd: base,
4069
+ running: true,
4070
+ pid: legacy.pid,
4071
+ threadId: null,
4072
+ threadStatus: null,
4073
+ threadUpdatedAt: null
4074
+ });
4075
+ }
4076
+ return rows;
4077
+ }
4078
+ async function rowForPair(base, pair) {
4079
+ const ports = portsForEntry(pair);
4080
+ const stateDir = new StateDirResolver(join13(base, "pairs", pair.pairId));
4081
+ const lifecycle = new DaemonLifecycle({
4082
+ stateDir,
4083
+ controlPort: ports.controlPort,
4084
+ log: () => {}
4085
+ });
4086
+ const [running, status] = await Promise.all([
4087
+ lifecycle.isHealthy(),
4088
+ Promise.resolve(lifecycle.readStatus())
4089
+ ]);
4090
+ const thread = readRawCurrentThread(stateDir);
4091
+ return {
4092
+ pairId: pair.pairId,
4093
+ name: pair.name ?? "-",
4094
+ slot: pair.slot,
4095
+ ports,
4096
+ source: pair.source,
4097
+ cwd: pair.cwd,
4098
+ running,
4099
+ pid: typeof status?.pid === "number" ? status.pid : null,
4100
+ threadId: thread?.threadId ?? null,
4101
+ threadStatus: thread?.status ?? null,
4102
+ threadUpdatedAt: thread?.updatedAt ?? null
4103
+ };
4104
+ }
4105
+ function printTable(rows, options = {}) {
4106
+ if (rows.length === 0) {
4107
+ console.log("No pairs registered.");
4108
+ return;
4109
+ }
4110
+ const data = rows.map((row) => ({
4111
+ name: row.name,
4112
+ pairId: row.pairId,
4113
+ slot: row.slot === null ? "-" : String(row.slot),
4114
+ ports: `${row.ports.appPort}/${row.ports.proxyPort}/${row.ports.controlPort}`,
4115
+ source: row.source,
4116
+ cwd: row.cwd,
4117
+ status: row.running ? "running" : "stopped",
4118
+ pid: row.pid === null ? "-" : String(row.pid),
4119
+ thread: row.threadId === null ? "-" : row.threadId,
4120
+ threadStatus: row.threadStatus === null ? "-" : row.threadStatus
4121
+ }));
4122
+ const headers = {
4123
+ name: "name",
4124
+ pairId: "pairId",
4125
+ slot: "slot",
4126
+ ports: "app/proxy/control",
4127
+ source: "source",
4128
+ status: "status",
4129
+ pid: "pid",
4130
+ thread: "threadId",
4131
+ threadStatus: "thread",
4132
+ cwd: "cwd"
4133
+ };
4134
+ const visibleKeys = options.includeThreads ? ["name", "pairId", "slot", "ports", "source", "status", "pid", "thread", "threadStatus", "cwd"] : ["name", "pairId", "slot", "ports", "source", "status", "pid", "cwd"];
4135
+ const widths = Object.fromEntries(visibleKeys.map((key) => [
4136
+ key,
4137
+ Math.max(headers[key].length, ...data.map((row) => row[key].length))
4138
+ ]));
4139
+ const line = (row) => visibleKeys.map((key) => row[key].padEnd(widths[key])).join(" ");
4140
+ console.log(line(headers));
4141
+ console.log(visibleKeys.map((key) => "-".repeat(widths[key])).join(" "));
4142
+ for (const row of data) {
4143
+ console.log(line(row));
4144
+ }
4145
+ }
4146
+ function printKnownPairs2(base) {
4147
+ const pairs = listPairs(base);
4148
+ if (pairs.length === 0) {
4149
+ console.log("No pairs registered.");
4150
+ return;
4151
+ }
4152
+ console.log("Known pairs:");
4153
+ for (const pair of pairs) {
4154
+ console.log(` ${pair.pairId}`);
4155
+ }
4156
+ }
4157
+ var init_pairs = __esm(() => {
4158
+ init_daemon_lifecycle();
4159
+ init_pair_registry();
4160
+ init_pair_resolver();
4161
+ init_state_dir();
4162
+ init_thread_state();
4163
+ init_kill();
4164
+ });
4165
+
4166
+ // src/daemon-status.ts
4167
+ async function fetchDaemonStatus(port, path = "/healthz", timeoutMs = DAEMON_STATUS_FETCH_TIMEOUT_MS) {
4168
+ const controller = new AbortController;
4169
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4170
+ try {
4171
+ const response = await fetch(`http://127.0.0.1:${port}${path}`, { signal: controller.signal });
4172
+ if (!response.ok)
4173
+ return null;
4174
+ return await response.json();
4175
+ } catch {
4176
+ return null;
4177
+ } finally {
4178
+ clearTimeout(timer);
4179
+ }
4180
+ }
4181
+ var DAEMON_STATUS_FETCH_TIMEOUT_MS = 1000;
4182
+
4183
+ // src/resume-pollution.ts
4184
+ import { Database } from "bun:sqlite";
4185
+ import {
4186
+ copyFileSync,
4187
+ existsSync as existsSync11,
4188
+ mkdirSync as mkdirSync7,
4189
+ readFileSync as readFileSync11
4190
+ } from "fs";
4191
+ import { dirname as dirname5, join as join14 } from "path";
4192
+ function isKickoffText(text) {
4193
+ if (!text)
4194
+ return false;
4195
+ return KICKOFF_FINGERPRINTS.some((fingerprint) => text.includes(fingerprint));
1146
4196
  }
1147
- async function waitForProxyReady(proxyUrl, maxRetries = 20, delayMs = 100) {
1148
- const healthUrl = proxyHealthUrl(proxyUrl);
1149
- for (let attempt = 0;attempt < maxRetries; attempt++) {
4197
+ function extractFirstRealUserMessage(rolloutPath) {
4198
+ if (!existsSync11(rolloutPath))
4199
+ return null;
4200
+ const raw = readFileSync11(rolloutPath, "utf-8");
4201
+ for (const line of raw.split(`
4202
+ `)) {
4203
+ if (!line.trim())
4204
+ continue;
4205
+ let entry;
1150
4206
  try {
1151
- const response = await fetch(healthUrl);
1152
- if (response.ok) {
1153
- return;
4207
+ entry = JSON.parse(line);
4208
+ } catch {
4209
+ continue;
4210
+ }
4211
+ const message = extractUserText(entry);
4212
+ if (!message)
4213
+ continue;
4214
+ if (isSyntheticUserMessage(message))
4215
+ continue;
4216
+ return message.trim();
4217
+ }
4218
+ return null;
4219
+ }
4220
+ function scanResumePollution(options = {}) {
4221
+ const codexHome2 = options.codexHome ?? codexHome();
4222
+ const dbPath = options.dbPath ?? join14(codexHome2, "state_5.sqlite");
4223
+ if (!existsSync11(dbPath)) {
4224
+ return { codexHome: codexHome2, dbPath, scanned: 0, candidates: [], applied: 0, renamed: 0, deleted: 0 };
4225
+ }
4226
+ const db = options.apply ? new Database(dbPath) : new Database(dbPath, { readonly: true });
4227
+ try {
4228
+ const rows = db.query(`
4229
+ select id, cwd, rollout_path as rolloutPath, title,
4230
+ first_user_message as firstUserMessage, preview
4231
+ from threads
4232
+ where archived = 0 and (
4233
+ first_user_message like '%AgentBridge%' or
4234
+ first_user_message like '%multi-agent collaboration%' or
4235
+ title like '%AgentBridge%' or
4236
+ title like '%multi-agent collaboration%' or
4237
+ preview like '%AgentBridge%' or
4238
+ preview like '%multi-agent collaboration%'
4239
+ )
4240
+ order by updated_at desc
4241
+ `).all();
4242
+ const candidates = [];
4243
+ for (const row of rows) {
4244
+ const pollutedFields = [
4245
+ isKickoffText(row.title) ? "title" : null,
4246
+ isKickoffText(row.firstUserMessage) ? "first_user_message" : null,
4247
+ isKickoffText(row.preview) ? "preview" : null
4248
+ ].filter(Boolean);
4249
+ if (pollutedFields.length === 0)
4250
+ continue;
4251
+ const realMessage = extractFirstRealUserMessage(row.rolloutPath);
4252
+ if (!realMessage) {
4253
+ candidates.push({
4254
+ id: row.id,
4255
+ cwd: row.cwd,
4256
+ rolloutPath: row.rolloutPath,
4257
+ title: row.title,
4258
+ firstUserMessage: row.firstUserMessage,
4259
+ preview: row.preview,
4260
+ action: "delete",
4261
+ replacementTitle: row.title,
4262
+ replacementFirstUserMessage: row.firstUserMessage,
4263
+ replacementPreview: row.preview,
4264
+ reason: `kickoff-only, polluted ${pollutedFields.join(", ")}`
4265
+ });
4266
+ continue;
1154
4267
  }
1155
- } catch {}
1156
- await new Promise((resolve2) => setTimeout(resolve2, delayMs));
4268
+ candidates.push({
4269
+ id: row.id,
4270
+ cwd: row.cwd,
4271
+ rolloutPath: row.rolloutPath,
4272
+ title: row.title,
4273
+ firstUserMessage: row.firstUserMessage,
4274
+ preview: row.preview,
4275
+ action: "rename",
4276
+ replacementTitle: isKickoffText(row.title) ? sidebarTitle(realMessage) : row.title,
4277
+ replacementFirstUserMessage: isKickoffText(row.firstUserMessage) ? realMessage : row.firstUserMessage,
4278
+ replacementPreview: isKickoffText(row.preview) ? previewText(realMessage) : row.preview,
4279
+ reason: `polluted ${pollutedFields.join(", ")}`
4280
+ });
4281
+ }
4282
+ let renamed = 0;
4283
+ let deleted = 0;
4284
+ let backupDir;
4285
+ if (options.apply && candidates.length > 0) {
4286
+ backupDir = backupCodexStateFiles(dbPath, options.now);
4287
+ const update = db.prepare(`
4288
+ update threads
4289
+ set title = ?,
4290
+ first_user_message = ?,
4291
+ preview = ?
4292
+ where id = ?
4293
+ `);
4294
+ const remove = db.prepare(`delete from threads where id = ?`);
4295
+ const tx = db.transaction((items) => {
4296
+ for (const item of items) {
4297
+ if (item.action === "delete") {
4298
+ remove.run(item.id);
4299
+ deleted++;
4300
+ } else {
4301
+ update.run(item.replacementTitle, item.replacementFirstUserMessage, item.replacementPreview, item.id);
4302
+ renamed++;
4303
+ }
4304
+ }
4305
+ });
4306
+ tx(candidates);
4307
+ }
4308
+ const applied = renamed + deleted;
4309
+ return { codexHome: codexHome2, dbPath, scanned: rows.length, candidates, applied, renamed, deleted, backupDir };
4310
+ } finally {
4311
+ db.close();
1157
4312
  }
1158
- throw new Error(`Timed out waiting for Codex proxy readiness on ${healthUrl}`);
1159
4313
  }
1160
- var OWNED_FLAGS2;
1161
- var init_codex = __esm(() => {
1162
- init_state_dir();
1163
- init_config_service();
1164
- init_daemon_lifecycle();
1165
- init_stderr_ring_buffer();
1166
- init_claude();
1167
- OWNED_FLAGS2 = ["--remote"];
4314
+ function backupCodexStateFiles(dbPath, now = new Date().toISOString()) {
4315
+ const safeStamp = now.replace(/[:.]/g, "-");
4316
+ const base = join14(dirname5(dbPath), "agentbridge-backups", `resume-pollution-${safeStamp}`);
4317
+ mkdirSync7(base, { recursive: true });
4318
+ for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
4319
+ if (!existsSync11(path))
4320
+ continue;
4321
+ const target = join14(base, path.split("/").pop());
4322
+ mkdirSync7(dirname5(target), { recursive: true });
4323
+ copyFileSync(path, target);
4324
+ }
4325
+ return base;
4326
+ }
4327
+ function extractUserText(entry) {
4328
+ if (entry?.type === "event_msg" && entry.payload?.type === "user_message") {
4329
+ return typeof entry.payload.message === "string" ? entry.payload.message : null;
4330
+ }
4331
+ if (entry?.type === "response_item" && entry.payload?.type === "message" && entry.payload?.role === "user") {
4332
+ const content = entry.payload.content;
4333
+ if (!Array.isArray(content))
4334
+ return null;
4335
+ const parts = content.map((item) => typeof item?.text === "string" ? item.text : typeof item?.input_text?.text === "string" ? item.input_text.text : null).filter((part) => !!part);
4336
+ return parts.length > 0 ? parts.join(`
4337
+ `) : null;
4338
+ }
4339
+ return null;
4340
+ }
4341
+ function isSyntheticUserMessage(message) {
4342
+ return isKickoffText(message) || message.includes("AGENTS.md instructions for") || message.includes("<environment_context>");
4343
+ }
4344
+ function sidebarTitle(message) {
4345
+ return compact(message).slice(0, 80);
4346
+ }
4347
+ function previewText(message) {
4348
+ return compact(message).slice(0, 160);
4349
+ }
4350
+ function compact(message) {
4351
+ return message.replace(/\s+/g, " ").trim();
4352
+ }
4353
+ var KICKOFF_FINGERPRINTS;
4354
+ var init_resume_pollution = __esm(() => {
4355
+ init_thread_state();
4356
+ KICKOFF_FINGERPRINTS = [
4357
+ "Claude Code has connected via AgentBridge",
4358
+ "You are now in a multi-agent collaboration session",
4359
+ "When you receive a complex task, propose a division of labor to Claude"
4360
+ ];
1168
4361
  });
1169
4362
 
1170
- // src/cli/kill.ts
1171
- var exports_kill = {};
1172
- __export(exports_kill, {
1173
- runKill: () => runKill
4363
+ // src/cli/doctor.ts
4364
+ var exports_doctor = {};
4365
+ __export(exports_doctor, {
4366
+ runDoctor: () => runDoctor,
4367
+ formatDoctorReport: () => formatDoctorReport
1174
4368
  });
1175
- import { execFileSync as execFileSync5 } from "child_process";
1176
- import { readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "fs";
1177
- async function runKill() {
1178
- console.log(`AgentBridge Kill \u2014 stopping daemon and managed Codex TUI
1179
- `);
1180
- const stateDir = new StateDirResolver;
1181
- const controlPort = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
1182
- const lifecycle = new DaemonLifecycle({
1183
- stateDir,
1184
- controlPort,
1185
- log: (msg) => console.log(` ${msg}`)
1186
- });
1187
- lifecycle.markKilled();
1188
- const tuiKilled = await killManagedCodexTui(stateDir, (msg) => console.log(` ${msg}`));
1189
- const killed = await lifecycle.kill();
1190
- if (killed || tuiKilled) {
1191
- console.log(`
1192
- AgentBridge stopped.`);
1193
- console.log("Please restart Claude Code (`agentbridge claude`), switch to a new conversation, or run `/resume` to fully disconnect.");
1194
- } else {
1195
- console.log(`
1196
- No running AgentBridge daemon or managed Codex TUI found.`);
1197
- console.log("Stale state files cleaned up (if any).");
4369
+ import { existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync4, realpathSync as realpathSync3, statSync as statSync5 } from "fs";
4370
+ import { homedir as homedir5 } from "os";
4371
+ import { join as join15 } from "path";
4372
+ async function runDoctor(args = []) {
4373
+ if (args[0] === "resume-pollution") {
4374
+ runResumePollution(args.slice(1));
4375
+ return;
4376
+ }
4377
+ const json = args.includes("--json");
4378
+ const agent = args.includes("--agent");
4379
+ const { pairFlag, rest } = parsePairFlag(args.filter((arg) => arg !== "--json" && arg !== "--agent"));
4380
+ const unknown = rest.filter((arg) => arg.startsWith("-"));
4381
+ if (unknown.length > 0) {
4382
+ console.error(`Unknown doctor option(s): ${unknown.join(", ")}`);
4383
+ console.error("Usage: abg doctor [--pair <name|id>] [--json] [--agent]");
4384
+ process.exit(1);
4385
+ }
4386
+ let resolution;
4387
+ try {
4388
+ resolution = resolvePairReadOnly(pairFlag);
4389
+ } catch (err) {
4390
+ console.error(`[agentbridge] ${err.message}`);
4391
+ process.exit(1);
4392
+ }
4393
+ const report = await buildDoctorReport(resolution.pair, resolution.registered);
4394
+ if (agent) {
4395
+ report.checks.push({
4396
+ name: "agent backend",
4397
+ status: "warn",
4398
+ detail: "--agent is reserved for read-only delegated analysis; static diagnostics were run locally in this build."
4399
+ });
1198
4400
  }
4401
+ if (report.checks.some((check) => check.status === "fail")) {
4402
+ process.exitCode = 1;
4403
+ }
4404
+ if (json) {
4405
+ console.log(JSON.stringify(report, null, 2));
4406
+ return;
4407
+ }
4408
+ printDoctorReport(report);
1199
4409
  }
1200
- async function killManagedCodexTui(stateDir, log, gracefulTimeoutMs = 3000) {
1201
- const pid = readTuiPid(stateDir);
1202
- if (!pid) {
1203
- log("No Codex TUI pid file found");
1204
- removeTuiPidFile(stateDir);
1205
- return false;
4410
+ function runResumePollution(args) {
4411
+ const json = args.includes("--json");
4412
+ const apply = args.includes("--apply");
4413
+ const codexHomeIndex = args.indexOf("--codex-home");
4414
+ const codexHome2 = codexHomeIndex >= 0 ? args[codexHomeIndex + 1] : undefined;
4415
+ if (codexHomeIndex >= 0 && !codexHome2) {
4416
+ console.error("Usage: abg doctor resume-pollution [--json] [--apply] [--codex-home <path>]");
4417
+ process.exit(1);
1206
4418
  }
1207
- if (!isProcessAlive(pid)) {
1208
- log(`Codex TUI pid ${pid} is not alive, cleaning up stale pid file`);
1209
- removeTuiPidFile(stateDir);
1210
- return false;
4419
+ const report = scanResumePollution({ codexHome: codexHome2, apply });
4420
+ if (json) {
4421
+ console.log(JSON.stringify(report, null, 2));
4422
+ return;
1211
4423
  }
1212
- if (!isManagedCodexTuiProcess(pid)) {
1213
- log(`Pid ${pid} is alive but is NOT a managed AgentBridge Codex TUI \u2014 refusing to kill. Cleaning up stale pid file.`);
1214
- removeTuiPidFile(stateDir);
1215
- return false;
4424
+ const renameCount = report.candidates.filter((c) => c.action === "rename").length;
4425
+ const deleteCount = report.candidates.filter((c) => c.action === "delete").length;
4426
+ console.log(`Resume pollution scan: ${report.candidates.length} candidate(s) ` + `(${renameCount} rename, ${deleteCount} delete) from ${report.dbPath}`);
4427
+ if (report.backupDir)
4428
+ console.log(`Backup: ${report.backupDir}`);
4429
+ for (const candidate of report.candidates) {
4430
+ if (candidate.action === "delete") {
4431
+ const verb = apply ? "deleted" : "would delete";
4432
+ console.log(`${verb} ${candidate.id}: ${candidate.reason}`);
4433
+ } else {
4434
+ const verb = apply ? "updated" : "would rename";
4435
+ console.log(`${verb} ${candidate.id}: ${candidate.reason}`);
4436
+ console.log(` title: ${candidate.replacementTitle}`);
4437
+ }
1216
4438
  }
1217
- log(`Sending SIGTERM to Codex TUI pid ${pid}`);
1218
- try {
1219
- process.kill(pid, "SIGTERM");
1220
- } catch {
1221
- removeTuiPidFile(stateDir);
1222
- return false;
4439
+ if (apply) {
4440
+ console.log(`Applied: ${report.renamed} renamed, ${report.deleted} deleted.`);
4441
+ } else if (report.candidates.length > 0) {
4442
+ console.log("Dry-run only. Re-run with --apply to rename/delete Codex sessions after backing up state.");
1223
4443
  }
1224
- const deadline = Date.now() + gracefulTimeoutMs;
1225
- while (Date.now() < deadline) {
1226
- if (!isProcessAlive(pid)) {
1227
- log(`Codex TUI pid ${pid} stopped gracefully`);
1228
- removeTuiPidFile(stateDir);
1229
- return true;
4444
+ }
4445
+ async function buildDoctorReport(pair, registered) {
4446
+ const cwd = process.cwd();
4447
+ const env = inspectAgentBridgeEnv({ cwd, env: process.env });
4448
+ const [health, ready] = registered ? await Promise.all([
4449
+ fetchDaemonStatus(pair.ports.controlPort, "/healthz"),
4450
+ fetchDaemonStatus(pair.ports.controlPort, "/readyz")
4451
+ ]) : [null, null];
4452
+ const launcherStamped = BUILD_INFO.commit !== "source";
4453
+ const buildDrift = !launcherStamped ? null : health?.build ? !sameRuntimeContract(health.build, BUILD_INFO) : health ? true : null;
4454
+ const rawThread = readRawCurrentThread(pair.stateDir);
4455
+ const usableThread = readUsableCurrentThread({
4456
+ stateDir: pair.stateDir,
4457
+ pairId: pair.manual ? null : pair.pairId,
4458
+ pairName: pair.name,
4459
+ cwd
4460
+ });
4461
+ const checks = [];
4462
+ checks.push({
4463
+ name: "pair registration",
4464
+ status: registered ? "ok" : "warn",
4465
+ detail: registered ? pair.manual ? "manual mode (explicit env)" : `registered as ${pair.pairId}` : `not registered yet \u2014 would be ${pair.pairId} (created on first launch)`,
4466
+ hint: registered ? undefined : "\u8BE5\u76EE\u5F55\u8FD8\u6CA1\u6709\u6CE8\u518C\u8FC7 pair\uFF1A\u8FD0\u884C `agentbridge claude` \u5373\u4F1A\u521B\u5EFA\u3002\u4EE5\u4E0B\u68C0\u67E5\u6309\u672A\u542F\u52A8\u72B6\u6001\u89E3\u8BFB\u3002"
4467
+ });
4468
+ checks.push({
4469
+ name: "env",
4470
+ status: env.ok ? "ok" : "fail",
4471
+ detail: env.ok ? "AgentBridge env matches cwd" : env.reasons.join("; "),
4472
+ 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"
4473
+ });
4474
+ checks.push({
4475
+ name: "daemon health",
4476
+ status: health ? "ok" : "warn",
4477
+ detail: health ? `healthz reachable pid=${health.pid}` : registered ? `no daemon reachable on :${pair.ports.controlPort}` : "n/a \u2014 pair not registered",
4478
+ hint: health ? undefined : "daemon \u672A\u8FD0\u884C\u3002\u8FD0\u884C `agentbridge claude`\uFF08\u6216 `agentbridge codex`\uFF09\u4F1A\u81EA\u52A8\u542F\u52A8\u5B83\u3002"
4479
+ });
4480
+ checks.push({
4481
+ name: "daemon readiness",
4482
+ status: ready ? "ok" : health ? "warn" : "skip",
4483
+ detail: ready ? `ready thread=${ready.threadId ?? "none"}` : health ? "readyz is not OK" : "n/a \u2014 daemon not running",
4484
+ hint: !ready && health ? "daemon \u5728\u8FD0\u884C\u4F46 codex app-server \u5C1A\u672A\u5C31\u7EEA\uFF1B\u7A0D\u5019\u7247\u523B\u91CD\u8BD5\uFF0C\u6301\u7EED\u4E0D\u5C31\u7EEA\u8BF7\u67E5\u770B\u4E0B\u65B9 daemon log\u3002" : undefined
4485
+ });
4486
+ checks.push({
4487
+ name: "build drift",
4488
+ status: buildDrift === false ? "ok" : buildDrift === true ? "fail" : "skip",
4489
+ detail: buildDrift === false ? `runtime matches launcher ${formatBuildInfo(BUILD_INFO)}` : buildDrift === true ? `runtime ${formatBuildInfo(health?.build)} differs from launcher ${formatBuildInfo(BUILD_INFO)}` : launcherStamped ? "n/a \u2014 daemon not running" : "n/a \u2014 launcher running from source (unstamped)",
4490
+ hint: buildDrift === true ? "daemon \u8FD0\u884C\u7684\u662F\u65E7\u6784\u5EFA\uFF08\u901A\u5E38\u7531\u65E7\u7248 CLI \u6216\u672A\u91CD\u5F00\u7684 Claude Code \u7A97\u53E3\u542F\u52A8\uFF09\u3002" + "\u6CA1\u6709\u8FDB\u884C\u4E2D\u7684 Codex \u4F1A\u8BDD\u65F6\uFF0C\u8FD0\u884C `abg kill` \u540E\u91CD\u65B0 `agentbridge claude` \u5373\u53EF\u5BF9\u9F50\uFF1B" + "\u6709\u6D3B\u8DC3\u4F1A\u8BDD\u5219\u7B49\u6536\u5C3E\u540E\u518D\u91CD\u542F\u2014\u2014\u7248\u672C\u5DEE\u5F02\u4E0D\u4F1A\u5F3A\u6740\u6D3B\u8DC3\u4F1A\u8BDD\uFF0C\u53EF\u4EE5\u7EE7\u7EED\u7528\u3002" : undefined
4491
+ });
4492
+ checks.push(artifactAlignmentCheck());
4493
+ checks.push({
4494
+ name: "current thread",
4495
+ status: usableThread ? "ok" : rawThread ? "warn" : registered ? "warn" : "skip",
4496
+ detail: usableThread ? `current=${usableThread.threadId}` : rawThread ? rawThread.status === "current" ? `stored ${rawThread.threadId} has no rollout file yet` : `stored ${rawThread.threadId} is still ${rawThread.status} (no first response yet)` : registered ? "no current-thread.json for this pair" : "n/a \u2014 pair not registered",
4497
+ hint: usableThread ? undefined : rawThread ? "\u901A\u5E38\u65E0\u5BB3\uFF1A\u7EBF\u7A0B\u8FD8\u6CA1\u6709\u4EA7\u751F\u9996\u6761\u56DE\u5E94\u3001\u6216 rollout \u6587\u4EF6\u5C1A\u672A\u843D\u76D8\u3002" + "\u4EC5\u5F53 `abg codex`\uFF08resume\uFF09\u5931\u8D25\u65F6\u624D\u9700\u8981\u5904\u7406\uFF1A\u7528 `abg codex --new` \u5F00\u65B0\u7EBF\u7A0B\u3002" : registered ? "\u5C1A\u65E0\u7EBF\u7A0B\u8BB0\u5F55\uFF1A\u8FDE\u63A5 Codex \u540E\u5EFA\u7ACB\u9996\u4E2A\u7EBF\u7A0B\u65F6\u4F1A\u81EA\u52A8\u5199\u5165\uFF0C\u65E0\u9700\u5904\u7406\u3002" : undefined
4498
+ });
4499
+ const pairProxyUrl = `ws://127.0.0.1:${pair.ports.proxyPort}`;
4500
+ const managedTuis = listManagedCodexTuiProcesses();
4501
+ const attachedHere = [];
4502
+ const attachedElsewhere = [];
4503
+ for (const tui of managedTuis) {
4504
+ if (commandMatchesManagedCodexTui(tui.command, pairProxyUrl)) {
4505
+ attachedHere.push(tui);
4506
+ } else {
4507
+ attachedElsewhere.push(tui);
1230
4508
  }
1231
- await new Promise((resolve2) => setTimeout(resolve2, 200));
1232
4509
  }
1233
- log(`Codex TUI pid ${pid} did not stop gracefully, sending SIGKILL`);
4510
+ checks.push({
4511
+ name: "codex tui (this pair)",
4512
+ status: attachedHere.length > 0 ? "ok" : "warn",
4513
+ detail: attachedHere.length > 0 ? `${attachedHere.length} attached to ${pairProxyUrl} (pid ${attachedHere.map((t) => t.pid).join(", ")})` : `no managed Codex TUI attached to this pair's proxy ${pairProxyUrl}`,
4514
+ hint: attachedHere.length > 0 ? undefined : "\u53E6\u5F00\u4E00\u4E2A\u7EC8\u7AEF\u3001\u5728\u540C\u4E00\u76EE\u5F55\u8FD0\u884C `agentbridge codex` \u8FDE\u63A5\u672C pair\u3002"
4515
+ });
4516
+ checks.push({
4517
+ name: "codex tui (other pairs)",
4518
+ status: attachedElsewhere.length > 0 ? "warn" : "ok",
4519
+ detail: attachedElsewhere.length > 0 ? `${attachedElsewhere.length} managed Codex TUI(s) attached to a DIFFERENT pair/proxy \u2014 likely started from another cwd, will not bridge here: ` + attachedElsewhere.map((t) => `pid ${t.pid}\u2192${t.remoteUrl ?? "?"}`).join(", ") : "no managed Codex TUI attached to another pair",
4520
+ hint: attachedElsewhere.length > 0 ? "\u8FD9\u4E9B TUI \u5C5E\u4E8E\u5176\u4ED6\u76EE\u5F55\u7684 pair\uFF0C\u4E0D\u5F71\u54CD\u672C pair\uFF1B\u5B83\u4EEC\u4E0D\u4F1A\u6865\u63A5\u5230\u8FD9\u91CC\u3002\u5982\u4E0D\u518D\u9700\u8981\uFF0C\u53BB\u5BF9\u5E94\u76EE\u5F55\u8FD0\u884C `abg kill`\u3002" : undefined
4521
+ });
4522
+ for (const [name, path] of [
4523
+ ["daemon log", pair.stateDir.logFile],
4524
+ ["codex wrapper log", pair.stateDir.codexWrapperLogFile]
4525
+ ]) {
4526
+ checks.push(logCheck(name, path));
4527
+ }
4528
+ return {
4529
+ cwd,
4530
+ pair: {
4531
+ pairId: pair.pairId,
4532
+ name: pair.name,
4533
+ manual: pair.manual,
4534
+ slot: pair.slot,
4535
+ stateDir: pair.stateDir.dir,
4536
+ ports: pair.ports
4537
+ },
4538
+ env,
4539
+ daemon: { health, ready, buildDrift },
4540
+ tui: {
4541
+ attachedHere: attachedHere.map((t) => ({ pid: t.pid, remoteUrl: t.remoteUrl })),
4542
+ attachedElsewhere: attachedElsewhere.map((t) => ({ pid: t.pid, remoteUrl: t.remoteUrl }))
4543
+ },
4544
+ checks
4545
+ };
4546
+ }
4547
+ function artifactAlignmentCheck() {
4548
+ const stamps = [];
4549
+ if (BUILD_INFO.commit !== "source") {
4550
+ stamps.push({ label: `launcher(${BUILD_INFO.bundle})`, commit: BUILD_INFO.commit });
4551
+ }
4552
+ const bin = Bun.which("agentbridge") ?? Bun.which("abg");
4553
+ if (bin) {
4554
+ try {
4555
+ const commit = extractBundleCommit(realpathSync3(bin));
4556
+ if (commit)
4557
+ stamps.push({ label: "global-cli", commit });
4558
+ } catch {}
4559
+ }
4560
+ const cacheRoot = join15(homedir5(), ".claude", "plugins", "cache", "agentbridge", "agentbridge");
1234
4561
  try {
1235
- process.kill(pid, "SIGKILL");
4562
+ for (const version of readdirSync4(cacheRoot)) {
4563
+ const commit = extractBundleCommit(join15(cacheRoot, version, "server", "daemon.js"));
4564
+ if (commit)
4565
+ stamps.push({ label: `plugin-cache@${version}`, commit });
4566
+ }
1236
4567
  } catch {}
1237
- removeTuiPidFile(stateDir);
1238
- return true;
4568
+ const repoBundle = join15(process.cwd(), "plugins", "agentbridge", "server", "daemon.js");
4569
+ if (existsSync12(repoBundle)) {
4570
+ const commit = extractBundleCommit(repoBundle);
4571
+ if (commit)
4572
+ stamps.push({ label: "repo-bundle", commit });
4573
+ }
4574
+ if (stamps.length < 2) {
4575
+ return {
4576
+ name: "artifact alignment",
4577
+ status: "skip",
4578
+ detail: "n/a \u2014 fewer than two stamped artifacts found"
4579
+ };
4580
+ }
4581
+ const commits = new Set(stamps.map((s) => s.commit));
4582
+ const rendered = stamps.map((s) => `${s.label}=${s.commit}`).join(", ");
4583
+ if (commits.size === 1) {
4584
+ return { name: "artifact alignment", status: "ok", detail: rendered };
4585
+ }
4586
+ return {
4587
+ name: "artifact alignment",
4588
+ status: "fail",
4589
+ detail: `deployed artifacts are at DIFFERENT builds: ${rendered}`,
4590
+ hint: "\u90E8\u7F72\u7269\u7248\u672C\u5206\u88C2\u4F1A\u5BFC\u81F4\u4E92\u76F8\u66FF\u6362 daemon\uFF08\u6740\u6389\u6D3B\u4F1A\u8BDD\uFF09\u3002\u5728\u4ED3\u5E93\u76EE\u5F55\u8FD0\u884C `bun run install:global` " + "\u4E00\u6B21\u6027\u5BF9\u9F50\u5168\u5C40 CLI \u4E0E\u63D2\u4EF6\u7F13\u5B58\uFF0C\u7136\u540E\u5173\u95ED\u5E76\u91CD\u5F00\u4ECD\u5728\u4F7F\u7528\u65E7\u63D2\u4EF6\u7684 Claude Code \u7A97\u53E3\u3002"
4591
+ };
1239
4592
  }
1240
- function readTuiPid(stateDir) {
4593
+ function extractBundleCommit(path) {
1241
4594
  try {
1242
- const raw = readFileSync4(stateDir.tuiPidFile, "utf-8").trim();
1243
- if (!raw)
1244
- return null;
1245
- const pid = Number.parseInt(raw, 10);
1246
- return Number.isFinite(pid) ? pid : null;
4595
+ const match = readFileSync12(path, "utf-8").match(/commit:\s*defineString\("([^"]+)",\s*"source"\)/);
4596
+ return match ? match[1] : null;
1247
4597
  } catch {
1248
4598
  return null;
1249
4599
  }
1250
4600
  }
1251
- function removeTuiPidFile(stateDir) {
1252
- try {
1253
- unlinkSync3(stateDir.tuiPidFile);
1254
- } catch {}
4601
+ function logCheck(name, path) {
4602
+ if (!existsSync12(path)) {
4603
+ return {
4604
+ name,
4605
+ status: "warn",
4606
+ detail: `missing: ${path}`,
4607
+ 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"
4608
+ };
4609
+ }
4610
+ const stat = statSync5(path);
4611
+ if (stat.size > LARGE_LOG_WARN_BYTES) {
4612
+ return {
4613
+ name,
4614
+ status: "warn",
4615
+ detail: `${path} (${stat.size} bytes, oversized; stop the pair, rebuild/reinstall, then rotate or remove this log)`,
4616
+ hint: "\u65E5\u5FD7\u8FC7\u5927\uFF1A`abg kill` \u505C\u6B62 pair \u540E\u5220\u9664\u8BE5\u6587\u4EF6\u518D\u91CD\u542F\u5373\u53EF\u3002"
4617
+ };
4618
+ }
4619
+ return { name, status: "ok", detail: `${path} (${stat.size} bytes)` };
1255
4620
  }
1256
- function isManagedCodexTuiProcess(pid) {
1257
- try {
1258
- const cmd = execFileSync5("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
1259
- return cmd.includes("codex") && cmd.includes("--enable") && cmd.includes("tui_app_server") && cmd.includes("--remote");
1260
- } catch {
1261
- return false;
4621
+ function formatDoctorReport(report) {
4622
+ const lines = [];
4623
+ lines.push(`AgentBridge doctor: ${report.pair.pairId}`);
4624
+ lines.push(`cwd: ${report.cwd}`);
4625
+ lines.push(`state: ${report.pair.stateDir}`);
4626
+ lines.push(`ports: ${report.pair.ports.appPort}/${report.pair.ports.proxyPort}/${report.pair.ports.controlPort}`);
4627
+ for (const check of report.checks) {
4628
+ lines.push(`${check.status.toUpperCase().padEnd(4)} ${check.name}: ${check.detail}`);
4629
+ if ((check.status === "warn" || check.status === "fail") && check.hint) {
4630
+ lines.push(` \u21B3 ${check.hint}`);
4631
+ }
4632
+ }
4633
+ const fails = report.checks.filter((c) => c.status === "fail");
4634
+ const warns = report.checks.filter((c) => c.status === "warn");
4635
+ lines.push("");
4636
+ if (fails.length === 0 && warns.length === 0) {
4637
+ lines.push("\u7ED3\u8BBA: \u5168\u90E8\u68C0\u67E5\u901A\u8FC7 \u2705");
4638
+ } else if (fails.length > 0) {
4639
+ lines.push(`\u7ED3\u8BBA: ${fails.length} FAIL / ${warns.length} WARN \u2014 \u4F18\u5148\u5904\u7406: ${fails[0].name}\uFF08\u89C1\u4E0A\u65B9 \u21B3 \u63D0\u793A\uFF09`);
4640
+ } else {
4641
+ lines.push(`\u7ED3\u8BBA: ${warns.length} WARN\uFF08\u65E0 FAIL\uFF09\u2014 \u591A\u6570 WARN \u662F\u5F85\u8FDE\u63A5/\u672A\u542F\u52A8\u7684\u6B63\u5E38\u4E2D\u95F4\u6001\uFF0C\u6309 \u21B3 \u63D0\u793A\u5224\u65AD\u5373\u53EF`);
1262
4642
  }
4643
+ return lines;
1263
4644
  }
1264
- var init_kill = __esm(() => {
1265
- init_state_dir();
1266
- init_daemon_lifecycle();
4645
+ function printDoctorReport(report) {
4646
+ for (const line of formatDoctorReport(report)) {
4647
+ console.log(line);
4648
+ }
4649
+ }
4650
+ var LARGE_LOG_WARN_BYTES;
4651
+ var init_doctor = __esm(() => {
4652
+ init_build_info();
4653
+ init_env_guard();
4654
+ init_pair_resolver();
4655
+ init_thread_state();
4656
+ init_resume_pollution();
4657
+ init_process_lifecycle();
4658
+ LARGE_LOG_WARN_BYTES = 100 * 1024 * 1024;
1267
4659
  });
1268
4660
 
1269
- // package.json
1270
- var require_package = __commonJS((exports, module) => {
1271
- module.exports = {
1272
- name: "@raysonmeng/agentbridge",
1273
- version: "0.1.6",
1274
- description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
1275
- type: "module",
1276
- bin: {
1277
- agentbridge: "dist/cli.js",
1278
- abg: "dist/cli.js"
1279
- },
1280
- files: [
1281
- "dist/",
1282
- "plugins/",
1283
- ".claude-plugin/",
1284
- "scripts/postinstall.cjs",
1285
- "README.md",
1286
- "LICENSE"
1287
- ],
1288
- scripts: {
1289
- start: "bun run src/bridge.ts",
1290
- "build:cli": "mkdir -p dist && bun build src/cli.ts --outfile dist/cli.js --target bun && chmod +x dist/cli.js",
1291
- "build:plugin": "mkdir -p plugins/agentbridge/server && bun build src/bridge.ts --outfile plugins/agentbridge/server/bridge-server.js --target bun && bun build src/daemon.ts --outfile plugins/agentbridge/server/daemon.js --target bun",
1292
- "verify:plugin-sync": "node scripts/verify-plugin-sync.cjs",
1293
- postinstall: "node scripts/postinstall.cjs",
1294
- prepublishOnly: "bun run build:cli && bun run build:plugin",
1295
- "validate:plugin": "claude plugin validate plugins/agentbridge && claude plugin validate .claude-plugin/marketplace.json",
1296
- test: "bun test src",
1297
- typecheck: "tsc --noEmit",
1298
- "validate:plugin-versions": "bun scripts/check-plugin-versions.js",
1299
- check: "tsc --noEmit && bun test src && bun run verify:plugin-sync && bun scripts/check-plugin-versions.js"
1300
- },
1301
- repository: {
1302
- type: "git",
1303
- url: "https://github.com/raysonmeng/agent-bridge.git"
1304
- },
1305
- homepage: "https://github.com/raysonmeng/agent-bridge#readme",
1306
- bugs: {
1307
- url: "https://github.com/raysonmeng/agent-bridge/issues"
1308
- },
1309
- keywords: [
1310
- "claude-code",
1311
- "codex",
1312
- "mcp",
1313
- "agent",
1314
- "bridge",
1315
- "multi-agent",
1316
- "channels"
1317
- ],
1318
- author: "AgentBridge Contributors",
1319
- license: "MIT",
1320
- devDependencies: {
1321
- "@modelcontextprotocol/sdk": "^1.27.1",
1322
- "@types/bun": "^1.3.11",
1323
- typescript: "^5.8.0"
4661
+ // src/budget/render.ts
4662
+ function formatEpoch(epochSeconds) {
4663
+ if (!epochSeconds || epochSeconds <= 0)
4664
+ return "\u672A\u77E5";
4665
+ return new Date(epochSeconds * 1000).toISOString().replace("T", " ").replace(/\.\d+Z$/, "Z");
4666
+ }
4667
+ function formatWindow(window, label) {
4668
+ if (!window)
4669
+ return `${label} \u672A\u77E5`;
4670
+ return `${label} ${window.util}%\uFF08\u91CD\u7F6E ${formatEpoch(window.resetEpoch)}\uFF09`;
4671
+ }
4672
+ function formatAgent(name, usage, snapshotAt) {
4673
+ if (!usage)
4674
+ return `${name}\uFF1A\u672A\u77E5\uFF08\u63A2\u6D4B\u4E0D\u53EF\u7528\uFF09`;
4675
+ const parts = [
4676
+ formatWindow(usage.fiveHour, "5h"),
4677
+ formatWindow(usage.weekly, "\u5468"),
4678
+ `\u95E8\u63A7 ${usage.gateUtil}%`,
4679
+ `\u9884\u8B66 ${usage.warnUtil}%`
4680
+ ];
4681
+ if (usage.rateLimitedUntil > 0) {
4682
+ parts.push(`\u9650\u6D41\u81F3 ${formatEpoch(usage.rateLimitedUntil)}`);
4683
+ }
4684
+ const ageSec = usage.fetchedAt > 0 ? snapshotAt - usage.fetchedAt : 0;
4685
+ if (ageSec > 300) {
4686
+ parts.push(`\u26A0\uFE0F \u6570\u636E\u91C7\u96C6\u4E8E ${Math.round(ageSec / 60)} \u5206\u949F\u524D`);
4687
+ } else if (usage.stale) {
4688
+ parts.push("\uFF08\u7F13\u5B58\u6570\u636E\uFF09");
4689
+ }
4690
+ return `${name}\uFF1A${parts.join(" \xB7 ")}`;
4691
+ }
4692
+ function renderBudgetSnapshot(snapshot) {
4693
+ const lines = [];
4694
+ lines.push(`\u3010\u9884\u7B97\u5FEB\u7167 \xB7 \u8D26\u53F7\u7EA7\u3011\u9636\u6BB5\uFF1A${PHASE_LABELS[snapshot.phase]} \xB7 \u66F4\u65B0\u4E8E ${formatEpoch(snapshot.updatedAt)}`);
4695
+ lines.push(formatAgent("Claude", snapshot.claude, snapshot.updatedAt));
4696
+ lines.push(formatAgent("Codex", snapshot.codex, snapshot.updatedAt));
4697
+ if (snapshot.claude && snapshot.codex) {
4698
+ const abs = Math.abs(snapshot.driftPct);
4699
+ if (abs > 0) {
4700
+ const heavier = snapshot.driftPct > 0 ? "Claude" : "Codex";
4701
+ const lighter = snapshot.driftPct > 0 ? "Codex" : "Claude";
4702
+ lines.push(`\u6F02\u79FB\uFF1A${heavier} \u6BD4 ${lighter} \u9AD8 ${abs} \u4E2A\u767E\u5206\u70B9`);
4703
+ } else {
4704
+ lines.push("\u6F02\u79FB\uFF1A\u53CC\u65B9\u6301\u5E73");
4705
+ }
4706
+ }
4707
+ if (snapshot.paused) {
4708
+ const resume = snapshot.resumeAfterEpoch ? `\uFF1B\u9884\u8BA1\u6062\u590D ${formatEpoch(snapshot.resumeAfterEpoch)}\uFF08\u4EE5\u5B9E\u6D4B\u4E3A\u51C6\uFF1B\u63D0\u524D\u5237\u65B0\u4F1A\u66F4\u65E9\u89E3\u9664\uFF09` : "";
4709
+ const reason = snapshot.pauseReason ?? "\u989D\u5EA6\u63A5\u8FD1\u8017\u5C3D";
4710
+ if (snapshot.pauseSide === "claude" && !snapshot.gateClosed) {
4711
+ lines.push(`\u63A5\u529B\u4E2D\uFF1AClaude \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF0C\u5DF2\u4EA4\u63A5 Codex \u7EE7\u7EED\u63A8\u8FDB\uFF08\u95F8\u95E8\u5F00\u653E\uFF09 \u2014 ${reason}${resume}`);
4712
+ } else if (snapshot.pauseSide === "codex") {
4713
+ lines.push(`\u6682\u505C\uFF1ACodex \u4FA7\u989D\u5EA6\u8017\u5C3D\uFF08\u95F8\u95E8\u5173\u95ED\uFF0CClaude \u53EF solo \u63A8\u8FDB\u72EC\u7ACB\u90E8\u5206\uFF09 \u2014 ${reason}${resume}`);
4714
+ } else {
4715
+ lines.push(`\u6682\u505C\uFF1A\u53CC\u4FA7\u8054\u5408\u6682\u505C\uFF08\u95F8\u95E8\u5173\u95ED\uFF09 \u2014 ${reason}${resume}`);
1324
4716
  }
4717
+ } else {
4718
+ lines.push("\u6682\u505C\uFF1A\u5426");
4719
+ }
4720
+ if (snapshot.parallelRecommended) {
4721
+ lines.push("\u5E76\u884C\u5EFA\u8BAE\uFF1A\u989D\u5EA6\u5BCC\u4F59\u4E14\u4E34\u8FD1\u7ED3\u7B97\uFF0C\u5EFA\u8BAE\u62C6\u5206\u66F4\u591A\u5E76\u884C\u5B50\u4EFB\u52A1");
4722
+ }
4723
+ if (snapshot.codexTier !== "full") {
4724
+ lines.push(`Codex \u6863\u4F4D\uFF1A${snapshot.codexTier}`);
4725
+ }
4726
+ if (snapshot.claudeAdvice) {
4727
+ lines.push(`Claude \u5EFA\u8BAE\uFF1A${snapshot.claudeAdvice}`);
4728
+ }
4729
+ lines.push("\u6CE8\uFF1A\u767E\u5206\u6BD4\u4E3A\u8BA2\u9605\u8D26\u53F7\u7EA7\u7528\u91CF\uFF08\u540C\u673A\u5176\u4ED6\u4F1A\u8BDD\u5171\u4EAB\u540C\u4E00\u989D\u5EA6\u6C60\uFF09\u3002");
4730
+ return lines.join(`
4731
+ `);
4732
+ }
4733
+ var PHASE_LABELS, BUDGET_UNAVAILABLE_TEXT = "\u9884\u7B97\u611F\u77E5\u4E0D\u53EF\u7528\uFF1A\u672A\u68C0\u6D4B\u5230 agent-quota-guard \u63A2\u9488\uFF08~/.budget-guard/bin/budget-probe\uFF09\u6216 budget \u529F\u80FD\u5DF2\u7981\u7528\u3002\u534F\u4F5C\u4E0D\u53D7\u5F71\u54CD\u3002";
4734
+ var init_render = __esm(() => {
4735
+ PHASE_LABELS = {
4736
+ normal: "normal\uFF08\u6B63\u5E38\uFF09",
4737
+ balance: "balance\uFF08\u9700\u5747\u8861\uFF09",
4738
+ parallel: "parallel\uFF08\u5EFA\u8BAE\u5E76\u884C\u63D0\u901F\uFF09",
4739
+ paused: "paused\uFF08\u9884\u7B97\u5E72\u9884\u4E2D\uFF09"
1325
4740
  };
1326
4741
  });
1327
4742
 
4743
+ // src/cli/budget.ts
4744
+ var exports_budget = {};
4745
+ __export(exports_budget, {
4746
+ runBudget: () => runBudget
4747
+ });
4748
+ async function runBudget(args) {
4749
+ const json = args.includes("--json");
4750
+ const { pairFlag } = parsePairFlag(args.filter((arg) => arg !== "--json"));
4751
+ let resolution;
4752
+ try {
4753
+ resolution = resolvePairReadOnly(pairFlag);
4754
+ } catch (err) {
4755
+ const message = err instanceof Error ? err.message : String(err);
4756
+ if (json) {
4757
+ console.log(JSON.stringify({ ok: false, error: message }));
4758
+ } else {
4759
+ console.error(`[agentbridge] ${message}`);
4760
+ }
4761
+ process.exit(1);
4762
+ return;
4763
+ }
4764
+ const { pair } = resolution;
4765
+ if (!resolution.registered) {
4766
+ if (json) {
4767
+ console.log(JSON.stringify({ ok: false, error: "pair_not_registered" }));
4768
+ } else {
4769
+ console.error("\u8BE5\u76EE\u5F55\u5C1A\u65E0 pair\uFF0C\u5148\u8FD0\u884C abg claude");
4770
+ }
4771
+ process.exit(1);
4772
+ return;
4773
+ }
4774
+ const status = await fetchDaemonStatus(pair.ports.controlPort);
4775
+ if (!status) {
4776
+ if (json) {
4777
+ console.log(JSON.stringify({ ok: false, pairId: pair.pairId, error: "daemon_unreachable" }));
4778
+ } else {
4779
+ console.error(`AgentBridge daemon \u672A\u8FD0\u884C\uFF08pair ${pair.pairId}\uFF0C\u63A7\u5236\u7AEF\u53E3 ${pair.ports.controlPort}\uFF09\u3002` + "\u5148\u8FD0\u884C `abg claude` \u542F\u52A8\u4F1A\u8BDD\u3002");
4780
+ }
4781
+ process.exit(1);
4782
+ }
4783
+ if (json) {
4784
+ console.log(JSON.stringify({ ok: true, pairId: status.pairId ?? pair.pairId, budget: status.budget ?? null }, null, 2));
4785
+ return;
4786
+ }
4787
+ console.log(`pair: ${status.pairId ?? pair.pairId}`);
4788
+ console.log(status.budget ? renderBudgetSnapshot(status.budget) : BUDGET_UNAVAILABLE_TEXT);
4789
+ }
4790
+ var init_budget = __esm(() => {
4791
+ init_pair_resolver();
4792
+ init_render();
4793
+ });
4794
+
1328
4795
  // src/cli.ts
1329
- async function main() {
4796
+ function parseTopLevel(args) {
4797
+ const pairTokens = [];
4798
+ let i = 0;
4799
+ for (;i < args.length; i++) {
4800
+ const a = args[i];
4801
+ if (a === "--pair") {
4802
+ pairTokens.push(a);
4803
+ const next = args[i + 1];
4804
+ if (next !== undefined && !next.startsWith("-")) {
4805
+ pairTokens.push(next);
4806
+ i++;
4807
+ }
4808
+ continue;
4809
+ }
4810
+ if (a.startsWith("--pair=")) {
4811
+ pairTokens.push(a);
4812
+ continue;
4813
+ }
4814
+ break;
4815
+ }
4816
+ const command = args[i];
4817
+ const tail = args.slice(i + 1);
4818
+ if (command !== undefined && PAIR_AWARE_COMMANDS.has(command)) {
4819
+ return { command, restArgs: [...pairTokens, ...tail] };
4820
+ }
4821
+ return { command, restArgs: tail };
4822
+ }
4823
+ async function main(command, restArgs) {
4824
+ if (command && NOTIFY_COMMANDS.has(command)) {
4825
+ try {
4826
+ const { maybeNotifyUpdate: maybeNotifyUpdate2 } = await Promise.resolve().then(() => (init_update_notifier(), exports_update_notifier));
4827
+ maybeNotifyUpdate2({ refresh: REFRESH_COMMANDS.has(command) });
4828
+ } catch {}
4829
+ }
1330
4830
  switch (command) {
1331
4831
  case "init":
1332
4832
  const { runInit: runInit2 } = await Promise.resolve().then(() => (init_init(), exports_init));
@@ -1334,7 +4834,7 @@ async function main() {
1334
4834
  break;
1335
4835
  case "dev":
1336
4836
  const { runDev: runDev2 } = await Promise.resolve().then(() => (init_dev(), exports_dev));
1337
- await runDev2();
4837
+ await runDev2(restArgs);
1338
4838
  break;
1339
4839
  case "claude":
1340
4840
  const { runClaude: runClaude2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
@@ -1346,7 +4846,19 @@ async function main() {
1346
4846
  break;
1347
4847
  case "kill":
1348
4848
  const { runKill: runKill2 } = await Promise.resolve().then(() => (init_kill(), exports_kill));
1349
- await runKill2();
4849
+ await runKill2(restArgs);
4850
+ break;
4851
+ case "pairs":
4852
+ const { runPairs: runPairs2 } = await Promise.resolve().then(() => (init_pairs(), exports_pairs));
4853
+ await runPairs2(restArgs);
4854
+ break;
4855
+ case "doctor":
4856
+ const { runDoctor: runDoctor2 } = await Promise.resolve().then(() => (init_doctor(), exports_doctor));
4857
+ await runDoctor2(restArgs);
4858
+ break;
4859
+ case "budget":
4860
+ const { runBudget: runBudget2 } = await Promise.resolve().then(() => (init_budget(), exports_budget));
4861
+ await runBudget2(restArgs);
1350
4862
  break;
1351
4863
  case "--help":
1352
4864
  case "-h":
@@ -1368,27 +4880,54 @@ function printHelp() {
1368
4880
  AgentBridge \u2014 Multi-agent collaboration bridge
1369
4881
 
1370
4882
  Usage:
1371
- agentbridge <command> [args...]
1372
- abg <command> [args...]
4883
+ agentbridge [--pair <name>] <command> [args...]
4884
+ abg [--pair <name>] <command> [args...]
1373
4885
 
1374
4886
  Commands:
1375
- init Install plugin, check dependencies, generate project config
1376
- dev Register local marketplace + install plugin (for local dev)
1377
- claude [args...] Start Claude Code with push channel enabled
1378
- codex [args...] Start Codex TUI connected to AgentBridge daemon
1379
- kill Force kill all AgentBridge processes
4887
+ init Install plugin, check dependencies, generate project config
4888
+ dev Register local marketplace + install plugin (for local dev)
4889
+ claude [args...] Start Claude Code with push channel enabled
4890
+ codex [args...] Start Codex TUI connected to AgentBridge daemon
4891
+ (bare command auto-resumes the last thread; --new starts fresh)
4892
+ pairs [rm <name|id> | prune [--dry-run]]
4893
+ List pairs; remove one (rm), or delete orphan state dirs (prune)
4894
+ doctor [--json] Diagnose env, daemon, build drift, logs, and current thread
4895
+ doctor resume-pollution [--apply] Find/fix old AgentBridge kickoff metadata
4896
+ budget [--json] Show both agents' subscription quota snapshot (5h/weekly, drift, pause state)
4897
+ kill [all | --all | --pair <name|id>]
4898
+ Stop this directory's pairs (default), every pair (all/--all), or one (--pair)
1380
4899
 
1381
4900
  Options:
1382
- --help, -h Show this help message
1383
- --version, -v Show version
4901
+ --pair <name> Run claude/codex/kill in a named pair. The name is scoped to
4902
+ the current directory, so the same name in another directory
4903
+ is a separate pair. Goes BEFORE the command. Without it, the
4904
+ pair name defaults to "main" for the current directory.
4905
+ --help, -h Show this help message
4906
+ --version, -v Show version
4907
+
4908
+ Multi-pair:
4909
+ Each pair is an isolated daemon with its own port triple. The first pair uses
4910
+ the classic ports 4500/4501/4502; each additional pair steps +10. If "main" in
4911
+ this directory already has a live Claude session, "abg claude" errors instead of
4912
+ contesting it \u2014 pick another --pair name (or kill the live one first).
1384
4913
 
1385
4914
  Examples:
1386
4915
  abg init # First-time setup
1387
- abg claude # Start Claude Code
1388
- abg claude --resume # Start Claude Code and resume session
1389
- abg codex # Start Codex TUI
1390
- abg codex --model o3 # Start Codex with specific model
1391
- abg kill # Emergency: kill all processes
4916
+ abg claude # Start the "main" pair for this directory
4917
+ abg codex # Connect Codex to this directory's "main" pair
4918
+ abg --pair work claude # Start a named pair "work" (this directory)
4919
+ abg --pair work codex # Connect Codex to the "work" pair
4920
+ abg --pair review claude # A second, parallel pair
4921
+ abg pairs # List all pairs and their ports/status
4922
+ abg pairs --threads # Include current thread mapping
4923
+ abg doctor --json # Emit a structured diagnostics report
4924
+ abg pairs rm work # Stop this directory's "work" pair and free its slot
4925
+ abg pairs rm work-1a2b3c4d # ...or by its full id (from that pair's directory)
4926
+ abg pairs prune --dry-run # Preview orphan pair dirs (no registry entry, not live)
4927
+ abg pairs prune # ...delete those orphan state directories
4928
+ abg --pair work kill # Stop only this directory's "work" pair
4929
+ abg kill # Stop this directory's pairs (+ any legacy-root daemon)
4930
+ abg kill all # Stop every pair in every directory (+ legacy-root)
1392
4931
  `.trim());
1393
4932
  }
1394
4933
  function printVersion() {
@@ -1399,19 +4938,23 @@ function printVersion() {
1399
4938
  console.log("agentbridge (version unknown)");
1400
4939
  }
1401
4940
  }
1402
- var args, command, restArgs, MARKETPLACE_NAME = "agentbridge", PLUGIN_NAME = "agentbridge";
4941
+ var MARKETPLACE_NAME = "agentbridge", PLUGIN_NAME = "agentbridge", REFRESH_COMMANDS, NOTIFY_COMMANDS, PAIR_AWARE_COMMANDS;
1403
4942
  var init_cli = __esm(() => {
1404
- args = process.argv.slice(2);
1405
- command = args[0];
1406
- restArgs = args.slice(1);
1407
- main().catch((err) => {
1408
- console.error(`Error: ${err.message}`);
1409
- process.exit(1);
1410
- });
4943
+ REFRESH_COMMANDS = new Set(["claude", "codex"]);
4944
+ NOTIFY_COMMANDS = new Set(["claude", "codex", "init", "dev"]);
4945
+ PAIR_AWARE_COMMANDS = new Set(["claude", "codex", "kill", "doctor", "budget"]);
4946
+ if (import.meta.main) {
4947
+ const { command, restArgs } = parseTopLevel(process.argv.slice(2));
4948
+ main(command, restArgs).catch((err) => {
4949
+ console.error(`Error: ${err.message}`);
4950
+ process.exit(1);
4951
+ });
4952
+ }
1411
4953
  });
1412
4954
  init_cli();
1413
4955
 
1414
4956
  export {
4957
+ parseTopLevel,
1415
4958
  PLUGIN_NAME,
1416
4959
  MARKETPLACE_NAME
1417
4960
  };