@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/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, 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";
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
- daemon: {
116
- port: 4500,
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 as dirname2, join as join2 } from "path";
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 = dirname2(dir);
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 readFileSync2, unlinkSync, writeFileSync as writeFileSync2, openSync, closeSync, constants } from "fs";
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 = readFileSync2(this.stateDir.statusFile, "utf-8");
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
- writeFileSync2(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
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 = readFileSync2(this.stateDir.pidFile, "utf-8").trim();
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
- writeFileSync2(this.stateDir.pidFile, `${pid ?? process.pid}
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
- writeFileSync2(this.stateDir.killedFile, `${Date.now()}
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
- writeFileSync2(fd, `${process.pid}
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(readFileSync2(this.stateDir.lockFile, "utf-8").trim(), 10);
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 join3 } from "path";
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 = join3(homedir2(), "Library", "Application Support", "AgentBridge");
763
+ this.stateDir = join4(homedir2(), "Library", "Application Support", "AgentBridge");
623
764
  } else {
624
- const xdgState = process.env.XDG_STATE_HOME ?? join3(homedir2(), ".local", "state");
625
- this.stateDir = join3(xdgState, "agentbridge");
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 join3(this.stateDir, "daemon.pid");
778
+ return join4(this.stateDir, "daemon.pid");
638
779
  }
639
780
  get tuiPidFile() {
640
- return join3(this.stateDir, "codex-tui.pid");
781
+ return join4(this.stateDir, "codex-tui.pid");
641
782
  }
642
783
  get lockFile() {
643
- return join3(this.stateDir, "daemon.lock");
784
+ return join4(this.stateDir, "daemon.lock");
644
785
  }
645
786
  get statusFile() {
646
- return join3(this.stateDir, "status.json");
787
+ return join4(this.stateDir, "status.json");
647
788
  }
648
789
  get portsFile() {
649
- return join3(this.stateDir, "ports.json");
790
+ return join4(this.stateDir, "ports.json");
650
791
  }
651
792
  get logFile() {
652
- return join3(this.stateDir, "agentbridge.log");
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 join3(this.stateDir, "killed");
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 { openSync as openSync2, writeSync, closeSync as closeSync2, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
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.daemon.proxyPort}`;
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: process.env
1061
+ stdio: ["inherit", "inherit", "pipe"],
1062
+ env: buildChildEnv()
839
1063
  });
840
1064
  if (typeof child.pid === "number") {
841
- writeFileSync3(stateDir.tuiPidFile, `${child.pid}
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 readFileSync3, unlinkSync as unlinkSync3 } from "fs";
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 = readFileSync3(stateDir.tuiPidFile, "utf-8").trim();
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.4",
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 build:plugin && bun scripts/check-plugin-versions.js"
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",