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