@raysonmeng/agentbridge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +24 -0
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/README.zh-CN.md +291 -0
- package/dist/cli.js +1158 -0
- package/package.json +54 -0
- package/plugins/agentbridge/.claude-plugin/plugin.json +12 -0
- package/plugins/agentbridge/.mcp.json +11 -0
- package/plugins/agentbridge/README.md +43 -0
- package/plugins/agentbridge/commands/init.md +70 -0
- package/plugins/agentbridge/hooks/hooks.json +16 -0
- package/plugins/agentbridge/scripts/health-check.sh +51 -0
- package/plugins/agentbridge/server/bridge-server.js +14734 -0
- package/plugins/agentbridge/server/daemon.js +1762 -0
- package/scripts/postinstall.cjs +27 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
5
|
+
var __returnValue = (v) => v;
|
|
6
|
+
function __exportSetter(name, newValue) {
|
|
7
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
8
|
+
}
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, {
|
|
12
|
+
get: all[name],
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
set: __exportSetter.bind(all, name)
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
19
|
+
|
|
20
|
+
// src/config-service.ts
|
|
21
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
|
|
24
|
+
class ConfigService {
|
|
25
|
+
configDir;
|
|
26
|
+
configPath;
|
|
27
|
+
collaborationPath;
|
|
28
|
+
constructor(projectRoot) {
|
|
29
|
+
const root = projectRoot ?? process.cwd();
|
|
30
|
+
this.configDir = join(root, CONFIG_DIR);
|
|
31
|
+
this.configPath = join(this.configDir, CONFIG_FILE);
|
|
32
|
+
this.collaborationPath = join(this.configDir, COLLABORATION_FILE);
|
|
33
|
+
}
|
|
34
|
+
hasConfig() {
|
|
35
|
+
return existsSync(this.configPath);
|
|
36
|
+
}
|
|
37
|
+
load() {
|
|
38
|
+
try {
|
|
39
|
+
const raw = readFileSync(this.configPath, "utf-8");
|
|
40
|
+
return JSON.parse(raw);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
loadOrDefault() {
|
|
46
|
+
return this.load() ?? structuredClone(DEFAULT_CONFIG);
|
|
47
|
+
}
|
|
48
|
+
save(config) {
|
|
49
|
+
this.ensureConfigDir();
|
|
50
|
+
writeFileSync(this.configPath, JSON.stringify(config, null, 2) + `
|
|
51
|
+
`, "utf-8");
|
|
52
|
+
}
|
|
53
|
+
loadCollaboration() {
|
|
54
|
+
try {
|
|
55
|
+
return readFileSync(this.collaborationPath, "utf-8");
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
saveCollaboration(content) {
|
|
61
|
+
this.ensureConfigDir();
|
|
62
|
+
writeFileSync(this.collaborationPath, content, "utf-8");
|
|
63
|
+
}
|
|
64
|
+
initDefaults() {
|
|
65
|
+
this.ensureConfigDir();
|
|
66
|
+
const created = [];
|
|
67
|
+
if (!existsSync(this.configPath)) {
|
|
68
|
+
this.save(DEFAULT_CONFIG);
|
|
69
|
+
created.push(this.configPath);
|
|
70
|
+
}
|
|
71
|
+
if (!existsSync(this.collaborationPath)) {
|
|
72
|
+
this.saveCollaboration(DEFAULT_COLLABORATION_MD);
|
|
73
|
+
created.push(this.collaborationPath);
|
|
74
|
+
}
|
|
75
|
+
return created;
|
|
76
|
+
}
|
|
77
|
+
get configFilePath() {
|
|
78
|
+
return this.configPath;
|
|
79
|
+
}
|
|
80
|
+
get collaborationFilePath() {
|
|
81
|
+
return this.collaborationPath;
|
|
82
|
+
}
|
|
83
|
+
ensureConfigDir() {
|
|
84
|
+
if (!existsSync(this.configDir)) {
|
|
85
|
+
mkdirSync(this.configDir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
var DEFAULT_CONFIG, DEFAULT_COLLABORATION_MD = `# Collaboration Rules
|
|
90
|
+
|
|
91
|
+
## Roles
|
|
92
|
+
- Claude: Reviewer, Planner, Hypothesis Challenger
|
|
93
|
+
- Codex: Implementer, Executor, Reproducer/Verifier
|
|
94
|
+
|
|
95
|
+
## Thinking Patterns
|
|
96
|
+
- Analytical/review tasks: Independent Analysis & Convergence
|
|
97
|
+
- Implementation tasks: Architect -> Builder -> Critic
|
|
98
|
+
- Debugging tasks: Hypothesis -> Experiment -> Interpretation
|
|
99
|
+
|
|
100
|
+
## Communication
|
|
101
|
+
- Use explicit phrases: "My independent view is:", "I agree on:", "I disagree on:", "Current consensus:"
|
|
102
|
+
- Tag messages with [IMPORTANT], [STATUS], or [FYI]
|
|
103
|
+
|
|
104
|
+
## Review Process
|
|
105
|
+
- Cross-review: author never reviews their own code
|
|
106
|
+
- All changes go through feature/fix branches + PR
|
|
107
|
+
- Merge via squash merge
|
|
108
|
+
|
|
109
|
+
## Custom Rules
|
|
110
|
+
<!-- Add your project-specific collaboration rules here -->
|
|
111
|
+
`, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json", COLLABORATION_FILE = "collaboration.md";
|
|
112
|
+
var init_config_service = __esm(() => {
|
|
113
|
+
DEFAULT_CONFIG = {
|
|
114
|
+
version: "1.0",
|
|
115
|
+
daemon: {
|
|
116
|
+
port: 4500,
|
|
117
|
+
proxyPort: 4501
|
|
118
|
+
},
|
|
119
|
+
agents: {
|
|
120
|
+
claude: {
|
|
121
|
+
role: "Reviewer, Planner",
|
|
122
|
+
mode: "push"
|
|
123
|
+
},
|
|
124
|
+
codex: {
|
|
125
|
+
role: "Implementer, Executor"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
markers: ["IMPORTANT", "STATUS", "FYI"],
|
|
129
|
+
turnCoordination: {
|
|
130
|
+
attentionWindowSeconds: 15,
|
|
131
|
+
busyGuard: true
|
|
132
|
+
},
|
|
133
|
+
idleShutdownSeconds: 30
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// src/cli/pkg-root.ts
|
|
138
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
139
|
+
import { existsSync as existsSync2 } from "fs";
|
|
140
|
+
import { execFileSync } from "child_process";
|
|
141
|
+
function findPackageRoot() {
|
|
142
|
+
let dir = import.meta.dir;
|
|
143
|
+
while (true) {
|
|
144
|
+
if (existsSync2(join2(dir, "package.json"))) {
|
|
145
|
+
return dir;
|
|
146
|
+
}
|
|
147
|
+
const parent = dirname2(dir);
|
|
148
|
+
if (parent === dir) {
|
|
149
|
+
throw new Error("Could not find package.json in any parent directory");
|
|
150
|
+
}
|
|
151
|
+
dir = parent;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function registerMarketplace(marketplaceRoot) {
|
|
155
|
+
execFileSync("claude", ["plugin", "marketplace", "add", marketplaceRoot], {
|
|
156
|
+
stdio: "inherit"
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
var init_pkg_root = () => {};
|
|
160
|
+
|
|
161
|
+
// src/cli/init.ts
|
|
162
|
+
var exports_init = {};
|
|
163
|
+
__export(exports_init, {
|
|
164
|
+
runInit: () => runInit,
|
|
165
|
+
compareVersions: () => compareVersions
|
|
166
|
+
});
|
|
167
|
+
import { execSync, execFileSync as execFileSync2 } from "child_process";
|
|
168
|
+
async function runInit() {
|
|
169
|
+
console.log(`AgentBridge Init
|
|
170
|
+
`);
|
|
171
|
+
console.log("Checking dependencies...");
|
|
172
|
+
checkBun();
|
|
173
|
+
checkClaude();
|
|
174
|
+
checkCodex();
|
|
175
|
+
console.log("");
|
|
176
|
+
console.log("Generating project config...");
|
|
177
|
+
const configService = new ConfigService;
|
|
178
|
+
const created = configService.initDefaults();
|
|
179
|
+
if (created.length > 0) {
|
|
180
|
+
for (const file of created) {
|
|
181
|
+
console.log(` Created: ${file}`);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
console.log(" Project config already exists, skipping.");
|
|
185
|
+
}
|
|
186
|
+
console.log("");
|
|
187
|
+
console.log("Installing AgentBridge plugin...");
|
|
188
|
+
try {
|
|
189
|
+
registerMarketplace(findPackageRoot());
|
|
190
|
+
execFileSync2("claude", ["plugin", "install", `${PLUGIN_NAME}@${MARKETPLACE_NAME}`], {
|
|
191
|
+
stdio: "inherit"
|
|
192
|
+
});
|
|
193
|
+
console.log(" Plugin installed successfully.");
|
|
194
|
+
} catch {
|
|
195
|
+
console.log(" Plugin install skipped (marketplace registration or install failed).");
|
|
196
|
+
console.log(" You can install it later with:");
|
|
197
|
+
console.log(` abg dev # registers marketplace and installs plugin`);
|
|
198
|
+
}
|
|
199
|
+
console.log("");
|
|
200
|
+
console.log(`Setup complete!
|
|
201
|
+
`);
|
|
202
|
+
console.log("Next steps:");
|
|
203
|
+
console.log(" 1. If Claude Code is already running, execute /reload-plugins in your session");
|
|
204
|
+
console.log(" 2. Start Claude Code: agentbridge claude");
|
|
205
|
+
console.log(" 3. Start Codex TUI: agentbridge codex");
|
|
206
|
+
}
|
|
207
|
+
function checkBun() {
|
|
208
|
+
try {
|
|
209
|
+
const version = execSync("bun --version", { encoding: "utf-8" }).trim();
|
|
210
|
+
console.log(` bun: ${version}`);
|
|
211
|
+
} catch {
|
|
212
|
+
console.error(" ERROR: bun not found in PATH.");
|
|
213
|
+
console.error(" Install Bun: https://bun.sh");
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function checkClaude() {
|
|
218
|
+
try {
|
|
219
|
+
const versionOutput = execSync("claude --version", { encoding: "utf-8" }).trim();
|
|
220
|
+
const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
|
|
221
|
+
if (match) {
|
|
222
|
+
const version = match[1];
|
|
223
|
+
console.log(` claude: ${version}`);
|
|
224
|
+
if (compareVersions(version, MIN_CLAUDE_VERSION) < 0) {
|
|
225
|
+
console.error(` ERROR: Claude Code version ${version} is too old.`);
|
|
226
|
+
console.error(` Channels require >= ${MIN_CLAUDE_VERSION}.`);
|
|
227
|
+
console.error(" Update: npm update -g @anthropic-ai/claude-code");
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
console.log(` claude: ${versionOutput} (version check skipped)`);
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
console.error(" ERROR: claude not found in PATH.");
|
|
235
|
+
console.error(" Install Claude Code: npm install -g @anthropic-ai/claude-code");
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function checkCodex() {
|
|
240
|
+
try {
|
|
241
|
+
const version = execSync("codex --version", { encoding: "utf-8" }).trim();
|
|
242
|
+
console.log(` codex: ${version}`);
|
|
243
|
+
} catch {
|
|
244
|
+
console.error(" ERROR: codex not found in PATH.");
|
|
245
|
+
console.error(" Install Codex: https://github.com/openai/codex");
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function compareVersions(a, b) {
|
|
250
|
+
const pa = a.split(".").map(Number);
|
|
251
|
+
const pb = b.split(".").map(Number);
|
|
252
|
+
for (let i = 0;i < 3; i++) {
|
|
253
|
+
const va = pa[i] ?? 0;
|
|
254
|
+
const vb = pb[i] ?? 0;
|
|
255
|
+
if (va < vb)
|
|
256
|
+
return -1;
|
|
257
|
+
if (va > vb)
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
var MIN_CLAUDE_VERSION = "2.1.80";
|
|
263
|
+
var init_init = __esm(() => {
|
|
264
|
+
init_config_service();
|
|
265
|
+
init_cli();
|
|
266
|
+
init_pkg_root();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// src/cli/dev.ts
|
|
270
|
+
var exports_dev = {};
|
|
271
|
+
__export(exports_dev, {
|
|
272
|
+
runDev: () => runDev
|
|
273
|
+
});
|
|
274
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
275
|
+
import { resolve } from "path";
|
|
276
|
+
import { existsSync as existsSync3, cpSync, rmSync } from "fs";
|
|
277
|
+
import { homedir } from "os";
|
|
278
|
+
async function runDev() {
|
|
279
|
+
console.log(`AgentBridge Dev Setup
|
|
280
|
+
`);
|
|
281
|
+
const projectRoot = findPackageRoot();
|
|
282
|
+
const marketplacePath = resolve(projectRoot, ".claude-plugin", "marketplace.json");
|
|
283
|
+
const pluginDir = resolve(projectRoot, "plugins", "agentbridge");
|
|
284
|
+
const pluginManifest = resolve(pluginDir, ".claude-plugin", "plugin.json");
|
|
285
|
+
if (!existsSync3(pluginManifest)) {
|
|
286
|
+
console.error(` ERROR: Plugin manifest not found at ${pluginManifest}`);
|
|
287
|
+
console.error(" Run 'bun run build:plugin' first, or check your working tree.");
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
if (!existsSync3(marketplacePath)) {
|
|
291
|
+
console.error(` ERROR: Marketplace manifest not found at ${marketplacePath}`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
console.log(` Plugin source: ${pluginDir}`);
|
|
295
|
+
console.log(`
|
|
296
|
+
Registering local marketplace...`);
|
|
297
|
+
try {
|
|
298
|
+
registerMarketplace(projectRoot);
|
|
299
|
+
} catch (e) {
|
|
300
|
+
console.error(` ERROR: Failed to register marketplace: ${e.message}`);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
const pluginRef = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
304
|
+
console.log(`
|
|
305
|
+
Installing plugin...`);
|
|
306
|
+
try {
|
|
307
|
+
const listOutput = execFileSync3("claude", ["plugin", "list"], { encoding: "utf-8" });
|
|
308
|
+
if (!listOutput.includes(pluginRef)) {
|
|
309
|
+
execFileSync3("claude", ["plugin", "install", pluginRef], { stdio: "inherit" });
|
|
310
|
+
} else {
|
|
311
|
+
console.log(` Plugin '${pluginRef}' already installed.`);
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error(` ERROR: Failed to install plugin: ${e.message}`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
console.log(`
|
|
318
|
+
Syncing local plugin to cache...`);
|
|
319
|
+
const cacheDir = resolve(homedir(), ".claude", "plugins", "cache", MARKETPLACE_NAME, PLUGIN_NAME);
|
|
320
|
+
if (existsSync3(cacheDir)) {
|
|
321
|
+
const versionDirs = Bun.spawnSync(["ls", cacheDir]).stdout.toString().trim().split(`
|
|
322
|
+
`).filter(Boolean);
|
|
323
|
+
for (const ver of versionDirs) {
|
|
324
|
+
const targetDir = resolve(cacheDir, ver);
|
|
325
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
326
|
+
cpSync(pluginDir, targetDir, { recursive: true });
|
|
327
|
+
console.log(` Synced to ${targetDir}`);
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
console.log(" Cache directory not found, plugin install should have created it.");
|
|
331
|
+
}
|
|
332
|
+
console.log(`
|
|
333
|
+
\u2705 Dev setup complete!
|
|
334
|
+
`);
|
|
335
|
+
console.log("Next steps:");
|
|
336
|
+
console.log(" agentbridge claude # Start Claude Code (plugin auto-loaded)");
|
|
337
|
+
console.log(" agentbridge codex # Start Codex TUI");
|
|
338
|
+
console.log("");
|
|
339
|
+
console.log("Code changed? Run 'agentbridge dev' again, then restart Claude Code or /reload-plugins.");
|
|
340
|
+
}
|
|
341
|
+
var init_dev = __esm(() => {
|
|
342
|
+
init_cli();
|
|
343
|
+
init_pkg_root();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// src/daemon-lifecycle.ts
|
|
347
|
+
import { spawn, execFileSync as execFileSync4 } from "child_process";
|
|
348
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2, openSync, closeSync, constants } from "fs";
|
|
349
|
+
import { fileURLToPath } from "url";
|
|
350
|
+
|
|
351
|
+
class DaemonLifecycle {
|
|
352
|
+
stateDir;
|
|
353
|
+
controlPort;
|
|
354
|
+
log;
|
|
355
|
+
constructor(opts) {
|
|
356
|
+
this.stateDir = opts.stateDir;
|
|
357
|
+
this.controlPort = opts.controlPort;
|
|
358
|
+
this.log = opts.log;
|
|
359
|
+
}
|
|
360
|
+
get healthUrl() {
|
|
361
|
+
return `http://127.0.0.1:${this.controlPort}/healthz`;
|
|
362
|
+
}
|
|
363
|
+
get readyUrl() {
|
|
364
|
+
return `http://127.0.0.1:${this.controlPort}/readyz`;
|
|
365
|
+
}
|
|
366
|
+
get controlWsUrl() {
|
|
367
|
+
return `ws://127.0.0.1:${this.controlPort}/ws`;
|
|
368
|
+
}
|
|
369
|
+
async ensureRunning() {
|
|
370
|
+
if (await this.isHealthy()) {
|
|
371
|
+
await this.waitForReady();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const existingPid = this.readPid();
|
|
375
|
+
if (existingPid) {
|
|
376
|
+
if (isProcessAlive(existingPid)) {
|
|
377
|
+
if (this.isDaemonProcess(existingPid)) {
|
|
378
|
+
try {
|
|
379
|
+
await this.waitForReady(12, 250);
|
|
380
|
+
return;
|
|
381
|
+
} catch {
|
|
382
|
+
throw new Error(`Found existing daemon process ${existingPid}, but control port ${this.controlPort} never became ready.`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
this.log(`Pid ${existingPid} is alive but not an AgentBridge daemon, removing stale pid file`);
|
|
386
|
+
}
|
|
387
|
+
this.removeStalePidFile();
|
|
388
|
+
}
|
|
389
|
+
const lockAcquired = this.acquireLock();
|
|
390
|
+
if (!lockAcquired) {
|
|
391
|
+
this.log("Another process is starting the daemon, waiting for readiness...");
|
|
392
|
+
await this.waitForReady();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
this.launch();
|
|
397
|
+
await this.waitForReady();
|
|
398
|
+
} finally {
|
|
399
|
+
this.releaseLock();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async isHealthy() {
|
|
403
|
+
try {
|
|
404
|
+
const response = await fetch(this.healthUrl);
|
|
405
|
+
return response.ok;
|
|
406
|
+
} catch {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async waitForHealthy(maxRetries = 40, delayMs = 250) {
|
|
411
|
+
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
412
|
+
if (await this.isHealthy())
|
|
413
|
+
return;
|
|
414
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
415
|
+
}
|
|
416
|
+
throw new Error(`Timed out waiting for AgentBridge daemon health on ${this.healthUrl}`);
|
|
417
|
+
}
|
|
418
|
+
async isReady() {
|
|
419
|
+
try {
|
|
420
|
+
const response = await fetch(this.readyUrl);
|
|
421
|
+
return response.ok;
|
|
422
|
+
} catch {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async waitForReady(maxRetries = 40, delayMs = 250) {
|
|
427
|
+
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
428
|
+
if (await this.isReady())
|
|
429
|
+
return;
|
|
430
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
431
|
+
}
|
|
432
|
+
throw new Error(`Timed out waiting for AgentBridge daemon readiness on ${this.readyUrl}`);
|
|
433
|
+
}
|
|
434
|
+
readStatus() {
|
|
435
|
+
try {
|
|
436
|
+
const raw = readFileSync2(this.stateDir.statusFile, "utf-8");
|
|
437
|
+
return JSON.parse(raw);
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
writeStatus(status) {
|
|
443
|
+
this.stateDir.ensure();
|
|
444
|
+
writeFileSync2(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
|
|
445
|
+
`, "utf-8");
|
|
446
|
+
}
|
|
447
|
+
readPid() {
|
|
448
|
+
try {
|
|
449
|
+
const raw = readFileSync2(this.stateDir.pidFile, "utf-8").trim();
|
|
450
|
+
if (!raw)
|
|
451
|
+
return null;
|
|
452
|
+
const pid = Number.parseInt(raw, 10);
|
|
453
|
+
return Number.isFinite(pid) ? pid : null;
|
|
454
|
+
} catch {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
writePid(pid) {
|
|
459
|
+
this.stateDir.ensure();
|
|
460
|
+
writeFileSync2(this.stateDir.pidFile, `${pid ?? process.pid}
|
|
461
|
+
`, "utf-8");
|
|
462
|
+
}
|
|
463
|
+
removePidFile() {
|
|
464
|
+
try {
|
|
465
|
+
unlinkSync(this.stateDir.pidFile);
|
|
466
|
+
} catch {}
|
|
467
|
+
}
|
|
468
|
+
removeStatusFile() {
|
|
469
|
+
try {
|
|
470
|
+
unlinkSync(this.stateDir.statusFile);
|
|
471
|
+
} catch {}
|
|
472
|
+
}
|
|
473
|
+
markKilled() {
|
|
474
|
+
this.stateDir.ensure();
|
|
475
|
+
writeFileSync2(this.stateDir.killedFile, `${Date.now()}
|
|
476
|
+
`, "utf-8");
|
|
477
|
+
}
|
|
478
|
+
clearKilled() {
|
|
479
|
+
try {
|
|
480
|
+
unlinkSync(this.stateDir.killedFile);
|
|
481
|
+
} catch {}
|
|
482
|
+
}
|
|
483
|
+
wasKilled() {
|
|
484
|
+
return existsSync4(this.stateDir.killedFile);
|
|
485
|
+
}
|
|
486
|
+
launch() {
|
|
487
|
+
this.stateDir.ensure();
|
|
488
|
+
this.log(`Launching detached daemon on control port ${this.controlPort}`);
|
|
489
|
+
const daemonProc = spawn(process.execPath, ["run", DAEMON_PATH], {
|
|
490
|
+
cwd: process.cwd(),
|
|
491
|
+
env: {
|
|
492
|
+
...process.env,
|
|
493
|
+
AGENTBRIDGE_CONTROL_PORT: String(this.controlPort),
|
|
494
|
+
AGENTBRIDGE_STATE_DIR: this.stateDir.dir
|
|
495
|
+
},
|
|
496
|
+
detached: true,
|
|
497
|
+
stdio: "ignore"
|
|
498
|
+
});
|
|
499
|
+
daemonProc.unref();
|
|
500
|
+
}
|
|
501
|
+
removeStalePidFile() {
|
|
502
|
+
this.log("Removing stale pid file");
|
|
503
|
+
this.removePidFile();
|
|
504
|
+
}
|
|
505
|
+
acquireLock(depth = 0) {
|
|
506
|
+
if (depth > 1) {
|
|
507
|
+
this.log("Lock acquisition failed after retry, proceeding without lock");
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
this.stateDir.ensure();
|
|
511
|
+
try {
|
|
512
|
+
const fd = openSync(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
513
|
+
writeFileSync2(fd, `${process.pid}
|
|
514
|
+
`);
|
|
515
|
+
closeSync(fd);
|
|
516
|
+
return true;
|
|
517
|
+
} catch (err) {
|
|
518
|
+
if (err.code === "EEXIST") {
|
|
519
|
+
try {
|
|
520
|
+
const holderPid = Number.parseInt(readFileSync2(this.stateDir.lockFile, "utf-8").trim(), 10);
|
|
521
|
+
if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
|
|
522
|
+
this.log(`Stale lock file from dead process ${holderPid}, removing`);
|
|
523
|
+
this.releaseLock();
|
|
524
|
+
return this.acquireLock(depth + 1);
|
|
525
|
+
}
|
|
526
|
+
} catch {
|
|
527
|
+
this.log("Cannot read lock file, removing stale lock");
|
|
528
|
+
this.releaseLock();
|
|
529
|
+
return this.acquireLock(depth + 1);
|
|
530
|
+
}
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
this.log(`Warning: could not acquire startup lock: ${err.message}`);
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
releaseLock() {
|
|
538
|
+
try {
|
|
539
|
+
unlinkSync(this.stateDir.lockFile);
|
|
540
|
+
} catch {}
|
|
541
|
+
}
|
|
542
|
+
async kill(gracefulTimeoutMs = 3000) {
|
|
543
|
+
const pid = this.readPid();
|
|
544
|
+
if (!pid) {
|
|
545
|
+
this.log("No daemon pid file found");
|
|
546
|
+
this.cleanup();
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
if (!isProcessAlive(pid)) {
|
|
550
|
+
this.log(`Daemon pid ${pid} is not alive, cleaning up stale files`);
|
|
551
|
+
this.cleanup();
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
if (!this.isDaemonProcess(pid)) {
|
|
555
|
+
this.log(`Pid ${pid} is alive but is NOT an AgentBridge daemon \u2014 refusing to kill. Cleaning up stale pid file.`);
|
|
556
|
+
this.cleanup();
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
this.log(`Sending SIGTERM to daemon pid ${pid}`);
|
|
560
|
+
try {
|
|
561
|
+
process.kill(pid, "SIGTERM");
|
|
562
|
+
} catch {
|
|
563
|
+
this.cleanup();
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
const deadline = Date.now() + gracefulTimeoutMs;
|
|
567
|
+
while (Date.now() < deadline) {
|
|
568
|
+
if (!isProcessAlive(pid)) {
|
|
569
|
+
this.log(`Daemon pid ${pid} stopped gracefully`);
|
|
570
|
+
this.cleanup();
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
await new Promise((resolve2) => setTimeout(resolve2, 200));
|
|
574
|
+
}
|
|
575
|
+
this.log(`Daemon pid ${pid} did not stop gracefully, sending SIGKILL`);
|
|
576
|
+
try {
|
|
577
|
+
process.kill(pid, "SIGKILL");
|
|
578
|
+
} catch {}
|
|
579
|
+
this.cleanup();
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
isDaemonProcess(pid) {
|
|
583
|
+
try {
|
|
584
|
+
const cmd = execFileSync4("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
585
|
+
return cmd.includes("daemon") && (cmd.includes("agentbridge") || cmd.includes("agent_bridge"));
|
|
586
|
+
} catch {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
cleanup() {
|
|
591
|
+
this.removePidFile();
|
|
592
|
+
this.removeStatusFile();
|
|
593
|
+
this.releaseLock();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function isProcessAlive(pid) {
|
|
597
|
+
try {
|
|
598
|
+
process.kill(pid, 0);
|
|
599
|
+
return true;
|
|
600
|
+
} catch {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
var DAEMON_ENTRY, DAEMON_PATH;
|
|
605
|
+
var init_daemon_lifecycle = __esm(() => {
|
|
606
|
+
DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY ?? "./daemon.ts";
|
|
607
|
+
DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// src/state-dir.ts
|
|
611
|
+
import { mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
|
|
612
|
+
import { join as join3 } from "path";
|
|
613
|
+
import { homedir as homedir2, platform } from "os";
|
|
614
|
+
|
|
615
|
+
class StateDirResolver {
|
|
616
|
+
stateDir;
|
|
617
|
+
constructor(envOverride) {
|
|
618
|
+
const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
|
|
619
|
+
if (override) {
|
|
620
|
+
this.stateDir = override;
|
|
621
|
+
} else if (platform() === "darwin") {
|
|
622
|
+
this.stateDir = join3(homedir2(), "Library", "Application Support", "AgentBridge");
|
|
623
|
+
} else {
|
|
624
|
+
const xdgState = process.env.XDG_STATE_HOME ?? join3(homedir2(), ".local", "state");
|
|
625
|
+
this.stateDir = join3(xdgState, "agentbridge");
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
ensure() {
|
|
629
|
+
if (!existsSync5(this.stateDir)) {
|
|
630
|
+
mkdirSync2(this.stateDir, { recursive: true });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
get dir() {
|
|
634
|
+
return this.stateDir;
|
|
635
|
+
}
|
|
636
|
+
get pidFile() {
|
|
637
|
+
return join3(this.stateDir, "daemon.pid");
|
|
638
|
+
}
|
|
639
|
+
get tuiPidFile() {
|
|
640
|
+
return join3(this.stateDir, "codex-tui.pid");
|
|
641
|
+
}
|
|
642
|
+
get lockFile() {
|
|
643
|
+
return join3(this.stateDir, "daemon.lock");
|
|
644
|
+
}
|
|
645
|
+
get statusFile() {
|
|
646
|
+
return join3(this.stateDir, "status.json");
|
|
647
|
+
}
|
|
648
|
+
get portsFile() {
|
|
649
|
+
return join3(this.stateDir, "ports.json");
|
|
650
|
+
}
|
|
651
|
+
get logFile() {
|
|
652
|
+
return join3(this.stateDir, "agentbridge.log");
|
|
653
|
+
}
|
|
654
|
+
get killedFile() {
|
|
655
|
+
return join3(this.stateDir, "killed");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
var init_state_dir = () => {};
|
|
659
|
+
|
|
660
|
+
// src/cli/claude.ts
|
|
661
|
+
var exports_claude = {};
|
|
662
|
+
__export(exports_claude, {
|
|
663
|
+
runClaude: () => runClaude,
|
|
664
|
+
checkOwnedFlagConflicts: () => checkOwnedFlagConflicts
|
|
665
|
+
});
|
|
666
|
+
import { spawn as spawn2 } from "child_process";
|
|
667
|
+
async function runClaude(args) {
|
|
668
|
+
checkOwnedFlagConflicts(args, "agentbridge claude", OWNED_FLAGS);
|
|
669
|
+
const stateDir = new StateDirResolver;
|
|
670
|
+
const controlPort = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
|
|
671
|
+
const lifecycle = new DaemonLifecycle({
|
|
672
|
+
stateDir,
|
|
673
|
+
controlPort,
|
|
674
|
+
log: (msg) => console.error(`[agentbridge] ${msg}`)
|
|
675
|
+
});
|
|
676
|
+
lifecycle.clearKilled();
|
|
677
|
+
const channelEntry = `plugin:${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
|
|
678
|
+
const fullArgs = [
|
|
679
|
+
"--dangerously-load-development-channels",
|
|
680
|
+
channelEntry,
|
|
681
|
+
...args
|
|
682
|
+
];
|
|
683
|
+
const child = spawn2("claude", fullArgs, {
|
|
684
|
+
stdio: "inherit",
|
|
685
|
+
env: process.env
|
|
686
|
+
});
|
|
687
|
+
child.on("exit", (code) => {
|
|
688
|
+
process.exit(code ?? 0);
|
|
689
|
+
});
|
|
690
|
+
child.on("error", (err) => {
|
|
691
|
+
if (err.code === "ENOENT") {
|
|
692
|
+
console.error("Error: claude not found in PATH.");
|
|
693
|
+
console.error("Install Claude Code: npm install -g @anthropic-ai/claude-code");
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
console.error(`Error starting Claude Code: ${err.message}`);
|
|
697
|
+
process.exit(1);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
function checkOwnedFlagConflicts(args, commandName, ownedFlags) {
|
|
701
|
+
for (const flag of ownedFlags) {
|
|
702
|
+
if (args.some((a) => a === flag || a.startsWith(`${flag}=`))) {
|
|
703
|
+
console.error(`Error: "${flag}" is automatically set by ${commandName}.`);
|
|
704
|
+
console.error("");
|
|
705
|
+
console.error("AgentBridge automatically injects these flags:");
|
|
706
|
+
for (const f of ownedFlags) {
|
|
707
|
+
console.error(` ${f}`);
|
|
708
|
+
}
|
|
709
|
+
console.error("");
|
|
710
|
+
const nativeCmd = commandName.includes("codex") ? "codex" : "claude";
|
|
711
|
+
console.error("If you need full control over these flags, use the native command directly:");
|
|
712
|
+
console.error(` ${nativeCmd} [your flags here]`);
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
var OWNED_FLAGS;
|
|
718
|
+
var init_claude = __esm(() => {
|
|
719
|
+
init_cli();
|
|
720
|
+
init_daemon_lifecycle();
|
|
721
|
+
init_state_dir();
|
|
722
|
+
OWNED_FLAGS = ["--channels", "--dangerously-load-development-channels"];
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
// src/cli/codex.ts
|
|
726
|
+
var exports_codex = {};
|
|
727
|
+
__export(exports_codex, {
|
|
728
|
+
runCodex: () => runCodex
|
|
729
|
+
});
|
|
730
|
+
import { spawn as spawn3, execSync as execSync2 } from "child_process";
|
|
731
|
+
import { openSync as openSync2, writeSync, closeSync as closeSync2, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
732
|
+
async function runCodex(args) {
|
|
733
|
+
checkOwnedFlagConflicts(args, "agentbridge codex", OWNED_FLAGS2);
|
|
734
|
+
for (let i = 0;i < args.length; i++) {
|
|
735
|
+
if (args[i] === "--enable" && args[i + 1] === "tui_app_server") {
|
|
736
|
+
console.error(`Error: "--enable tui_app_server" is automatically set by agentbridge codex.`);
|
|
737
|
+
console.error("");
|
|
738
|
+
console.error("If you need full control over these flags, use the native command directly:");
|
|
739
|
+
console.error(" codex [your flags here]");
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
if (args[i] === "--enable=tui_app_server") {
|
|
743
|
+
console.error(`Error: "--enable=tui_app_server" is automatically set by agentbridge codex.`);
|
|
744
|
+
console.error("");
|
|
745
|
+
console.error("If you need full control over these flags, use the native command directly:");
|
|
746
|
+
console.error(" codex [your flags here]");
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const stateDir = new StateDirResolver;
|
|
751
|
+
const configService = new ConfigService;
|
|
752
|
+
const config = configService.loadOrDefault();
|
|
753
|
+
const controlPort = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
|
|
754
|
+
const lifecycle = new DaemonLifecycle({
|
|
755
|
+
stateDir,
|
|
756
|
+
controlPort,
|
|
757
|
+
log: (msg) => console.error(`[agentbridge] ${msg}`)
|
|
758
|
+
});
|
|
759
|
+
console.error("[agentbridge] Ensuring daemon is running...");
|
|
760
|
+
try {
|
|
761
|
+
lifecycle.clearKilled();
|
|
762
|
+
await lifecycle.ensureRunning();
|
|
763
|
+
console.error("[agentbridge] Daemon is ready.");
|
|
764
|
+
} catch (err) {
|
|
765
|
+
console.error(`[agentbridge] Failed to start daemon: ${err.message}`);
|
|
766
|
+
console.error("[agentbridge] Try: agentbridge kill && agentbridge claude");
|
|
767
|
+
process.exit(1);
|
|
768
|
+
}
|
|
769
|
+
let proxyUrl;
|
|
770
|
+
const status = lifecycle.readStatus();
|
|
771
|
+
if (status?.proxyUrl) {
|
|
772
|
+
proxyUrl = status.proxyUrl;
|
|
773
|
+
} else {
|
|
774
|
+
proxyUrl = `ws://127.0.0.1:${config.daemon.proxyPort}`;
|
|
775
|
+
console.error(`[agentbridge] No daemon status found, using config default: ${proxyUrl}`);
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
await waitForProxyReady(proxyUrl);
|
|
779
|
+
} catch (err) {
|
|
780
|
+
console.error(`[agentbridge] ${err.message}`);
|
|
781
|
+
process.exit(1);
|
|
782
|
+
}
|
|
783
|
+
console.log(`Connecting Codex TUI to AgentBridge at ${proxyUrl}...`);
|
|
784
|
+
let savedStty = null;
|
|
785
|
+
if (process.stdin.isTTY) {
|
|
786
|
+
try {
|
|
787
|
+
savedStty = execSync2("stty -g", { encoding: "utf-8", stdio: ["inherit", "pipe", "pipe"] }).trim();
|
|
788
|
+
} catch {}
|
|
789
|
+
}
|
|
790
|
+
function restoreTerminal() {
|
|
791
|
+
if (savedStty && process.stdin.isTTY) {
|
|
792
|
+
try {
|
|
793
|
+
execSync2(`stty ${savedStty}`, { stdio: ["inherit", "ignore", "ignore"] });
|
|
794
|
+
} catch {
|
|
795
|
+
try {
|
|
796
|
+
execSync2("stty sane", { stdio: ["inherit", "ignore", "ignore"] });
|
|
797
|
+
} catch {}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
let ttyFd = null;
|
|
801
|
+
try {
|
|
802
|
+
ttyFd = openSync2("/dev/tty", "w");
|
|
803
|
+
} catch {
|
|
804
|
+
if (process.stdout.isTTY) {
|
|
805
|
+
ttyFd = 1;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (ttyFd !== null) {
|
|
809
|
+
const sequences = [
|
|
810
|
+
"\x1B[<u",
|
|
811
|
+
"\x1B[?2004l",
|
|
812
|
+
"\x1B[?1004l",
|
|
813
|
+
"\x1B[?1049l",
|
|
814
|
+
"\x1B[?25h",
|
|
815
|
+
"\x1B[0m"
|
|
816
|
+
];
|
|
817
|
+
for (const seq of sequences) {
|
|
818
|
+
try {
|
|
819
|
+
writeSync(ttyFd, seq);
|
|
820
|
+
} catch {}
|
|
821
|
+
}
|
|
822
|
+
if (ttyFd !== 1) {
|
|
823
|
+
try {
|
|
824
|
+
closeSync2(ttyFd);
|
|
825
|
+
} catch {}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const fullArgs = [
|
|
830
|
+
"--enable",
|
|
831
|
+
"tui_app_server",
|
|
832
|
+
"--remote",
|
|
833
|
+
proxyUrl,
|
|
834
|
+
...args
|
|
835
|
+
];
|
|
836
|
+
const child = spawn3("codex", fullArgs, {
|
|
837
|
+
stdio: "inherit",
|
|
838
|
+
env: process.env
|
|
839
|
+
});
|
|
840
|
+
if (typeof child.pid === "number") {
|
|
841
|
+
writeFileSync3(stateDir.tuiPidFile, `${child.pid}
|
|
842
|
+
`, "utf-8");
|
|
843
|
+
}
|
|
844
|
+
let cleanedTuiPid = false;
|
|
845
|
+
function cleanupTuiPidFile() {
|
|
846
|
+
if (cleanedTuiPid)
|
|
847
|
+
return;
|
|
848
|
+
cleanedTuiPid = true;
|
|
849
|
+
try {
|
|
850
|
+
unlinkSync2(stateDir.tuiPidFile);
|
|
851
|
+
} catch {}
|
|
852
|
+
}
|
|
853
|
+
process.on("exit", () => {
|
|
854
|
+
restoreTerminal();
|
|
855
|
+
cleanupTuiPidFile();
|
|
856
|
+
});
|
|
857
|
+
process.on("SIGINT", () => {
|
|
858
|
+
restoreTerminal();
|
|
859
|
+
cleanupTuiPidFile();
|
|
860
|
+
process.exit(130);
|
|
861
|
+
});
|
|
862
|
+
process.on("SIGTERM", () => {
|
|
863
|
+
restoreTerminal();
|
|
864
|
+
cleanupTuiPidFile();
|
|
865
|
+
process.exit(143);
|
|
866
|
+
});
|
|
867
|
+
child.on("exit", (code) => {
|
|
868
|
+
cleanupTuiPidFile();
|
|
869
|
+
process.exit(code ?? 0);
|
|
870
|
+
});
|
|
871
|
+
child.on("error", (err) => {
|
|
872
|
+
cleanupTuiPidFile();
|
|
873
|
+
if (err.code === "ENOENT") {
|
|
874
|
+
console.error("Error: codex not found in PATH.");
|
|
875
|
+
console.error("Install Codex: https://github.com/openai/codex");
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
console.error(`Error starting Codex: ${err.message}`);
|
|
879
|
+
process.exit(1);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
function proxyHealthUrl(proxyUrl) {
|
|
883
|
+
const url = new URL(proxyUrl);
|
|
884
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
885
|
+
url.pathname = "/healthz";
|
|
886
|
+
url.search = "";
|
|
887
|
+
url.hash = "";
|
|
888
|
+
return url.toString();
|
|
889
|
+
}
|
|
890
|
+
async function waitForProxyReady(proxyUrl, maxRetries = 20, delayMs = 100) {
|
|
891
|
+
const healthUrl = proxyHealthUrl(proxyUrl);
|
|
892
|
+
for (let attempt = 0;attempt < maxRetries; attempt++) {
|
|
893
|
+
try {
|
|
894
|
+
const response = await fetch(healthUrl);
|
|
895
|
+
if (response.ok) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
} catch {}
|
|
899
|
+
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
900
|
+
}
|
|
901
|
+
throw new Error(`Timed out waiting for Codex proxy readiness on ${healthUrl}`);
|
|
902
|
+
}
|
|
903
|
+
var OWNED_FLAGS2;
|
|
904
|
+
var init_codex = __esm(() => {
|
|
905
|
+
init_state_dir();
|
|
906
|
+
init_config_service();
|
|
907
|
+
init_daemon_lifecycle();
|
|
908
|
+
init_claude();
|
|
909
|
+
OWNED_FLAGS2 = ["--remote"];
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// src/cli/kill.ts
|
|
913
|
+
var exports_kill = {};
|
|
914
|
+
__export(exports_kill, {
|
|
915
|
+
runKill: () => runKill
|
|
916
|
+
});
|
|
917
|
+
import { execFileSync as execFileSync5 } from "child_process";
|
|
918
|
+
import { readFileSync as readFileSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
919
|
+
async function runKill() {
|
|
920
|
+
console.log(`AgentBridge Kill \u2014 stopping daemon and managed Codex TUI
|
|
921
|
+
`);
|
|
922
|
+
const stateDir = new StateDirResolver;
|
|
923
|
+
const controlPort = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
|
|
924
|
+
const lifecycle = new DaemonLifecycle({
|
|
925
|
+
stateDir,
|
|
926
|
+
controlPort,
|
|
927
|
+
log: (msg) => console.log(` ${msg}`)
|
|
928
|
+
});
|
|
929
|
+
lifecycle.markKilled();
|
|
930
|
+
const tuiKilled = await killManagedCodexTui(stateDir, (msg) => console.log(` ${msg}`));
|
|
931
|
+
const killed = await lifecycle.kill();
|
|
932
|
+
if (killed || tuiKilled) {
|
|
933
|
+
console.log(`
|
|
934
|
+
AgentBridge stopped.`);
|
|
935
|
+
console.log("Please restart Claude Code (`agentbridge claude`), switch to a new conversation, or run `/resume` to fully disconnect.");
|
|
936
|
+
} else {
|
|
937
|
+
console.log(`
|
|
938
|
+
No running AgentBridge daemon or managed Codex TUI found.`);
|
|
939
|
+
console.log("Stale state files cleaned up (if any).");
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
async function killManagedCodexTui(stateDir, log, gracefulTimeoutMs = 3000) {
|
|
943
|
+
const pid = readTuiPid(stateDir);
|
|
944
|
+
if (!pid) {
|
|
945
|
+
log("No Codex TUI pid file found");
|
|
946
|
+
removeTuiPidFile(stateDir);
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
if (!isProcessAlive(pid)) {
|
|
950
|
+
log(`Codex TUI pid ${pid} is not alive, cleaning up stale pid file`);
|
|
951
|
+
removeTuiPidFile(stateDir);
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
if (!isManagedCodexTuiProcess(pid)) {
|
|
955
|
+
log(`Pid ${pid} is alive but is NOT a managed AgentBridge Codex TUI \u2014 refusing to kill. Cleaning up stale pid file.`);
|
|
956
|
+
removeTuiPidFile(stateDir);
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
log(`Sending SIGTERM to Codex TUI pid ${pid}`);
|
|
960
|
+
try {
|
|
961
|
+
process.kill(pid, "SIGTERM");
|
|
962
|
+
} catch {
|
|
963
|
+
removeTuiPidFile(stateDir);
|
|
964
|
+
return false;
|
|
965
|
+
}
|
|
966
|
+
const deadline = Date.now() + gracefulTimeoutMs;
|
|
967
|
+
while (Date.now() < deadline) {
|
|
968
|
+
if (!isProcessAlive(pid)) {
|
|
969
|
+
log(`Codex TUI pid ${pid} stopped gracefully`);
|
|
970
|
+
removeTuiPidFile(stateDir);
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
await new Promise((resolve2) => setTimeout(resolve2, 200));
|
|
974
|
+
}
|
|
975
|
+
log(`Codex TUI pid ${pid} did not stop gracefully, sending SIGKILL`);
|
|
976
|
+
try {
|
|
977
|
+
process.kill(pid, "SIGKILL");
|
|
978
|
+
} catch {}
|
|
979
|
+
removeTuiPidFile(stateDir);
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
function readTuiPid(stateDir) {
|
|
983
|
+
try {
|
|
984
|
+
const raw = readFileSync3(stateDir.tuiPidFile, "utf-8").trim();
|
|
985
|
+
if (!raw)
|
|
986
|
+
return null;
|
|
987
|
+
const pid = Number.parseInt(raw, 10);
|
|
988
|
+
return Number.isFinite(pid) ? pid : null;
|
|
989
|
+
} catch {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function removeTuiPidFile(stateDir) {
|
|
994
|
+
try {
|
|
995
|
+
unlinkSync3(stateDir.tuiPidFile);
|
|
996
|
+
} catch {}
|
|
997
|
+
}
|
|
998
|
+
function isManagedCodexTuiProcess(pid) {
|
|
999
|
+
try {
|
|
1000
|
+
const cmd = execFileSync5("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf-8" }).trim();
|
|
1001
|
+
return cmd.includes("codex") && cmd.includes("--enable") && cmd.includes("tui_app_server") && cmd.includes("--remote");
|
|
1002
|
+
} catch {
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
var init_kill = __esm(() => {
|
|
1007
|
+
init_state_dir();
|
|
1008
|
+
init_daemon_lifecycle();
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// package.json
|
|
1012
|
+
var require_package = __commonJS((exports, module) => {
|
|
1013
|
+
module.exports = {
|
|
1014
|
+
name: "@raysonmeng/agentbridge",
|
|
1015
|
+
version: "0.1.0",
|
|
1016
|
+
description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
|
|
1017
|
+
type: "module",
|
|
1018
|
+
bin: {
|
|
1019
|
+
agentbridge: "dist/cli.js",
|
|
1020
|
+
abg: "dist/cli.js"
|
|
1021
|
+
},
|
|
1022
|
+
files: [
|
|
1023
|
+
"dist/",
|
|
1024
|
+
"plugins/",
|
|
1025
|
+
".claude-plugin/",
|
|
1026
|
+
"scripts/postinstall.cjs",
|
|
1027
|
+
"README.md",
|
|
1028
|
+
"LICENSE"
|
|
1029
|
+
],
|
|
1030
|
+
scripts: {
|
|
1031
|
+
start: "bun run src/bridge.ts",
|
|
1032
|
+
"build:cli": "mkdir -p dist && bun build src/cli.ts --outfile dist/cli.js --target bun && chmod +x dist/cli.js",
|
|
1033
|
+
"build:plugin": "mkdir -p plugins/agentbridge/server && bun build src/bridge.ts --outfile plugins/agentbridge/server/bridge-server.js --target bun && bun build src/daemon.ts --outfile plugins/agentbridge/server/daemon.js --target bun",
|
|
1034
|
+
postinstall: "node scripts/postinstall.cjs",
|
|
1035
|
+
prepublishOnly: "bun run build:cli && bun run build:plugin",
|
|
1036
|
+
"validate:plugin": "claude plugin validate plugins/agentbridge && claude plugin validate .claude-plugin/marketplace.json",
|
|
1037
|
+
test: "bun test src",
|
|
1038
|
+
typecheck: "tsc --noEmit",
|
|
1039
|
+
"validate:plugin-versions": "bun scripts/check-plugin-versions.js",
|
|
1040
|
+
check: "tsc --noEmit && bun test src && bun run build:plugin && bun scripts/check-plugin-versions.js"
|
|
1041
|
+
},
|
|
1042
|
+
repository: {
|
|
1043
|
+
type: "git",
|
|
1044
|
+
url: "https://github.com/raysonmeng/agent-bridge.git"
|
|
1045
|
+
},
|
|
1046
|
+
homepage: "https://github.com/raysonmeng/agent-bridge#readme",
|
|
1047
|
+
bugs: {
|
|
1048
|
+
url: "https://github.com/raysonmeng/agent-bridge/issues"
|
|
1049
|
+
},
|
|
1050
|
+
keywords: [
|
|
1051
|
+
"claude-code",
|
|
1052
|
+
"codex",
|
|
1053
|
+
"mcp",
|
|
1054
|
+
"agent",
|
|
1055
|
+
"bridge",
|
|
1056
|
+
"multi-agent",
|
|
1057
|
+
"channels"
|
|
1058
|
+
],
|
|
1059
|
+
author: "AgentBridge Contributors",
|
|
1060
|
+
license: "MIT",
|
|
1061
|
+
devDependencies: {
|
|
1062
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
1063
|
+
"@types/bun": "^1.3.11",
|
|
1064
|
+
typescript: "^5.8.0"
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// src/cli.ts
|
|
1070
|
+
async function main() {
|
|
1071
|
+
switch (command) {
|
|
1072
|
+
case "init":
|
|
1073
|
+
const { runInit: runInit2 } = await Promise.resolve().then(() => (init_init(), exports_init));
|
|
1074
|
+
await runInit2();
|
|
1075
|
+
break;
|
|
1076
|
+
case "dev":
|
|
1077
|
+
const { runDev: runDev2 } = await Promise.resolve().then(() => (init_dev(), exports_dev));
|
|
1078
|
+
await runDev2();
|
|
1079
|
+
break;
|
|
1080
|
+
case "claude":
|
|
1081
|
+
const { runClaude: runClaude2 } = await Promise.resolve().then(() => (init_claude(), exports_claude));
|
|
1082
|
+
await runClaude2(restArgs);
|
|
1083
|
+
break;
|
|
1084
|
+
case "codex":
|
|
1085
|
+
const { runCodex: runCodex2 } = await Promise.resolve().then(() => (init_codex(), exports_codex));
|
|
1086
|
+
await runCodex2(restArgs);
|
|
1087
|
+
break;
|
|
1088
|
+
case "kill":
|
|
1089
|
+
const { runKill: runKill2 } = await Promise.resolve().then(() => (init_kill(), exports_kill));
|
|
1090
|
+
await runKill2();
|
|
1091
|
+
break;
|
|
1092
|
+
case "--help":
|
|
1093
|
+
case "-h":
|
|
1094
|
+
case undefined:
|
|
1095
|
+
printHelp();
|
|
1096
|
+
break;
|
|
1097
|
+
case "--version":
|
|
1098
|
+
case "-v":
|
|
1099
|
+
printVersion();
|
|
1100
|
+
break;
|
|
1101
|
+
default:
|
|
1102
|
+
console.error(`Unknown command: ${command}`);
|
|
1103
|
+
console.error(`Run "agentbridge --help" (or "abg --help") for usage.`);
|
|
1104
|
+
process.exit(1);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function printHelp() {
|
|
1108
|
+
console.log(`
|
|
1109
|
+
AgentBridge \u2014 Multi-agent collaboration bridge
|
|
1110
|
+
|
|
1111
|
+
Usage:
|
|
1112
|
+
agentbridge <command> [args...]
|
|
1113
|
+
abg <command> [args...]
|
|
1114
|
+
|
|
1115
|
+
Commands:
|
|
1116
|
+
init Install plugin, check dependencies, generate project config
|
|
1117
|
+
dev Register local marketplace + install plugin (for local dev)
|
|
1118
|
+
claude [args...] Start Claude Code with push channel enabled
|
|
1119
|
+
codex [args...] Start Codex TUI connected to AgentBridge daemon
|
|
1120
|
+
kill Force kill all AgentBridge processes
|
|
1121
|
+
|
|
1122
|
+
Options:
|
|
1123
|
+
--help, -h Show this help message
|
|
1124
|
+
--version, -v Show version
|
|
1125
|
+
|
|
1126
|
+
Examples:
|
|
1127
|
+
abg init # First-time setup
|
|
1128
|
+
abg claude # Start Claude Code
|
|
1129
|
+
abg claude --resume # Start Claude Code and resume session
|
|
1130
|
+
abg codex # Start Codex TUI
|
|
1131
|
+
abg codex --model o3 # Start Codex with specific model
|
|
1132
|
+
abg kill # Emergency: kill all processes
|
|
1133
|
+
`.trim());
|
|
1134
|
+
}
|
|
1135
|
+
function printVersion() {
|
|
1136
|
+
try {
|
|
1137
|
+
const pkg = require_package();
|
|
1138
|
+
console.log(`agentbridge v${pkg.version}`);
|
|
1139
|
+
} catch {
|
|
1140
|
+
console.log("agentbridge (version unknown)");
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
var args, command, restArgs, MARKETPLACE_NAME = "agentbridge", PLUGIN_NAME = "agentbridge";
|
|
1144
|
+
var init_cli = __esm(() => {
|
|
1145
|
+
args = process.argv.slice(2);
|
|
1146
|
+
command = args[0];
|
|
1147
|
+
restArgs = args.slice(1);
|
|
1148
|
+
main().catch((err) => {
|
|
1149
|
+
console.error(`Error: ${err.message}`);
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
init_cli();
|
|
1154
|
+
|
|
1155
|
+
export {
|
|
1156
|
+
PLUGIN_NAME,
|
|
1157
|
+
MARKETPLACE_NAME
|
|
1158
|
+
};
|