@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/.claude-plugin/marketplace.json +1 -1
- package/README.md +53 -6
- package/README.zh-CN.md +37 -1
- package/dist/cli.js +3983 -440
- package/dist/daemon.js +4713 -0
- package/package.json +18 -5
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/README.md +2 -2
- package/plugins/agentbridge/scripts/health-check.sh +22 -3
- package/plugins/agentbridge/scripts/plugin-update-notice.mjs +73 -0
- package/plugins/agentbridge/server/bridge-server.js +1170 -142
- package/plugins/agentbridge/server/daemon.js +2690 -358
- package/scripts/install-safety.cjs +209 -0
- package/scripts/postinstall.cjs +114 -34
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/
|
|
21
|
-
import {
|
|
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 =
|
|
62
|
-
this.configPath =
|
|
419
|
+
this.configDir = join2(root, CONFIG_DIR);
|
|
420
|
+
this.configPath = join2(this.configDir, CONFIG_FILE);
|
|
63
421
|
}
|
|
64
422
|
hasConfig() {
|
|
65
|
-
return
|
|
423
|
+
return existsSync2(this.configPath);
|
|
66
424
|
}
|
|
67
425
|
load() {
|
|
68
426
|
try {
|
|
69
|
-
const raw =
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
97
|
-
|
|
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
|
|
118
|
-
import { existsSync as
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
245
|
-
import { join as
|
|
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:
|
|
351
|
-
{ name: "AGENTS.md", path:
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
console.error("
|
|
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
|
-
|
|
815
|
+
if (skipBuild) {
|
|
816
|
+
console.log(`Skipping builds (--skip-build: caller already built CLI + plugin)
|
|
414
817
|
`);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
461
|
-
if (
|
|
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/
|
|
488
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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.
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
}
|
|
540
|
-
this.releaseLock();
|
|
541
|
-
}
|
|
1307
|
+
});
|
|
542
1308
|
}
|
|
543
1309
|
async isHealthy() {
|
|
544
1310
|
try {
|
|
545
|
-
const response = await
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
654
|
-
|
|
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(
|
|
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
|
|
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.
|
|
1484
|
+
return this.acquireLockStrict(true);
|
|
666
1485
|
}
|
|
667
1486
|
} catch {
|
|
668
|
-
|
|
669
|
-
this.releaseLock();
|
|
670
|
-
return this.acquireLock(depth + 1);
|
|
1487
|
+
return false;
|
|
671
1488
|
}
|
|
672
1489
|
return false;
|
|
673
1490
|
}
|
|
674
|
-
this.log(`
|
|
675
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
752
|
-
import {
|
|
753
|
-
import {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
770
|
-
|
|
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
|
-
|
|
775
|
-
|
|
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
|
-
|
|
778
|
-
|
|
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
|
-
|
|
781
|
-
|
|
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
|
-
|
|
784
|
-
|
|
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
|
-
|
|
787
|
-
|
|
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
|
-
|
|
790
|
-
|
|
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
|
-
|
|
793
|
-
|
|
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
|
-
|
|
796
|
-
|
|
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
|
-
|
|
799
|
-
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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/
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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 =
|
|
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
|
|
2817
|
+
var DEFAULT_MAX_BYTES2;
|
|
913
2818
|
var init_stderr_ring_buffer = __esm(() => {
|
|
914
|
-
|
|
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
|
|
3062
|
+
openSync as openSync3,
|
|
925
3063
|
writeSync,
|
|
926
|
-
closeSync as
|
|
927
|
-
writeFileSync as
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
existsSync as
|
|
931
|
-
mkdirSync as
|
|
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 {
|
|
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 =
|
|
937
|
-
if (!
|
|
938
|
-
|
|
3075
|
+
const dir = dirname4(path);
|
|
3076
|
+
if (!existsSync10(dir)) {
|
|
3077
|
+
mkdirSync6(dir, { recursive: true });
|
|
939
3078
|
}
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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 (
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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(
|
|
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
|
-
|
|
994
|
-
|
|
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 =
|
|
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
|
-
|
|
3287
|
+
closeSync3(ttyFd);
|
|
1044
3288
|
} catch {}
|
|
1045
3289
|
}
|
|
1046
3290
|
}
|
|
1047
3291
|
}
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3351
|
+
unlinkSync4(stateDir.tuiPidFile);
|
|
1084
3352
|
} catch {}
|
|
1085
3353
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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 =
|
|
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
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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/
|
|
1171
|
-
var
|
|
1172
|
-
__export(
|
|
1173
|
-
|
|
4363
|
+
// src/cli/doctor.ts
|
|
4364
|
+
var exports_doctor = {};
|
|
4365
|
+
__export(exports_doctor, {
|
|
4366
|
+
runDoctor: () => runDoctor,
|
|
4367
|
+
formatDoctorReport: () => formatDoctorReport
|
|
1174
4368
|
});
|
|
1175
|
-
import {
|
|
1176
|
-
import {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
-
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
return
|
|
4419
|
+
const report = scanResumePollution({ codexHome: codexHome2, apply });
|
|
4420
|
+
if (json) {
|
|
4421
|
+
console.log(JSON.stringify(report, null, 2));
|
|
4422
|
+
return;
|
|
1211
4423
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1238
|
-
|
|
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
|
|
4593
|
+
function extractBundleCommit(path) {
|
|
1241
4594
|
try {
|
|
1242
|
-
const
|
|
1243
|
-
|
|
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
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
//
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
"
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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
|
-
|
|
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
|
|
1376
|
-
dev
|
|
1377
|
-
claude [args...]
|
|
1378
|
-
codex [args...]
|
|
1379
|
-
|
|
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
|
-
--
|
|
1383
|
-
|
|
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
|
|
1388
|
-
abg
|
|
1389
|
-
abg
|
|
1390
|
-
abg
|
|
1391
|
-
abg
|
|
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
|
|
4941
|
+
var MARKETPLACE_NAME = "agentbridge", PLUGIN_NAME = "agentbridge", REFRESH_COMMANDS, NOTIFY_COMMANDS, PAIR_AWARE_COMMANDS;
|
|
1403
4942
|
var init_cli = __esm(() => {
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
};
|