@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/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();
@@ -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 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";
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 = readFileSync2(this.stateDir.statusFile, "utf-8");
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
- writeFileSync2(this.stateDir.statusFile, JSON.stringify(status, null, 2) + `
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 = readFileSync2(this.stateDir.pidFile, "utf-8").trim();
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
- writeFileSync2(this.stateDir.pidFile, `${pid ?? process.pid}
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
- writeFileSync2(this.stateDir.killedFile, `${Date.now()}
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
- writeFileSync2(fd, `${process.pid}
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(readFileSync2(this.stateDir.lockFile, "utf-8").trim(), 10);
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 join3 } from "path";
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 = join3(homedir2(), "Library", "Application Support", "AgentBridge");
763
+ this.stateDir = join4(homedir2(), "Library", "Application Support", "AgentBridge");
645
764
  } else {
646
- const xdgState = process.env.XDG_STATE_HOME ?? join3(homedir2(), ".local", "state");
647
- this.stateDir = join3(xdgState, "agentbridge");
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 join3(this.stateDir, "daemon.pid");
778
+ return join4(this.stateDir, "daemon.pid");
660
779
  }
661
780
  get tuiPidFile() {
662
- return join3(this.stateDir, "codex-tui.pid");
781
+ return join4(this.stateDir, "codex-tui.pid");
663
782
  }
664
783
  get lockFile() {
665
- return join3(this.stateDir, "daemon.lock");
784
+ return join4(this.stateDir, "daemon.lock");
666
785
  }
667
786
  get statusFile() {
668
- return join3(this.stateDir, "status.json");
787
+ return join4(this.stateDir, "status.json");
669
788
  }
670
789
  get portsFile() {
671
- return join3(this.stateDir, "ports.json");
790
+ return join4(this.stateDir, "ports.json");
672
791
  }
673
792
  get logFile() {
674
- 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");
675
797
  }
676
798
  get killedFile() {
677
- return join3(this.stateDir, "killed");
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 { 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
+ }
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.daemon.proxyPort}`;
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: process.env
1061
+ stdio: ["inherit", "inherit", "pipe"],
1062
+ env: buildChildEnv()
861
1063
  });
862
1064
  if (typeof child.pid === "number") {
863
- writeFileSync3(stateDir.tuiPidFile, `${child.pid}
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 readFileSync3, unlinkSync as unlinkSync3 } from "fs";
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 = readFileSync3(stateDir.tuiPidFile, "utf-8").trim();
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.5",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raysonmeng/agentbridge",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Bridge between Claude Code and Codex — bidirectional agent communication via MCP Channel + JSON-RPC",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentbridge",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Bridge Claude Code and Codex with a shared daemon, push channel delivery, and bidirectional reply tooling.",
5
5
  "author": {
6
6
  "name": "AgentBridge Contributors",