@raysonmeng/agentbridge 0.1.5 → 0.1.6
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 +2 -3
- package/README.zh-CN.md +2 -3
- package/dist/cli.js +324 -88
- package/package.json +1 -1
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +121 -117
- package/plugins/agentbridge/server/daemon.js +336 -124
- package/scripts/postinstall.cjs +44 -4
package/dist/cli.js
CHANGED
|
@@ -20,16 +20,46 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
20
20
|
// src/config-service.ts
|
|
21
21
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
22
22
|
import { join } from "path";
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function normalizeInteger(value, fallback) {
|
|
27
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
28
|
+
return value;
|
|
29
|
+
if (typeof value === "string") {
|
|
30
|
+
const parsed = Number(value);
|
|
31
|
+
if (Number.isFinite(parsed))
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
function normalizeConfig(raw) {
|
|
37
|
+
if (!isRecord(raw))
|
|
38
|
+
return null;
|
|
39
|
+
const config = raw;
|
|
40
|
+
const codex = isRecord(config.codex) ? config.codex : {};
|
|
41
|
+
const daemon = isRecord(config.daemon) ? config.daemon : {};
|
|
42
|
+
const turnCoordination = isRecord(config.turnCoordination) ? config.turnCoordination : {};
|
|
43
|
+
return {
|
|
44
|
+
version: typeof config.version === "string" ? config.version : DEFAULT_CONFIG.version,
|
|
45
|
+
codex: {
|
|
46
|
+
appPort: normalizeInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort),
|
|
47
|
+
proxyPort: normalizeInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort)
|
|
48
|
+
},
|
|
49
|
+
turnCoordination: {
|
|
50
|
+
attentionWindowSeconds: normalizeInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds)
|
|
51
|
+
},
|
|
52
|
+
idleShutdownSeconds: normalizeInteger(config.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
23
55
|
|
|
24
56
|
class ConfigService {
|
|
25
57
|
configDir;
|
|
26
58
|
configPath;
|
|
27
|
-
collaborationPath;
|
|
28
59
|
constructor(projectRoot) {
|
|
29
60
|
const root = projectRoot ?? process.cwd();
|
|
30
61
|
this.configDir = join(root, CONFIG_DIR);
|
|
31
62
|
this.configPath = join(this.configDir, CONFIG_FILE);
|
|
32
|
-
this.collaborationPath = join(this.configDir, COLLABORATION_FILE);
|
|
33
63
|
}
|
|
34
64
|
hasConfig() {
|
|
35
65
|
return existsSync(this.configPath);
|
|
@@ -37,7 +67,7 @@ class ConfigService {
|
|
|
37
67
|
load() {
|
|
38
68
|
try {
|
|
39
69
|
const raw = readFileSync(this.configPath, "utf-8");
|
|
40
|
-
return JSON.parse(raw);
|
|
70
|
+
return normalizeConfig(JSON.parse(raw));
|
|
41
71
|
} catch {
|
|
42
72
|
return null;
|
|
43
73
|
}
|
|
@@ -50,17 +80,6 @@ class ConfigService {
|
|
|
50
80
|
writeFileSync(this.configPath, JSON.stringify(config, null, 2) + `
|
|
51
81
|
`, "utf-8");
|
|
52
82
|
}
|
|
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
83
|
initDefaults() {
|
|
65
84
|
this.ensureConfigDir();
|
|
66
85
|
const created = [];
|
|
@@ -68,74 +87,34 @@ class ConfigService {
|
|
|
68
87
|
this.save(DEFAULT_CONFIG);
|
|
69
88
|
created.push(this.configPath);
|
|
70
89
|
}
|
|
71
|
-
if (!existsSync(this.collaborationPath)) {
|
|
72
|
-
this.saveCollaboration(DEFAULT_COLLABORATION_MD);
|
|
73
|
-
created.push(this.collaborationPath);
|
|
74
|
-
}
|
|
75
90
|
return created;
|
|
76
91
|
}
|
|
77
92
|
get configFilePath() {
|
|
78
93
|
return this.configPath;
|
|
79
94
|
}
|
|
80
|
-
get collaborationFilePath() {
|
|
81
|
-
return this.collaborationPath;
|
|
82
|
-
}
|
|
83
95
|
ensureConfigDir() {
|
|
84
96
|
if (!existsSync(this.configDir)) {
|
|
85
97
|
mkdirSync(this.configDir, { recursive: true });
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
100
|
}
|
|
89
|
-
var DEFAULT_CONFIG,
|
|
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";
|
|
101
|
+
var DEFAULT_CONFIG, CONFIG_DIR = ".agentbridge", CONFIG_FILE = "config.json";
|
|
112
102
|
var init_config_service = __esm(() => {
|
|
113
103
|
DEFAULT_CONFIG = {
|
|
114
104
|
version: "1.0",
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
codex: {
|
|
106
|
+
appPort: 4500,
|
|
117
107
|
proxyPort: 4501
|
|
118
108
|
},
|
|
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
109
|
turnCoordination: {
|
|
130
|
-
attentionWindowSeconds: 15
|
|
131
|
-
busyGuard: true
|
|
110
|
+
attentionWindowSeconds: 15
|
|
132
111
|
},
|
|
133
112
|
idleShutdownSeconds: 30
|
|
134
113
|
};
|
|
135
114
|
});
|
|
136
115
|
|
|
137
116
|
// src/cli/pkg-root.ts
|
|
138
|
-
import { dirname
|
|
117
|
+
import { dirname, join as join2 } from "path";
|
|
139
118
|
import { existsSync as existsSync2 } from "fs";
|
|
140
119
|
import { execFileSync } from "child_process";
|
|
141
120
|
function findPackageRoot() {
|
|
@@ -144,7 +123,7 @@ function findPackageRoot() {
|
|
|
144
123
|
if (existsSync2(join2(dir, "package.json"))) {
|
|
145
124
|
return dir;
|
|
146
125
|
}
|
|
147
|
-
const parent =
|
|
126
|
+
const parent = dirname(dir);
|
|
148
127
|
if (parent === dir) {
|
|
149
128
|
throw new Error("Could not find package.json in any parent directory");
|
|
150
129
|
}
|
|
@@ -158,13 +137,112 @@ function registerMarketplace(marketplaceRoot) {
|
|
|
158
137
|
}
|
|
159
138
|
var init_pkg_root = () => {};
|
|
160
139
|
|
|
140
|
+
// src/marker-section.ts
|
|
141
|
+
function upsertMarkedSection(content, sectionId, section) {
|
|
142
|
+
const startMarker = MARKER_START(sectionId);
|
|
143
|
+
const endMarker = MARKER_END(sectionId);
|
|
144
|
+
const block = `${startMarker}
|
|
145
|
+
${section}
|
|
146
|
+
${endMarker}`;
|
|
147
|
+
const startIdx = content.indexOf(startMarker);
|
|
148
|
+
const endIdx = content.indexOf(endMarker);
|
|
149
|
+
const hasStart = startIdx !== -1;
|
|
150
|
+
const hasEnd = endIdx !== -1;
|
|
151
|
+
if (hasStart && hasEnd && startIdx < endIdx) {
|
|
152
|
+
const before = content.slice(0, startIdx);
|
|
153
|
+
const after = content.slice(endIdx + endMarker.length);
|
|
154
|
+
return before + block + after;
|
|
155
|
+
}
|
|
156
|
+
if (hasStart || hasEnd) {
|
|
157
|
+
throw new Error(`Malformed ${sectionId} markers in file (start=${startIdx}, end=${endIdx}). ` + `Please repair the file manually \u2014 remove the stray marker(s) or restore the pair.`);
|
|
158
|
+
}
|
|
159
|
+
if (content.trim() === "") {
|
|
160
|
+
return block + `
|
|
161
|
+
`;
|
|
162
|
+
}
|
|
163
|
+
const trimmed = content.endsWith(`
|
|
164
|
+
`) ? content : content + `
|
|
165
|
+
`;
|
|
166
|
+
return trimmed + `
|
|
167
|
+
` + block + `
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
var MARKER_START = (id) => `<!-- ${id}:start -->`, MARKER_END = (id) => `<!-- ${id}:end -->`;
|
|
171
|
+
|
|
172
|
+
// src/collaboration-content.ts
|
|
173
|
+
var MARKER_ID = "AgentBridge", CLAUDE_MD_SECTION = `## AgentBridge \u2014 Multi-Agent Collaboration
|
|
174
|
+
|
|
175
|
+
You are working in a **multi-agent environment** powered by AgentBridge.
|
|
176
|
+
Another AI agent (Codex, by OpenAI) is available in a parallel session on this machine.
|
|
177
|
+
|
|
178
|
+
### Communication mechanism
|
|
179
|
+
- **Claude \u2192 Codex**: Use the AgentBridge MCP tools (\`reply\` / \`get_messages\`) \u2014 these are yours only.
|
|
180
|
+
- **Codex \u2192 Claude**: Codex has no symmetric tool. The bridge transparently intercepts Codex's normal output and forwards it to you. Messages arrive as push notifications (or via \`get_messages\` in pull mode).
|
|
181
|
+
- 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
|
+
|
|
183
|
+
### When to collaborate vs. work solo
|
|
184
|
+
- **Collaborate** when the task benefits from a second perspective, parallel execution, or capabilities you lack (e.g., sandboxed code execution, independent verification).
|
|
185
|
+
- **Work solo** for simple, self-contained tasks where the coordination overhead isn't worth it.
|
|
186
|
+
- When in doubt, **propose a task split** to Codex rather than doing everything yourself.
|
|
187
|
+
|
|
188
|
+
### Capability comparison
|
|
189
|
+
| Capability | Claude (you) | Codex |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| Architecture & planning | Strong | Moderate |
|
|
192
|
+
| Code review & analysis | Strong | Strong |
|
|
193
|
+
| Sandboxed code execution | No | Yes |
|
|
194
|
+
| File editing & refactoring | Yes (via tools) | Yes (via sandbox) |
|
|
195
|
+
| Web search & docs | Yes | Limited |
|
|
196
|
+
| Independent verification | Cross-review | Reproduce & test |
|
|
197
|
+
|
|
198
|
+
### How to start collaborating
|
|
199
|
+
1. When you receive a complex task, **proactively propose a division of labor** to Codex via the reply tool.
|
|
200
|
+
2. State what you'll handle and what you'd like Codex to take on.
|
|
201
|
+
3. Ask for Codex's agreement or counter-proposal before proceeding.
|
|
202
|
+
4. After task completion, **cross-review** each other's work.`, AGENTS_MD_SECTION = `## AgentBridge \u2014 Multi-Agent Collaboration
|
|
203
|
+
|
|
204
|
+
You are working in a **multi-agent environment** powered by AgentBridge.
|
|
205
|
+
Another AI agent (Claude, by Anthropic) is available in a parallel session on this machine.
|
|
206
|
+
|
|
207
|
+
### Communication mechanism (read this first)
|
|
208
|
+
AgentBridge is a **transparent proxy** on your side. You do **not** have a tool to "send a message to Claude".
|
|
209
|
+
|
|
210
|
+
- **Codex \u2192 Claude**: Just write your normal response. The bridge intercepts your \`agentMessage\` output and forwards it to Claude automatically. No tool call needed.
|
|
211
|
+
- **Claude \u2192 Codex**: Claude uses its own MCP tools (\`reply\` / \`get_messages\`). Those messages arrive in your session as new user turns \u2014 you'll see them like any other user input.
|
|
212
|
+
|
|
213
|
+
**Do not** search the AgentBridge source for a Codex-side "send" / "reply" / "sendToClaude" API \u2014 it does not exist, and looking for it wastes turns. If you catch yourself thinking "I need to find how to message Claude", stop and just write your reply as normal text.
|
|
214
|
+
|
|
215
|
+
### When to collaborate vs. work solo
|
|
216
|
+
- **Collaborate** when the task benefits from a second perspective, parallel execution, or capabilities the other agent has.
|
|
217
|
+
- **Work solo** for simple, self-contained tasks where the coordination overhead isn't worth it.
|
|
218
|
+
- When in doubt, **propose a task split** to Claude rather than doing everything yourself.
|
|
219
|
+
|
|
220
|
+
### Capability comparison
|
|
221
|
+
| Capability | Codex (you) | Claude |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| Sandboxed code execution | Yes | No |
|
|
224
|
+
| Reproduce & verify bugs | Strong | Limited |
|
|
225
|
+
| Architecture & planning | Moderate | Strong |
|
|
226
|
+
| Code review & analysis | Strong | Strong |
|
|
227
|
+
| Web search & docs | Limited | Yes |
|
|
228
|
+
| File editing & refactoring | Yes (via sandbox) | Yes (via tools) |
|
|
229
|
+
|
|
230
|
+
### How to start collaborating
|
|
231
|
+
1. When you receive a complex task, **proactively propose a division of labor** in your response (Claude will receive it).
|
|
232
|
+
2. State what you'll handle and what you'd like Claude to take on.
|
|
233
|
+
3. Ask for Claude's agreement or counter-proposal before proceeding.
|
|
234
|
+
4. After task completion, **cross-review** each other's work.`;
|
|
235
|
+
|
|
161
236
|
// src/cli/init.ts
|
|
162
237
|
var exports_init = {};
|
|
163
238
|
__export(exports_init, {
|
|
239
|
+
writeCollaborationSections: () => writeCollaborationSections,
|
|
164
240
|
runInit: () => runInit,
|
|
165
241
|
compareVersions: () => compareVersions
|
|
166
242
|
});
|
|
167
243
|
import { execSync, execFileSync as execFileSync2 } from "child_process";
|
|
244
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
245
|
+
import { join as join3 } from "path";
|
|
168
246
|
async function runInit() {
|
|
169
247
|
console.log(`AgentBridge Init
|
|
170
248
|
`);
|
|
@@ -184,6 +262,13 @@ async function runInit() {
|
|
|
184
262
|
console.log(" Project config already exists, skipping.");
|
|
185
263
|
}
|
|
186
264
|
console.log("");
|
|
265
|
+
console.log("Writing collaboration sections...");
|
|
266
|
+
const projectRoot = process.cwd();
|
|
267
|
+
const collabResults = writeCollaborationSections(projectRoot);
|
|
268
|
+
for (const result of collabResults) {
|
|
269
|
+
console.log(` ${result}`);
|
|
270
|
+
}
|
|
271
|
+
console.log("");
|
|
187
272
|
console.log("Installing AgentBridge plugin...");
|
|
188
273
|
try {
|
|
189
274
|
registerMarketplace(findPackageRoot());
|
|
@@ -259,6 +344,40 @@ function compareVersions(a, b) {
|
|
|
259
344
|
}
|
|
260
345
|
return 0;
|
|
261
346
|
}
|
|
347
|
+
function writeCollaborationSections(projectRoot) {
|
|
348
|
+
const results = [];
|
|
349
|
+
const files = [
|
|
350
|
+
{ name: "CLAUDE.md", path: join3(projectRoot, "CLAUDE.md"), section: CLAUDE_MD_SECTION },
|
|
351
|
+
{ name: "AGENTS.md", path: join3(projectRoot, "AGENTS.md"), section: AGENTS_MD_SECTION }
|
|
352
|
+
];
|
|
353
|
+
for (const { name, path, section } of files) {
|
|
354
|
+
let existing = "";
|
|
355
|
+
try {
|
|
356
|
+
existing = readFileSync2(path, "utf-8");
|
|
357
|
+
} catch {}
|
|
358
|
+
let updated;
|
|
359
|
+
try {
|
|
360
|
+
updated = upsertMarkedSection(existing, MARKER_ID, section);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
363
|
+
results.push(`${name}: skipped \u2014 ${msg}`);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (updated === existing) {
|
|
367
|
+
results.push(`${name}: unchanged (section already up to date)`);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
writeFileSync2(path, updated, "utf-8");
|
|
371
|
+
if (existing === "") {
|
|
372
|
+
results.push(`${name}: created with collaboration section`);
|
|
373
|
+
} else if (existing.includes(`<!-- ${MARKER_ID}:start -->`)) {
|
|
374
|
+
results.push(`${name}: updated collaboration section`);
|
|
375
|
+
} else {
|
|
376
|
+
results.push(`${name}: appended collaboration section`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return results;
|
|
380
|
+
}
|
|
262
381
|
var MIN_CLAUDE_VERSION = "2.1.80";
|
|
263
382
|
var init_init = __esm(() => {
|
|
264
383
|
init_config_service();
|
|
@@ -367,7 +486,7 @@ var init_dev = __esm(() => {
|
|
|
367
486
|
|
|
368
487
|
// src/daemon-lifecycle.ts
|
|
369
488
|
import { spawn, execFileSync as execFileSync4 } from "child_process";
|
|
370
|
-
import { existsSync as existsSync4, readFileSync as
|
|
489
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3, openSync, closeSync, constants } from "fs";
|
|
371
490
|
import { fileURLToPath } from "url";
|
|
372
491
|
|
|
373
492
|
class DaemonLifecycle {
|
|
@@ -455,7 +574,7 @@ class DaemonLifecycle {
|
|
|
455
574
|
}
|
|
456
575
|
readStatus() {
|
|
457
576
|
try {
|
|
458
|
-
const raw =
|
|
577
|
+
const raw = readFileSync3(this.stateDir.statusFile, "utf-8");
|
|
459
578
|
return JSON.parse(raw);
|
|
460
579
|
} catch {
|
|
461
580
|
return null;
|
|
@@ -463,12 +582,12 @@ class DaemonLifecycle {
|
|
|
463
582
|
}
|
|
464
583
|
writeStatus(status) {
|
|
465
584
|
this.stateDir.ensure();
|
|
466
|
-
|
|
585
|
+
writeFileSync3(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
|
|
467
586
|
`, "utf-8");
|
|
468
587
|
}
|
|
469
588
|
readPid() {
|
|
470
589
|
try {
|
|
471
|
-
const raw =
|
|
590
|
+
const raw = readFileSync3(this.stateDir.pidFile, "utf-8").trim();
|
|
472
591
|
if (!raw)
|
|
473
592
|
return null;
|
|
474
593
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -479,7 +598,7 @@ class DaemonLifecycle {
|
|
|
479
598
|
}
|
|
480
599
|
writePid(pid) {
|
|
481
600
|
this.stateDir.ensure();
|
|
482
|
-
|
|
601
|
+
writeFileSync3(this.stateDir.pidFile, `${pid ?? process.pid}
|
|
483
602
|
`, "utf-8");
|
|
484
603
|
}
|
|
485
604
|
removePidFile() {
|
|
@@ -494,7 +613,7 @@ class DaemonLifecycle {
|
|
|
494
613
|
}
|
|
495
614
|
markKilled() {
|
|
496
615
|
this.stateDir.ensure();
|
|
497
|
-
|
|
616
|
+
writeFileSync3(this.stateDir.killedFile, `${Date.now()}
|
|
498
617
|
`, "utf-8");
|
|
499
618
|
}
|
|
500
619
|
clearKilled() {
|
|
@@ -532,14 +651,14 @@ class DaemonLifecycle {
|
|
|
532
651
|
this.stateDir.ensure();
|
|
533
652
|
try {
|
|
534
653
|
const fd = openSync(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
535
|
-
|
|
654
|
+
writeFileSync3(fd, `${process.pid}
|
|
536
655
|
`);
|
|
537
656
|
closeSync(fd);
|
|
538
657
|
return true;
|
|
539
658
|
} catch (err) {
|
|
540
659
|
if (err.code === "EEXIST") {
|
|
541
660
|
try {
|
|
542
|
-
const holderPid = Number.parseInt(
|
|
661
|
+
const holderPid = Number.parseInt(readFileSync3(this.stateDir.lockFile, "utf-8").trim(), 10);
|
|
543
662
|
if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
|
|
544
663
|
this.log(`Stale lock file from dead process ${holderPid}, removing`);
|
|
545
664
|
this.releaseLock();
|
|
@@ -631,7 +750,7 @@ var init_daemon_lifecycle = __esm(() => {
|
|
|
631
750
|
|
|
632
751
|
// src/state-dir.ts
|
|
633
752
|
import { mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
|
|
634
|
-
import { join as
|
|
753
|
+
import { join as join4 } from "path";
|
|
635
754
|
import { homedir as homedir2, platform } from "os";
|
|
636
755
|
|
|
637
756
|
class StateDirResolver {
|
|
@@ -641,10 +760,10 @@ class StateDirResolver {
|
|
|
641
760
|
if (override) {
|
|
642
761
|
this.stateDir = override;
|
|
643
762
|
} else if (platform() === "darwin") {
|
|
644
|
-
this.stateDir =
|
|
763
|
+
this.stateDir = join4(homedir2(), "Library", "Application Support", "AgentBridge");
|
|
645
764
|
} else {
|
|
646
|
-
const xdgState = process.env.XDG_STATE_HOME ??
|
|
647
|
-
this.stateDir =
|
|
765
|
+
const xdgState = process.env.XDG_STATE_HOME ?? join4(homedir2(), ".local", "state");
|
|
766
|
+
this.stateDir = join4(xdgState, "agentbridge");
|
|
648
767
|
}
|
|
649
768
|
}
|
|
650
769
|
ensure() {
|
|
@@ -656,25 +775,28 @@ class StateDirResolver {
|
|
|
656
775
|
return this.stateDir;
|
|
657
776
|
}
|
|
658
777
|
get pidFile() {
|
|
659
|
-
return
|
|
778
|
+
return join4(this.stateDir, "daemon.pid");
|
|
660
779
|
}
|
|
661
780
|
get tuiPidFile() {
|
|
662
|
-
return
|
|
781
|
+
return join4(this.stateDir, "codex-tui.pid");
|
|
663
782
|
}
|
|
664
783
|
get lockFile() {
|
|
665
|
-
return
|
|
784
|
+
return join4(this.stateDir, "daemon.lock");
|
|
666
785
|
}
|
|
667
786
|
get statusFile() {
|
|
668
|
-
return
|
|
787
|
+
return join4(this.stateDir, "status.json");
|
|
669
788
|
}
|
|
670
789
|
get portsFile() {
|
|
671
|
-
return
|
|
790
|
+
return join4(this.stateDir, "ports.json");
|
|
672
791
|
}
|
|
673
792
|
get logFile() {
|
|
674
|
-
return
|
|
793
|
+
return join4(this.stateDir, "agentbridge.log");
|
|
794
|
+
}
|
|
795
|
+
get codexWrapperLogFile() {
|
|
796
|
+
return join4(this.stateDir, "codex-wrapper.log");
|
|
675
797
|
}
|
|
676
798
|
get killedFile() {
|
|
677
|
-
return
|
|
799
|
+
return join4(this.stateDir, "killed");
|
|
678
800
|
}
|
|
679
801
|
}
|
|
680
802
|
var init_state_dir = () => {};
|
|
@@ -744,13 +866,88 @@ var init_claude = __esm(() => {
|
|
|
744
866
|
OWNED_FLAGS = ["--channels", "--dangerously-load-development-channels"];
|
|
745
867
|
});
|
|
746
868
|
|
|
869
|
+
// src/stderr-ring-buffer.ts
|
|
870
|
+
class StderrRingBuffer {
|
|
871
|
+
maxBytes;
|
|
872
|
+
chunks = [];
|
|
873
|
+
bytes = 0;
|
|
874
|
+
constructor(maxBytes = DEFAULT_MAX_BYTES) {
|
|
875
|
+
this.maxBytes = maxBytes;
|
|
876
|
+
if (maxBytes <= 0) {
|
|
877
|
+
throw new Error("StderrRingBuffer maxBytes must be positive");
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
append(chunk) {
|
|
881
|
+
if (chunk.length === 0)
|
|
882
|
+
return;
|
|
883
|
+
if (chunk.length >= this.maxBytes) {
|
|
884
|
+
this.chunks = [chunk.subarray(chunk.length - this.maxBytes)];
|
|
885
|
+
this.bytes = this.maxBytes;
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
this.chunks.push(chunk);
|
|
889
|
+
this.bytes += chunk.length;
|
|
890
|
+
while (this.bytes > this.maxBytes && this.chunks.length > 0) {
|
|
891
|
+
const head = this.chunks[0];
|
|
892
|
+
const overflow = this.bytes - this.maxBytes;
|
|
893
|
+
if (head.length <= overflow) {
|
|
894
|
+
this.chunks.shift();
|
|
895
|
+
this.bytes -= head.length;
|
|
896
|
+
} else {
|
|
897
|
+
this.chunks[0] = head.subarray(overflow);
|
|
898
|
+
this.bytes -= overflow;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
snapshot() {
|
|
903
|
+
return Buffer.concat(this.chunks, this.bytes);
|
|
904
|
+
}
|
|
905
|
+
toString(encoding = "utf-8") {
|
|
906
|
+
return this.snapshot().toString(encoding);
|
|
907
|
+
}
|
|
908
|
+
get byteLength() {
|
|
909
|
+
return this.bytes;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
var DEFAULT_MAX_BYTES;
|
|
913
|
+
var init_stderr_ring_buffer = __esm(() => {
|
|
914
|
+
DEFAULT_MAX_BYTES = 64 * 1024;
|
|
915
|
+
});
|
|
916
|
+
|
|
747
917
|
// src/cli/codex.ts
|
|
748
918
|
var exports_codex = {};
|
|
749
919
|
__export(exports_codex, {
|
|
750
920
|
runCodex: () => runCodex
|
|
751
921
|
});
|
|
752
922
|
import { spawn as spawn3, execSync as execSync2 } from "child_process";
|
|
753
|
-
import {
|
|
923
|
+
import {
|
|
924
|
+
openSync as openSync2,
|
|
925
|
+
writeSync,
|
|
926
|
+
closeSync as closeSync2,
|
|
927
|
+
writeFileSync as writeFileSync4,
|
|
928
|
+
unlinkSync as unlinkSync2,
|
|
929
|
+
appendFileSync,
|
|
930
|
+
existsSync as existsSync6,
|
|
931
|
+
mkdirSync as mkdirSync3
|
|
932
|
+
} from "fs";
|
|
933
|
+
import { dirname as dirname2 } from "path";
|
|
934
|
+
function appendWrapperLog(path, entry) {
|
|
935
|
+
try {
|
|
936
|
+
const dir = dirname2(path);
|
|
937
|
+
if (!existsSync6(dir)) {
|
|
938
|
+
mkdirSync3(dir, { recursive: true });
|
|
939
|
+
}
|
|
940
|
+
appendFileSync(path, `[${new Date().toISOString()}] ${entry}
|
|
941
|
+
`, "utf-8");
|
|
942
|
+
} catch {}
|
|
943
|
+
}
|
|
944
|
+
function buildChildEnv() {
|
|
945
|
+
return {
|
|
946
|
+
...process.env,
|
|
947
|
+
RUST_BACKTRACE: process.env.RUST_BACKTRACE ?? "full",
|
|
948
|
+
RUST_LOG: process.env.RUST_LOG ?? "info,codex_core=debug,codex_tui=debug,codex_app_server=debug"
|
|
949
|
+
};
|
|
950
|
+
}
|
|
754
951
|
async function runCodex(args) {
|
|
755
952
|
checkOwnedFlagConflicts(args, "agentbridge codex", OWNED_FLAGS2);
|
|
756
953
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -793,7 +990,7 @@ async function runCodex(args) {
|
|
|
793
990
|
if (status?.proxyUrl) {
|
|
794
991
|
proxyUrl = status.proxyUrl;
|
|
795
992
|
} else {
|
|
796
|
-
proxyUrl = `ws://127.0.0.1:${config.
|
|
993
|
+
proxyUrl = `ws://127.0.0.1:${config.codex.proxyPort}`;
|
|
797
994
|
console.error(`[agentbridge] No daemon status found, using config default: ${proxyUrl}`);
|
|
798
995
|
}
|
|
799
996
|
try {
|
|
@@ -855,13 +1052,27 @@ async function runCodex(args) {
|
|
|
855
1052
|
proxyUrl,
|
|
856
1053
|
...args
|
|
857
1054
|
];
|
|
1055
|
+
const stderrTail = new StderrRingBuffer;
|
|
1056
|
+
const wrapperLogPath = stateDir.codexWrapperLogFile;
|
|
1057
|
+
const startedAt = Date.now();
|
|
1058
|
+
stateDir.ensure();
|
|
1059
|
+
appendWrapperLog(wrapperLogPath, `spawn: codex ${fullArgs.map((a) => a.includes(" ") ? JSON.stringify(a) : a).join(" ")}`);
|
|
858
1060
|
const child = spawn3("codex", fullArgs, {
|
|
859
|
-
stdio: "inherit",
|
|
860
|
-
env:
|
|
1061
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
1062
|
+
env: buildChildEnv()
|
|
861
1063
|
});
|
|
862
1064
|
if (typeof child.pid === "number") {
|
|
863
|
-
|
|
1065
|
+
writeFileSync4(stateDir.tuiPidFile, `${child.pid}
|
|
864
1066
|
`, "utf-8");
|
|
1067
|
+
appendWrapperLog(wrapperLogPath, `child pid=${child.pid}`);
|
|
1068
|
+
}
|
|
1069
|
+
if (child.stderr) {
|
|
1070
|
+
child.stderr.on("data", (chunk) => {
|
|
1071
|
+
try {
|
|
1072
|
+
process.stderr.write(chunk);
|
|
1073
|
+
} catch {}
|
|
1074
|
+
stderrTail.append(chunk);
|
|
1075
|
+
});
|
|
865
1076
|
}
|
|
866
1077
|
let cleanedTuiPid = false;
|
|
867
1078
|
function cleanupTuiPidFile() {
|
|
@@ -886,12 +1097,36 @@ async function runCodex(args) {
|
|
|
886
1097
|
cleanupTuiPidFile();
|
|
887
1098
|
process.exit(143);
|
|
888
1099
|
});
|
|
889
|
-
child.on("exit", (code) => {
|
|
1100
|
+
child.on("exit", (code, signal) => {
|
|
890
1101
|
cleanupTuiPidFile();
|
|
1102
|
+
const runtimeMs = Date.now() - startedAt;
|
|
1103
|
+
const tail = stderrTail.toString();
|
|
1104
|
+
const tailLines = tail.length === 0 ? "(no stderr captured)" : tail;
|
|
1105
|
+
let classification = "normal";
|
|
1106
|
+
if (/ERROR: remote app server/.test(tail))
|
|
1107
|
+
classification = "fatal_exit";
|
|
1108
|
+
else if (/Error: .* failed: Not initialized/.test(tail))
|
|
1109
|
+
classification = "not_initialized_after_reconnect";
|
|
1110
|
+
else if (/Error: .* failed:/.test(tail))
|
|
1111
|
+
classification = "rpc_error_exit";
|
|
1112
|
+
else if (signal)
|
|
1113
|
+
classification = `signal:${signal}`;
|
|
1114
|
+
else if (typeof code === "number" && code !== 0)
|
|
1115
|
+
classification = `nonzero_exit:${code}`;
|
|
1116
|
+
else if (code === 0 && tail.trim().length === 0)
|
|
1117
|
+
classification = "exit_0_empty_stderr";
|
|
1118
|
+
appendWrapperLog(wrapperLogPath, [
|
|
1119
|
+
`exit: code=${code ?? "null"} signal=${signal ?? "null"} runtime_ms=${runtimeMs} pid=${child.pid ?? "unknown"} classification=${classification}`,
|
|
1120
|
+
`--- last stderr (${stderrTail.byteLength} bytes) ---`,
|
|
1121
|
+
tailLines,
|
|
1122
|
+
`--- end stderr ---`
|
|
1123
|
+
].join(`
|
|
1124
|
+
`));
|
|
891
1125
|
process.exit(code ?? 0);
|
|
892
1126
|
});
|
|
893
1127
|
child.on("error", (err) => {
|
|
894
1128
|
cleanupTuiPidFile();
|
|
1129
|
+
appendWrapperLog(wrapperLogPath, `spawn error: ${err.message}`);
|
|
895
1130
|
if (err.code === "ENOENT") {
|
|
896
1131
|
console.error("Error: codex not found in PATH.");
|
|
897
1132
|
console.error("Install Codex: https://github.com/openai/codex");
|
|
@@ -927,6 +1162,7 @@ var init_codex = __esm(() => {
|
|
|
927
1162
|
init_state_dir();
|
|
928
1163
|
init_config_service();
|
|
929
1164
|
init_daemon_lifecycle();
|
|
1165
|
+
init_stderr_ring_buffer();
|
|
930
1166
|
init_claude();
|
|
931
1167
|
OWNED_FLAGS2 = ["--remote"];
|
|
932
1168
|
});
|
|
@@ -937,7 +1173,7 @@ __export(exports_kill, {
|
|
|
937
1173
|
runKill: () => runKill
|
|
938
1174
|
});
|
|
939
1175
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
940
|
-
import { readFileSync as
|
|
1176
|
+
import { readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
941
1177
|
async function runKill() {
|
|
942
1178
|
console.log(`AgentBridge Kill \u2014 stopping daemon and managed Codex TUI
|
|
943
1179
|
`);
|
|
@@ -1003,7 +1239,7 @@ async function killManagedCodexTui(stateDir, log, gracefulTimeoutMs = 3000) {
|
|
|
1003
1239
|
}
|
|
1004
1240
|
function readTuiPid(stateDir) {
|
|
1005
1241
|
try {
|
|
1006
|
-
const raw =
|
|
1242
|
+
const raw = readFileSync4(stateDir.tuiPidFile, "utf-8").trim();
|
|
1007
1243
|
if (!raw)
|
|
1008
1244
|
return null;
|
|
1009
1245
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -1034,7 +1270,7 @@ var init_kill = __esm(() => {
|
|
|
1034
1270
|
var require_package = __commonJS((exports, module) => {
|
|
1035
1271
|
module.exports = {
|
|
1036
1272
|
name: "@raysonmeng/agentbridge",
|
|
1037
|
-
version: "0.1.
|
|
1273
|
+
version: "0.1.6",
|
|
1038
1274
|
description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
|
|
1039
1275
|
type: "module",
|
|
1040
1276
|
bin: {
|
package/package.json
CHANGED