@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.
@@ -13663,6 +13663,60 @@ class StdioServerTransport {
13663
13663
  import { EventEmitter } from "events";
13664
13664
  import { randomUUID } from "crypto";
13665
13665
  import { appendFileSync } from "fs";
13666
+
13667
+ // src/state-dir.ts
13668
+ import { mkdirSync, existsSync } from "fs";
13669
+ import { join } from "path";
13670
+ import { homedir, platform } from "os";
13671
+
13672
+ class StateDirResolver {
13673
+ stateDir;
13674
+ constructor(envOverride) {
13675
+ const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
13676
+ if (override) {
13677
+ this.stateDir = override;
13678
+ } else if (platform() === "darwin") {
13679
+ this.stateDir = join(homedir(), "Library", "Application Support", "AgentBridge");
13680
+ } else {
13681
+ const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
13682
+ this.stateDir = join(xdgState, "agentbridge");
13683
+ }
13684
+ }
13685
+ ensure() {
13686
+ if (!existsSync(this.stateDir)) {
13687
+ mkdirSync(this.stateDir, { recursive: true });
13688
+ }
13689
+ }
13690
+ get dir() {
13691
+ return this.stateDir;
13692
+ }
13693
+ get pidFile() {
13694
+ return join(this.stateDir, "daemon.pid");
13695
+ }
13696
+ get tuiPidFile() {
13697
+ return join(this.stateDir, "codex-tui.pid");
13698
+ }
13699
+ get lockFile() {
13700
+ return join(this.stateDir, "daemon.lock");
13701
+ }
13702
+ get statusFile() {
13703
+ return join(this.stateDir, "status.json");
13704
+ }
13705
+ get portsFile() {
13706
+ return join(this.stateDir, "ports.json");
13707
+ }
13708
+ get logFile() {
13709
+ return join(this.stateDir, "agentbridge.log");
13710
+ }
13711
+ get codexWrapperLogFile() {
13712
+ return join(this.stateDir, "codex-wrapper.log");
13713
+ }
13714
+ get killedFile() {
13715
+ return join(this.stateDir, "killed");
13716
+ }
13717
+ }
13718
+
13719
+ // src/claude-adapter.ts
13666
13720
  var CLAUDE_INSTRUCTIONS = [
13667
13721
  "Codex is an AI coding agent (OpenAI) running in a separate session on the same machine.",
13668
13722
  "",
@@ -13697,23 +13751,27 @@ var CLAUDE_INSTRUCTIONS = [
13697
13751
  "- If the reply tool returns a busy error, Codex is still executing \u2014 wait and try again later."
13698
13752
  ].join(`
13699
13753
  `);
13700
- var LOG_FILE = "/tmp/agentbridge.log";
13701
13754
 
13702
13755
  class ClaudeAdapter extends EventEmitter {
13703
13756
  server;
13704
13757
  notificationSeq = 0;
13705
13758
  sessionId;
13706
13759
  notificationIdPrefix;
13760
+ instanceId;
13707
13761
  replySender = null;
13762
+ logFile;
13708
13763
  configuredMode;
13709
13764
  resolvedMode = null;
13710
13765
  pendingMessages = [];
13711
13766
  maxBufferedMessages;
13712
13767
  droppedMessageCount = 0;
13713
- constructor() {
13768
+ constructor(logFile = new StateDirResolver().logFile) {
13714
13769
  super();
13770
+ this.logFile = logFile;
13771
+ this.instanceId = randomUUID().slice(0, 8);
13715
13772
  this.sessionId = `codex_${Date.now()}`;
13716
13773
  this.notificationIdPrefix = randomUUID().replace(/-/g, "").slice(0, 12);
13774
+ this.log(`ClaudeAdapter created (instance=${this.instanceId})`);
13717
13775
  const envMode = process.env.AGENTBRIDGE_MODE;
13718
13776
  this.configuredMode = envMode && ["push", "pull", "auto"].includes(envMode) ? envMode : "auto";
13719
13777
  this.maxBufferedMessages = parseInt(process.env.AGENTBRIDGE_MAX_BUFFERED_MESSAGES ?? "100", 10);
@@ -13749,11 +13807,12 @@ class ClaudeAdapter extends EventEmitter {
13749
13807
  this.resolvedMode = this.configuredMode;
13750
13808
  this.log(`Delivery mode set by AGENTBRIDGE_MODE: ${this.resolvedMode}`);
13751
13809
  } else {
13752
- this.resolvedMode = "pull";
13753
- this.log("Delivery mode defaulting to pull (set AGENTBRIDGE_MODE=push to opt into channel delivery)");
13810
+ this.resolvedMode = "push";
13811
+ this.log("Delivery mode defaulting to push (set AGENTBRIDGE_MODE=pull to use polling instead)");
13754
13812
  }
13755
13813
  }
13756
13814
  async pushNotification(message) {
13815
+ this.log(`pushNotification (instance=${this.instanceId}, mode=${this.resolvedMode}, msgId=${message.id}, len=${message.content.length})`);
13757
13816
  if (this.resolvedMode === "push") {
13758
13817
  await this.pushViaChannel(message);
13759
13818
  } else {
@@ -13791,9 +13850,10 @@ class ClaudeAdapter extends EventEmitter {
13791
13850
  this.log(`Message queue full, dropped oldest message (total dropped: ${this.droppedMessageCount})`);
13792
13851
  }
13793
13852
  this.pendingMessages.push(message);
13794
- this.log(`Queued message for pull (${this.pendingMessages.length} pending)`);
13853
+ this.log(`Queued message for pull (${this.pendingMessages.length} pending, instance=${this.instanceId})`);
13795
13854
  }
13796
13855
  drainMessages() {
13856
+ this.log(`get_messages called (instance=${this.instanceId}, pending=${this.pendingMessages.length}, dropped=${this.droppedMessageCount})`);
13797
13857
  if (this.pendingMessages.length === 0 && this.droppedMessageCount === 0) {
13798
13858
  return {
13799
13859
  content: [{ type: "text", text: "No new messages from Codex." }]
@@ -13818,6 +13878,7 @@ Codex: ${msg.content}`;
13818
13878
  }).join(`
13819
13879
 
13820
13880
  `);
13881
+ this.log(`get_messages returning ${count} message(s) (instance=${this.instanceId}, dropped=${dropped})`);
13821
13882
  return {
13822
13883
  content: [
13823
13884
  {
@@ -13923,7 +13984,7 @@ ${formatted}`
13923
13984
  `;
13924
13985
  process.stderr.write(line);
13925
13986
  try {
13926
- appendFileSync(LOG_FILE, line);
13987
+ appendFileSync(this.logFile, line);
13927
13988
  } catch {}
13928
13989
  }
13929
13990
  }
@@ -14083,7 +14144,7 @@ class DaemonClient extends EventEmitter2 {
14083
14144
 
14084
14145
  // src/daemon-lifecycle.ts
14085
14146
  import { spawn, execFileSync } from "child_process";
14086
- import { existsSync, readFileSync, unlinkSync, writeFileSync, openSync, closeSync, constants } from "fs";
14147
+ import { existsSync as existsSync2, readFileSync, unlinkSync, writeFileSync, openSync, closeSync, constants } from "fs";
14087
14148
  import { fileURLToPath } from "url";
14088
14149
  var DAEMON_ENTRY = process.env.AGENTBRIDGE_DAEMON_ENTRY ?? "./daemon.ts";
14089
14150
  var DAEMON_PATH = fileURLToPath(new URL(DAEMON_ENTRY, import.meta.url));
@@ -14221,7 +14282,7 @@ class DaemonLifecycle {
14221
14282
  } catch {}
14222
14283
  }
14223
14284
  wasKilled() {
14224
- return existsSync(this.stateDir.killedFile);
14285
+ return existsSync2(this.stateDir.killedFile);
14225
14286
  }
14226
14287
  launch() {
14227
14288
  this.stateDir.ensure();
@@ -14342,116 +14403,62 @@ function isProcessAlive(pid) {
14342
14403
  }
14343
14404
  }
14344
14405
 
14345
- // src/state-dir.ts
14346
- import { mkdirSync, existsSync as existsSync2 } from "fs";
14347
- import { join } from "path";
14348
- import { homedir, platform } from "os";
14349
-
14350
- class StateDirResolver {
14351
- stateDir;
14352
- constructor(envOverride) {
14353
- const override = envOverride ?? process.env.AGENTBRIDGE_STATE_DIR;
14354
- if (override) {
14355
- this.stateDir = override;
14356
- } else if (platform() === "darwin") {
14357
- this.stateDir = join(homedir(), "Library", "Application Support", "AgentBridge");
14358
- } else {
14359
- const xdgState = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
14360
- this.stateDir = join(xdgState, "agentbridge");
14361
- }
14362
- }
14363
- ensure() {
14364
- if (!existsSync2(this.stateDir)) {
14365
- mkdirSync(this.stateDir, { recursive: true });
14366
- }
14367
- }
14368
- get dir() {
14369
- return this.stateDir;
14370
- }
14371
- get pidFile() {
14372
- return join(this.stateDir, "daemon.pid");
14373
- }
14374
- get tuiPidFile() {
14375
- return join(this.stateDir, "codex-tui.pid");
14376
- }
14377
- get lockFile() {
14378
- return join(this.stateDir, "daemon.lock");
14379
- }
14380
- get statusFile() {
14381
- return join(this.stateDir, "status.json");
14382
- }
14383
- get portsFile() {
14384
- return join(this.stateDir, "ports.json");
14385
- }
14386
- get logFile() {
14387
- return join(this.stateDir, "agentbridge.log");
14388
- }
14389
- get killedFile() {
14390
- return join(this.stateDir, "killed");
14391
- }
14392
- }
14393
-
14394
14406
  // src/config-service.ts
14395
14407
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
14396
14408
  import { join as join2 } from "path";
14397
14409
  var DEFAULT_CONFIG = {
14398
14410
  version: "1.0",
14399
- daemon: {
14400
- port: 4500,
14411
+ codex: {
14412
+ appPort: 4500,
14401
14413
  proxyPort: 4501
14402
14414
  },
14403
- agents: {
14404
- claude: {
14405
- role: "Reviewer, Planner",
14406
- mode: "push"
14407
- },
14408
- codex: {
14409
- role: "Implementer, Executor"
14410
- }
14411
- },
14412
- markers: ["IMPORTANT", "STATUS", "FYI"],
14413
14415
  turnCoordination: {
14414
- attentionWindowSeconds: 15,
14415
- busyGuard: true
14416
+ attentionWindowSeconds: 15
14416
14417
  },
14417
14418
  idleShutdownSeconds: 30
14418
14419
  };
14419
- var DEFAULT_COLLABORATION_MD = `# Collaboration Rules
14420
-
14421
- ## Roles
14422
- - Claude: Reviewer, Planner, Hypothesis Challenger
14423
- - Codex: Implementer, Executor, Reproducer/Verifier
14424
-
14425
- ## Thinking Patterns
14426
- - Analytical/review tasks: Independent Analysis & Convergence
14427
- - Implementation tasks: Architect -> Builder -> Critic
14428
- - Debugging tasks: Hypothesis -> Experiment -> Interpretation
14429
-
14430
- ## Communication
14431
- - Use explicit phrases: "My independent view is:", "I agree on:", "I disagree on:", "Current consensus:"
14432
- - Tag messages with [IMPORTANT], [STATUS], or [FYI]
14433
-
14434
- ## Review Process
14435
- - Cross-review: author never reviews their own code
14436
- - All changes go through feature/fix branches + PR
14437
- - Merge via squash merge
14438
-
14439
- ## Custom Rules
14440
- <!-- Add your project-specific collaboration rules here -->
14441
- `;
14442
14420
  var CONFIG_DIR = ".agentbridge";
14443
14421
  var CONFIG_FILE = "config.json";
14444
- var COLLABORATION_FILE = "collaboration.md";
14422
+ function isRecord(value) {
14423
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14424
+ }
14425
+ function normalizeInteger(value, fallback) {
14426
+ if (typeof value === "number" && Number.isFinite(value))
14427
+ return value;
14428
+ if (typeof value === "string") {
14429
+ const parsed = Number(value);
14430
+ if (Number.isFinite(parsed))
14431
+ return parsed;
14432
+ }
14433
+ return fallback;
14434
+ }
14435
+ function normalizeConfig(raw) {
14436
+ if (!isRecord(raw))
14437
+ return null;
14438
+ const config2 = raw;
14439
+ const codex = isRecord(config2.codex) ? config2.codex : {};
14440
+ const daemon = isRecord(config2.daemon) ? config2.daemon : {};
14441
+ const turnCoordination = isRecord(config2.turnCoordination) ? config2.turnCoordination : {};
14442
+ return {
14443
+ version: typeof config2.version === "string" ? config2.version : DEFAULT_CONFIG.version,
14444
+ codex: {
14445
+ appPort: normalizeInteger(codex.appPort ?? daemon.port, DEFAULT_CONFIG.codex.appPort),
14446
+ proxyPort: normalizeInteger(codex.proxyPort ?? daemon.proxyPort, DEFAULT_CONFIG.codex.proxyPort)
14447
+ },
14448
+ turnCoordination: {
14449
+ attentionWindowSeconds: normalizeInteger(turnCoordination.attentionWindowSeconds, DEFAULT_CONFIG.turnCoordination.attentionWindowSeconds)
14450
+ },
14451
+ idleShutdownSeconds: normalizeInteger(config2.idleShutdownSeconds, DEFAULT_CONFIG.idleShutdownSeconds)
14452
+ };
14453
+ }
14445
14454
 
14446
14455
  class ConfigService {
14447
14456
  configDir;
14448
14457
  configPath;
14449
- collaborationPath;
14450
14458
  constructor(projectRoot) {
14451
14459
  const root = projectRoot ?? process.cwd();
14452
14460
  this.configDir = join2(root, CONFIG_DIR);
14453
14461
  this.configPath = join2(this.configDir, CONFIG_FILE);
14454
- this.collaborationPath = join2(this.configDir, COLLABORATION_FILE);
14455
14462
  }
14456
14463
  hasConfig() {
14457
14464
  return existsSync3(this.configPath);
@@ -14459,7 +14466,7 @@ class ConfigService {
14459
14466
  load() {
14460
14467
  try {
14461
14468
  const raw = readFileSync2(this.configPath, "utf-8");
14462
- return JSON.parse(raw);
14469
+ return normalizeConfig(JSON.parse(raw));
14463
14470
  } catch {
14464
14471
  return null;
14465
14472
  }
@@ -14472,17 +14479,6 @@ class ConfigService {
14472
14479
  writeFileSync2(this.configPath, JSON.stringify(config2, null, 2) + `
14473
14480
  `, "utf-8");
14474
14481
  }
14475
- loadCollaboration() {
14476
- try {
14477
- return readFileSync2(this.collaborationPath, "utf-8");
14478
- } catch {
14479
- return null;
14480
- }
14481
- }
14482
- saveCollaboration(content) {
14483
- this.ensureConfigDir();
14484
- writeFileSync2(this.collaborationPath, content, "utf-8");
14485
- }
14486
14482
  initDefaults() {
14487
14483
  this.ensureConfigDir();
14488
14484
  const created = [];
@@ -14490,18 +14486,11 @@ class ConfigService {
14490
14486
  this.save(DEFAULT_CONFIG);
14491
14487
  created.push(this.configPath);
14492
14488
  }
14493
- if (!existsSync3(this.collaborationPath)) {
14494
- this.saveCollaboration(DEFAULT_COLLABORATION_MD);
14495
- created.push(this.collaborationPath);
14496
- }
14497
14489
  return created;
14498
14490
  }
14499
14491
  get configFilePath() {
14500
14492
  return this.configPath;
14501
14493
  }
14502
- get collaborationFilePath() {
14503
- return this.collaborationPath;
14504
- }
14505
14494
  ensureConfigDir() {
14506
14495
  if (!existsSync3(this.configDir)) {
14507
14496
  mkdirSync2(this.configDir, { recursive: true });
@@ -14521,16 +14510,19 @@ function disabledReplyError(reason) {
14521
14510
 
14522
14511
  // src/bridge.ts
14523
14512
  var stateDir = new StateDirResolver;
14513
+ stateDir.ensure();
14524
14514
  var configService = new ConfigService;
14525
14515
  var config2 = configService.loadOrDefault();
14526
14516
  var CONTROL_PORT = parseInt(process.env.AGENTBRIDGE_CONTROL_PORT ?? "4502", 10);
14527
14517
  var daemonLifecycle = new DaemonLifecycle({ stateDir, controlPort: CONTROL_PORT, log });
14528
14518
  var CONTROL_WS_URL = daemonLifecycle.controlWsUrl;
14529
- var claude = new ClaudeAdapter;
14519
+ var claude = new ClaudeAdapter(stateDir.logFile);
14530
14520
  var daemonClient = new DaemonClient(CONTROL_WS_URL);
14531
14521
  var shuttingDown = false;
14532
14522
  var daemonDisabled = false;
14533
14523
  var daemonDisabledReason = null;
14524
+ var hasSeenTuiConnect = false;
14525
+ var previousTuiConnected = false;
14534
14526
  var RECONNECT_NOTIFY_COOLDOWN_MS = 30000;
14535
14527
  var DISABLED_RECOVERY_INTERVAL_MS = 5000;
14536
14528
  var lastDisconnectNotifyTs = 0;
@@ -14555,6 +14547,18 @@ daemonClient.on("codexMessage", (message) => {
14555
14547
  });
14556
14548
  daemonClient.on("status", (status) => {
14557
14549
  log(`Daemon status: ready=${status.bridgeReady} tui=${status.tuiConnected} thread=${status.threadId ?? "none"} queued=${status.queuedMessageCount}`);
14550
+ if (!hasSeenTuiConnect && status.tuiConnected && !previousTuiConnected) {
14551
+ hasSeenTuiConnect = true;
14552
+ log("First TUI connect detected \u2014 sending kickoff message to Claude");
14553
+ claude.pushNotification(systemMessage("system_tui_kickoff", [
14554
+ "\uD83E\uDD1D Codex has connected via AgentBridge.",
14555
+ "You are now in a multi-agent collaboration session.",
14556
+ "When you receive a complex task, propose a division of labor to Codex.",
14557
+ "Use `reply` to send messages and `get_messages` to check for responses."
14558
+ ].join(`
14559
+ `)));
14560
+ }
14561
+ previousTuiConnected = status.tuiConnected;
14558
14562
  });
14559
14563
  daemonClient.on("disconnect", () => {
14560
14564
  if (shuttingDown || daemonDisabled)