@raysonmeng/agentbridge 0.1.4 → 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 +349 -90
- package/package.json +3 -2
- package/plugins/agentbridge/.claude-plugin/plugin.json +1 -1
- package/plugins/agentbridge/server/bridge-server.js +156 -118
- package/plugins/agentbridge/server/daemon.js +639 -176
- 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();
|
|
@@ -271,7 +390,7 @@ var exports_dev = {};
|
|
|
271
390
|
__export(exports_dev, {
|
|
272
391
|
runDev: () => runDev
|
|
273
392
|
});
|
|
274
|
-
import { execFileSync as execFileSync3 } from "child_process";
|
|
393
|
+
import { execFileSync as execFileSync3, spawnSync } from "child_process";
|
|
275
394
|
import { resolve } from "path";
|
|
276
395
|
import { existsSync as existsSync3, cpSync, rmSync } from "fs";
|
|
277
396
|
import { homedir } from "os";
|
|
@@ -282,6 +401,28 @@ async function runDev() {
|
|
|
282
401
|
const marketplacePath = resolve(projectRoot, ".claude-plugin", "marketplace.json");
|
|
283
402
|
const pluginDir = resolve(projectRoot, "plugins", "agentbridge");
|
|
284
403
|
const pluginManifest = resolve(pluginDir, ".claude-plugin", "plugin.json");
|
|
404
|
+
console.log("Building CLI from source...");
|
|
405
|
+
const cliBuild = spawnSync("bun", ["run", "build:cli"], {
|
|
406
|
+
cwd: projectRoot,
|
|
407
|
+
stdio: "inherit"
|
|
408
|
+
});
|
|
409
|
+
if (cliBuild.status !== 0) {
|
|
410
|
+
console.error(" ERROR: CLI build failed. Fix build errors and try again.");
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
console.log(` \u2713 CLI built successfully
|
|
414
|
+
`);
|
|
415
|
+
console.log("Building plugin from source...");
|
|
416
|
+
const buildResult = spawnSync("bun", ["run", "build:plugin"], {
|
|
417
|
+
cwd: projectRoot,
|
|
418
|
+
stdio: "inherit"
|
|
419
|
+
});
|
|
420
|
+
if (buildResult.status !== 0) {
|
|
421
|
+
console.error(" ERROR: Plugin build failed. Fix build errors and try again.");
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
console.log(` \u2713 Plugin built successfully
|
|
425
|
+
`);
|
|
285
426
|
if (!existsSync3(pluginManifest)) {
|
|
286
427
|
console.error(` ERROR: Plugin manifest not found at ${pluginManifest}`);
|
|
287
428
|
console.error(" Run 'bun run build:plugin' first, or check your working tree.");
|
|
@@ -345,7 +486,7 @@ var init_dev = __esm(() => {
|
|
|
345
486
|
|
|
346
487
|
// src/daemon-lifecycle.ts
|
|
347
488
|
import { spawn, execFileSync as execFileSync4 } from "child_process";
|
|
348
|
-
import { existsSync as existsSync4, readFileSync as
|
|
489
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync3, openSync, closeSync, constants } from "fs";
|
|
349
490
|
import { fileURLToPath } from "url";
|
|
350
491
|
|
|
351
492
|
class DaemonLifecycle {
|
|
@@ -433,7 +574,7 @@ class DaemonLifecycle {
|
|
|
433
574
|
}
|
|
434
575
|
readStatus() {
|
|
435
576
|
try {
|
|
436
|
-
const raw =
|
|
577
|
+
const raw = readFileSync3(this.stateDir.statusFile, "utf-8");
|
|
437
578
|
return JSON.parse(raw);
|
|
438
579
|
} catch {
|
|
439
580
|
return null;
|
|
@@ -441,12 +582,12 @@ class DaemonLifecycle {
|
|
|
441
582
|
}
|
|
442
583
|
writeStatus(status) {
|
|
443
584
|
this.stateDir.ensure();
|
|
444
|
-
|
|
585
|
+
writeFileSync3(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
|
|
445
586
|
`, "utf-8");
|
|
446
587
|
}
|
|
447
588
|
readPid() {
|
|
448
589
|
try {
|
|
449
|
-
const raw =
|
|
590
|
+
const raw = readFileSync3(this.stateDir.pidFile, "utf-8").trim();
|
|
450
591
|
if (!raw)
|
|
451
592
|
return null;
|
|
452
593
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -457,7 +598,7 @@ class DaemonLifecycle {
|
|
|
457
598
|
}
|
|
458
599
|
writePid(pid) {
|
|
459
600
|
this.stateDir.ensure();
|
|
460
|
-
|
|
601
|
+
writeFileSync3(this.stateDir.pidFile, `${pid ?? process.pid}
|
|
461
602
|
`, "utf-8");
|
|
462
603
|
}
|
|
463
604
|
removePidFile() {
|
|
@@ -472,7 +613,7 @@ class DaemonLifecycle {
|
|
|
472
613
|
}
|
|
473
614
|
markKilled() {
|
|
474
615
|
this.stateDir.ensure();
|
|
475
|
-
|
|
616
|
+
writeFileSync3(this.stateDir.killedFile, `${Date.now()}
|
|
476
617
|
`, "utf-8");
|
|
477
618
|
}
|
|
478
619
|
clearKilled() {
|
|
@@ -510,14 +651,14 @@ class DaemonLifecycle {
|
|
|
510
651
|
this.stateDir.ensure();
|
|
511
652
|
try {
|
|
512
653
|
const fd = openSync(this.stateDir.lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
513
|
-
|
|
654
|
+
writeFileSync3(fd, `${process.pid}
|
|
514
655
|
`);
|
|
515
656
|
closeSync(fd);
|
|
516
657
|
return true;
|
|
517
658
|
} catch (err) {
|
|
518
659
|
if (err.code === "EEXIST") {
|
|
519
660
|
try {
|
|
520
|
-
const holderPid = Number.parseInt(
|
|
661
|
+
const holderPid = Number.parseInt(readFileSync3(this.stateDir.lockFile, "utf-8").trim(), 10);
|
|
521
662
|
if (Number.isFinite(holderPid) && !isProcessAlive(holderPid)) {
|
|
522
663
|
this.log(`Stale lock file from dead process ${holderPid}, removing`);
|
|
523
664
|
this.releaseLock();
|
|
@@ -609,7 +750,7 @@ var init_daemon_lifecycle = __esm(() => {
|
|
|
609
750
|
|
|
610
751
|
// src/state-dir.ts
|
|
611
752
|
import { mkdirSync as mkdirSync2, existsSync as existsSync5 } from "fs";
|
|
612
|
-
import { join as
|
|
753
|
+
import { join as join4 } from "path";
|
|
613
754
|
import { homedir as homedir2, platform } from "os";
|
|
614
755
|
|
|
615
756
|
class StateDirResolver {
|
|
@@ -619,10 +760,10 @@ class StateDirResolver {
|
|
|
619
760
|
if (override) {
|
|
620
761
|
this.stateDir = override;
|
|
621
762
|
} else if (platform() === "darwin") {
|
|
622
|
-
this.stateDir =
|
|
763
|
+
this.stateDir = join4(homedir2(), "Library", "Application Support", "AgentBridge");
|
|
623
764
|
} else {
|
|
624
|
-
const xdgState = process.env.XDG_STATE_HOME ??
|
|
625
|
-
this.stateDir =
|
|
765
|
+
const xdgState = process.env.XDG_STATE_HOME ?? join4(homedir2(), ".local", "state");
|
|
766
|
+
this.stateDir = join4(xdgState, "agentbridge");
|
|
626
767
|
}
|
|
627
768
|
}
|
|
628
769
|
ensure() {
|
|
@@ -634,25 +775,28 @@ class StateDirResolver {
|
|
|
634
775
|
return this.stateDir;
|
|
635
776
|
}
|
|
636
777
|
get pidFile() {
|
|
637
|
-
return
|
|
778
|
+
return join4(this.stateDir, "daemon.pid");
|
|
638
779
|
}
|
|
639
780
|
get tuiPidFile() {
|
|
640
|
-
return
|
|
781
|
+
return join4(this.stateDir, "codex-tui.pid");
|
|
641
782
|
}
|
|
642
783
|
get lockFile() {
|
|
643
|
-
return
|
|
784
|
+
return join4(this.stateDir, "daemon.lock");
|
|
644
785
|
}
|
|
645
786
|
get statusFile() {
|
|
646
|
-
return
|
|
787
|
+
return join4(this.stateDir, "status.json");
|
|
647
788
|
}
|
|
648
789
|
get portsFile() {
|
|
649
|
-
return
|
|
790
|
+
return join4(this.stateDir, "ports.json");
|
|
650
791
|
}
|
|
651
792
|
get logFile() {
|
|
652
|
-
return
|
|
793
|
+
return join4(this.stateDir, "agentbridge.log");
|
|
794
|
+
}
|
|
795
|
+
get codexWrapperLogFile() {
|
|
796
|
+
return join4(this.stateDir, "codex-wrapper.log");
|
|
653
797
|
}
|
|
654
798
|
get killedFile() {
|
|
655
|
-
return
|
|
799
|
+
return join4(this.stateDir, "killed");
|
|
656
800
|
}
|
|
657
801
|
}
|
|
658
802
|
var init_state_dir = () => {};
|
|
@@ -722,13 +866,88 @@ var init_claude = __esm(() => {
|
|
|
722
866
|
OWNED_FLAGS = ["--channels", "--dangerously-load-development-channels"];
|
|
723
867
|
});
|
|
724
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
|
+
|
|
725
917
|
// src/cli/codex.ts
|
|
726
918
|
var exports_codex = {};
|
|
727
919
|
__export(exports_codex, {
|
|
728
920
|
runCodex: () => runCodex
|
|
729
921
|
});
|
|
730
922
|
import { spawn as spawn3, execSync as execSync2 } from "child_process";
|
|
731
|
-
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
|
+
}
|
|
732
951
|
async function runCodex(args) {
|
|
733
952
|
checkOwnedFlagConflicts(args, "agentbridge codex", OWNED_FLAGS2);
|
|
734
953
|
for (let i = 0;i < args.length; i++) {
|
|
@@ -771,7 +990,7 @@ async function runCodex(args) {
|
|
|
771
990
|
if (status?.proxyUrl) {
|
|
772
991
|
proxyUrl = status.proxyUrl;
|
|
773
992
|
} else {
|
|
774
|
-
proxyUrl = `ws://127.0.0.1:${config.
|
|
993
|
+
proxyUrl = `ws://127.0.0.1:${config.codex.proxyPort}`;
|
|
775
994
|
console.error(`[agentbridge] No daemon status found, using config default: ${proxyUrl}`);
|
|
776
995
|
}
|
|
777
996
|
try {
|
|
@@ -833,13 +1052,27 @@ async function runCodex(args) {
|
|
|
833
1052
|
proxyUrl,
|
|
834
1053
|
...args
|
|
835
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(" ")}`);
|
|
836
1060
|
const child = spawn3("codex", fullArgs, {
|
|
837
|
-
stdio: "inherit",
|
|
838
|
-
env:
|
|
1061
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
1062
|
+
env: buildChildEnv()
|
|
839
1063
|
});
|
|
840
1064
|
if (typeof child.pid === "number") {
|
|
841
|
-
|
|
1065
|
+
writeFileSync4(stateDir.tuiPidFile, `${child.pid}
|
|
842
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
|
+
});
|
|
843
1076
|
}
|
|
844
1077
|
let cleanedTuiPid = false;
|
|
845
1078
|
function cleanupTuiPidFile() {
|
|
@@ -864,12 +1097,36 @@ async function runCodex(args) {
|
|
|
864
1097
|
cleanupTuiPidFile();
|
|
865
1098
|
process.exit(143);
|
|
866
1099
|
});
|
|
867
|
-
child.on("exit", (code) => {
|
|
1100
|
+
child.on("exit", (code, signal) => {
|
|
868
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
|
+
`));
|
|
869
1125
|
process.exit(code ?? 0);
|
|
870
1126
|
});
|
|
871
1127
|
child.on("error", (err) => {
|
|
872
1128
|
cleanupTuiPidFile();
|
|
1129
|
+
appendWrapperLog(wrapperLogPath, `spawn error: ${err.message}`);
|
|
873
1130
|
if (err.code === "ENOENT") {
|
|
874
1131
|
console.error("Error: codex not found in PATH.");
|
|
875
1132
|
console.error("Install Codex: https://github.com/openai/codex");
|
|
@@ -905,6 +1162,7 @@ var init_codex = __esm(() => {
|
|
|
905
1162
|
init_state_dir();
|
|
906
1163
|
init_config_service();
|
|
907
1164
|
init_daemon_lifecycle();
|
|
1165
|
+
init_stderr_ring_buffer();
|
|
908
1166
|
init_claude();
|
|
909
1167
|
OWNED_FLAGS2 = ["--remote"];
|
|
910
1168
|
});
|
|
@@ -915,7 +1173,7 @@ __export(exports_kill, {
|
|
|
915
1173
|
runKill: () => runKill
|
|
916
1174
|
});
|
|
917
1175
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
918
|
-
import { readFileSync as
|
|
1176
|
+
import { readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
919
1177
|
async function runKill() {
|
|
920
1178
|
console.log(`AgentBridge Kill \u2014 stopping daemon and managed Codex TUI
|
|
921
1179
|
`);
|
|
@@ -981,7 +1239,7 @@ async function killManagedCodexTui(stateDir, log, gracefulTimeoutMs = 3000) {
|
|
|
981
1239
|
}
|
|
982
1240
|
function readTuiPid(stateDir) {
|
|
983
1241
|
try {
|
|
984
|
-
const raw =
|
|
1242
|
+
const raw = readFileSync4(stateDir.tuiPidFile, "utf-8").trim();
|
|
985
1243
|
if (!raw)
|
|
986
1244
|
return null;
|
|
987
1245
|
const pid = Number.parseInt(raw, 10);
|
|
@@ -1012,7 +1270,7 @@ var init_kill = __esm(() => {
|
|
|
1012
1270
|
var require_package = __commonJS((exports, module) => {
|
|
1013
1271
|
module.exports = {
|
|
1014
1272
|
name: "@raysonmeng/agentbridge",
|
|
1015
|
-
version: "0.1.
|
|
1273
|
+
version: "0.1.6",
|
|
1016
1274
|
description: "Bridge between Claude Code and Codex \u2014 bidirectional agent communication via MCP Channel + JSON-RPC",
|
|
1017
1275
|
type: "module",
|
|
1018
1276
|
bin: {
|
|
@@ -1031,13 +1289,14 @@ var require_package = __commonJS((exports, module) => {
|
|
|
1031
1289
|
start: "bun run src/bridge.ts",
|
|
1032
1290
|
"build:cli": "mkdir -p dist && bun build src/cli.ts --outfile dist/cli.js --target bun && chmod +x dist/cli.js",
|
|
1033
1291
|
"build:plugin": "mkdir -p plugins/agentbridge/server && bun build src/bridge.ts --outfile plugins/agentbridge/server/bridge-server.js --target bun && bun build src/daemon.ts --outfile plugins/agentbridge/server/daemon.js --target bun",
|
|
1292
|
+
"verify:plugin-sync": "node scripts/verify-plugin-sync.cjs",
|
|
1034
1293
|
postinstall: "node scripts/postinstall.cjs",
|
|
1035
1294
|
prepublishOnly: "bun run build:cli && bun run build:plugin",
|
|
1036
1295
|
"validate:plugin": "claude plugin validate plugins/agentbridge && claude plugin validate .claude-plugin/marketplace.json",
|
|
1037
1296
|
test: "bun test src",
|
|
1038
1297
|
typecheck: "tsc --noEmit",
|
|
1039
1298
|
"validate:plugin-versions": "bun scripts/check-plugin-versions.js",
|
|
1040
|
-
check: "tsc --noEmit && bun test src && bun run
|
|
1299
|
+
check: "tsc --noEmit && bun test src && bun run verify:plugin-sync && bun scripts/check-plugin-versions.js"
|
|
1041
1300
|
},
|
|
1042
1301
|
repository: {
|
|
1043
1302
|
type: "git",
|