@hua-labs/tap 0.2.1 → 0.2.2

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/index.d.mts CHANGED
@@ -10,6 +10,8 @@ interface AdapterContext {
10
10
  platform: Platform;
11
11
  /** Instance ID for TAP_AGENT_ID env injection. Set by 'tap add'. */
12
12
  instanceId?: string;
13
+ /** Agent name from state. Injected as TAP_AGENT_NAME in MCP config. */
14
+ agentName?: string;
13
15
  }
14
16
  interface ProbeResult {
15
17
  installed: boolean;
@@ -129,6 +131,10 @@ interface InstanceState {
129
131
  bridge: BridgeState | null;
130
132
  /** Headless mode configuration. null = interactive (default). */
131
133
  headless: HeadlessConfig | null;
134
+ /** Whether bridge manages its own app-server process. Saved for restart mode preservation. */
135
+ manageAppServer?: boolean;
136
+ /** Whether bridge runs without auth gateway. Saved for restart mode preservation. */
137
+ noAuth?: boolean;
132
138
  warnings: string[];
133
139
  }
134
140
  /** @deprecated Use InstanceState. Kept for v1 state migration. */
@@ -199,6 +205,8 @@ interface TapSharedConfig {
199
205
  appServerUrl?: string;
200
206
  /** GitHub URL for the comms repository (used by `tap comms pull/push`). */
201
207
  commsRepoUrl?: string;
208
+ /** Control tower agent name. Used for auto-notify on new agent join (M111). */
209
+ towerName?: string;
202
210
  }
203
211
  /**
204
212
  * Local config (tap-config.local.json) — gitignored, machine-specific overrides.
@@ -214,6 +222,7 @@ interface TapResolvedConfig {
214
222
  stateDir: string;
215
223
  runtimeCommand: string;
216
224
  appServerUrl: string;
225
+ towerName: string | null;
217
226
  }
218
227
  /** Config resolution source for diagnostics. */
219
228
  type ConfigSource = "cli-flag" | "env" | "local-config" | "shared-config" | "legacy-shell-config" | "auto";
@@ -239,6 +248,46 @@ declare function resolveConfig(overrides?: ConfigOverrides, startDir?: string):
239
248
  declare function saveSharedConfig(repoRoot: string, config: TapSharedConfig): void;
240
249
  declare function saveLocalConfig(repoRoot: string, config: TapLocalConfig): void;
241
250
 
251
+ interface BridgeStartOptions {
252
+ instanceId: InstanceId;
253
+ runtime: RuntimeName;
254
+ stateDir: string;
255
+ commsDir: string;
256
+ bridgeScript: string;
257
+ platform: Platform;
258
+ agentName?: string;
259
+ runtimeCommand?: string;
260
+ appServerUrl?: string;
261
+ repoRoot?: string;
262
+ port?: number;
263
+ /** Headless configuration. Passed as env vars to the bridge process. */
264
+ headless?: HeadlessConfig | null;
265
+ /** Bridge script operational flags (forwarded to codex-app-server-bridge.ts) */
266
+ busyMode?: "steer" | "wait";
267
+ pollSeconds?: number;
268
+ reconnectSeconds?: number;
269
+ messageLookbackMinutes?: number;
270
+ threadId?: string;
271
+ ephemeral?: boolean;
272
+ processExistingMessages?: boolean;
273
+ manageAppServer?: boolean;
274
+ /** Skip auth gateway — app-server listens directly on the public port (localhost only). */
275
+ noAuth?: boolean;
276
+ }
277
+ interface RestartBridgeOptions extends BridgeStartOptions {
278
+ /** Max seconds to wait for active turn to complete before killing. Default: 30 */
279
+ drainTimeoutSeconds?: number;
280
+ }
281
+ /**
282
+ * Graceful bridge restart: wait for active turn → cleanup → stop → start.
283
+ * Prevents message loss during restart by draining active work first
284
+ * and replaying unprocessed messages on the new instance.
285
+ *
286
+ * For headless instances: drain phase cleans up headless dispatch files
287
+ * to prevent the new bridge from re-injecting completed review requests.
288
+ * (별 finding: eager marking + replay collision)
289
+ */
290
+ declare function restartBridge(options: RestartBridgeOptions): Promise<BridgeState>;
242
291
  declare function rotateLog(logPath: string): void;
243
292
  /**
244
293
  * Update the heartbeat timestamp for a running bridge.
@@ -355,6 +404,19 @@ declare function startAgents(options?: AgentControlOptions): Promise<AgentContro
355
404
  * Always operates on the cwd-based repo (same as CLI commands).
356
405
  */
357
406
  declare function stopAgents(): Promise<AgentControlResult>;
407
+ interface HealthReport {
408
+ ok: boolean;
409
+ timestamp: string;
410
+ bridges: DashboardSnapshot["bridges"];
411
+ agents: DashboardSnapshot["agents"];
412
+ warnings: DashboardSnapshot["warnings"];
413
+ headless: Record<string, unknown>[];
414
+ }
415
+ /**
416
+ * Health check that combines dashboard snapshot with headless state.
417
+ * Consumed by monitoring tools (Uptime Kuma, cron, autopilot).
418
+ */
419
+ declare function getHealthReport(options?: StateApiOptions): HealthReport;
358
420
  /**
359
421
  * Resolve tap configuration for API consumers.
360
422
  * Returns paths and settings without requiring CLI args.
@@ -431,4 +493,4 @@ declare function resolveNodeRuntime(configCommand: string, repoRoot: string): Re
431
493
  */
432
494
  declare function buildRuntimeEnv(repoRoot: string, baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
433
495
 
434
- export { type AdapterContext, type AgentControlOptions, type AgentControlResult, type AgentInfo, type AppServerAuthState, type AppServerState, type ApplyResult, type ArtifactKind, type BridgeInfo, type BridgeMode, type BridgeState, type CommandCode, type CommandName, type CommandResult, type ConfigOverrides, type ConfigResolution, type ConfigSource, type DashboardSnapshot, type DashboardWarning, type EventStreamOptions, type HttpServerOptions, type InstanceId, type InstanceState, LOCAL_CONFIG_FILE, type OwnedArtifact, type PRInfo, type PatchOp, type PatchOpType, type PatchPlan, type Platform, type ProbeResult, type ResolvedRuntime, type RuntimeAdapter, type RuntimeName, type RuntimeSource, type RuntimeState, SHARED_CONFIG_FILE, type StateApiOptions, type TapLocalConfig, type TapResolvedConfig, type TapSharedConfig, type TapState, type TapStateV1, type VerifyCheck, type VerifyResult, buildRuntimeEnv, collectDashboardSnapshot, createInitialState, getConfig, getDashboardSnapshot, getFnmBinDir, getHeartbeatAge, loadLocalConfig, loadSharedConfig, loadState, probeFnmNode, readNodeVersion, resolveConfig, resolveNodeRuntime, rotateLog, saveLocalConfig, saveSharedConfig, saveState, startAgents, startHttpServer, stateExists, stopAgents, streamEvents, updateBridgeHeartbeat, version };
496
+ export { type AdapterContext, type AgentControlOptions, type AgentControlResult, type AgentInfo, type AppServerAuthState, type AppServerState, type ApplyResult, type ArtifactKind, type BridgeInfo, type BridgeMode, type BridgeState, type CommandCode, type CommandName, type CommandResult, type ConfigOverrides, type ConfigResolution, type ConfigSource, type DashboardSnapshot, type DashboardWarning, type EventStreamOptions, type HealthReport, type HttpServerOptions, type InstanceId, type InstanceState, LOCAL_CONFIG_FILE, type OwnedArtifact, type PRInfo, type PatchOp, type PatchOpType, type PatchPlan, type Platform, type ProbeResult, type ResolvedRuntime, type RuntimeAdapter, type RuntimeName, type RuntimeSource, type RuntimeState, SHARED_CONFIG_FILE, type StateApiOptions, type TapLocalConfig, type TapResolvedConfig, type TapSharedConfig, type TapState, type TapStateV1, type VerifyCheck, type VerifyResult, buildRuntimeEnv, collectDashboardSnapshot, createInitialState, getConfig, getDashboardSnapshot, getFnmBinDir, getHealthReport, getHeartbeatAge, loadLocalConfig, loadSharedConfig, loadState, probeFnmNode, readNodeVersion, resolveConfig, resolveNodeRuntime, restartBridge, rotateLog, saveLocalConfig, saveSharedConfig, saveState, startAgents, startHttpServer, stateExists, stopAgents, streamEvents, updateBridgeHeartbeat, version };
package/dist/index.mjs CHANGED
@@ -199,7 +199,8 @@ function resolveConfig(overrides = {}, startDir) {
199
199
  commsDir: "auto",
200
200
  stateDir: "auto",
201
201
  runtimeCommand: "auto",
202
- appServerUrl: "auto"
202
+ appServerUrl: "auto",
203
+ towerName: "auto"
203
204
  };
204
205
  let commsDir;
205
206
  if (overrides.commsDir) {
@@ -268,8 +269,16 @@ function resolveConfig(overrides = {}, startDir) {
268
269
  } else {
269
270
  appServerUrl = DEFAULT_APP_SERVER_URL;
270
271
  }
272
+ const towerName = local.towerName ?? shared.towerName ?? null;
271
273
  return {
272
- config: { repoRoot, commsDir, stateDir, runtimeCommand, appServerUrl },
274
+ config: {
275
+ repoRoot,
276
+ commsDir,
277
+ stateDir,
278
+ runtimeCommand,
279
+ appServerUrl,
280
+ towerName
281
+ },
273
282
  sources
274
283
  };
275
284
  }
@@ -469,6 +478,10 @@ function canWriteOrCreate(filePath) {
469
478
  return false;
470
479
  }
471
480
  }
481
+ function isEphemeralPath(p) {
482
+ const normalized = p.replace(/\\/g, "/").toLowerCase();
483
+ return normalized.includes("/_npx/") || normalized.includes("\\_npx\\") || normalized.includes("/fnm_multishells/") || normalized.includes("\\fnm_multishells\\") || normalized.includes("/tmp/") || normalized.includes("\\temp\\");
484
+ }
472
485
  function findLocalTapCommsSource(ctx) {
473
486
  const candidates = [
474
487
  path5.join(
@@ -528,8 +541,10 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
528
541
  const warnings = [];
529
542
  const issues = [];
530
543
  const env = {
531
- TAP_AGENT_NAME: "<set-per-session>",
532
- TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
544
+ TAP_AGENT_NAME: ctx.agentName ?? "<set-per-session>",
545
+ TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir),
546
+ TAP_STATE_DIR: toForwardSlashPath(ctx.stateDir),
547
+ TAP_REPO_ROOT: toForwardSlashPath(ctx.repoRoot)
533
548
  };
534
549
  if (instanceId) {
535
550
  env.TAP_AGENT_ID = instanceId;
@@ -542,11 +557,29 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
542
557
  }
543
558
  const isBundled = sourcePath.endsWith(".mjs");
544
559
  let command = bunCommand;
560
+ let args = [toForwardSlashPath(sourcePath)];
545
561
  if (!command && isBundled) {
546
- command = process.execPath;
547
- warnings.push(
548
- "bun not found; using node to run the compiled MCP server. Install bun for better performance."
549
- );
562
+ const isEphemeralSource = isEphemeralPath(sourcePath);
563
+ const isEphemeralNode = isEphemeralPath(process.execPath);
564
+ if (isEphemeralSource) {
565
+ command = "npx";
566
+ args = ["@hua-labs/tap", "serve"];
567
+ warnings.push(
568
+ "Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
569
+ );
570
+ } else if (isEphemeralNode) {
571
+ command = "node";
572
+ warnings.push(
573
+ "Detected ephemeral node path. Using `node` from PATH for MCP config stability."
574
+ );
575
+ } else {
576
+ command = toForwardSlashPath(process.execPath);
577
+ }
578
+ if (!isEphemeralSource) {
579
+ warnings.push(
580
+ "bun not found; using node to run the compiled MCP server. Install bun for better performance."
581
+ );
582
+ }
550
583
  }
551
584
  if (!command) {
552
585
  issues.push(
@@ -555,8 +588,8 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
555
588
  return { command: null, args: [], env, sourcePath, warnings, issues };
556
589
  }
557
590
  return {
558
- command: isBundled && command === process.execPath ? toForwardSlashPath(command) : command,
559
- args: [toForwardSlashPath(sourcePath)],
591
+ command,
592
+ args,
560
593
  env,
561
594
  sourcePath,
562
595
  warnings,
@@ -1558,6 +1591,40 @@ function isBridgeRunning(stateDir, instanceId) {
1558
1591
  if (!state) return false;
1559
1592
  return isProcessAlive(state.pid);
1560
1593
  }
1594
+ function resolveAgentName(instanceId, explicit, context) {
1595
+ if (explicit) return explicit;
1596
+ try {
1597
+ const repoRoot = context?.repoRoot ?? context?.stateDir?.replace(/[\\/].tap-comms$/, "") ?? process.cwd();
1598
+ const state = loadState(repoRoot);
1599
+ const stateAgent = state?.instances[instanceId]?.agentName;
1600
+ if (stateAgent) return stateAgent;
1601
+ } catch {
1602
+ }
1603
+ return process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME || null;
1604
+ }
1605
+ function inferRestartMode(bridgeState, flags, savedMode) {
1606
+ const wasManaged = bridgeState?.appServer != null;
1607
+ const hadAuth = bridgeState?.appServer?.auth != null;
1608
+ const manageAppServer = flags?.noServer === true ? false : flags?.noServer === void 0 ? savedMode?.manageAppServer ?? wasManaged : true;
1609
+ const noAuth = flags?.noAuth === true ? true : flags?.noAuth === void 0 ? savedMode?.noAuth ?? !hadAuth : false;
1610
+ return { manageAppServer, noAuth };
1611
+ }
1612
+ function cleanupHeadlessDispatch(inboxDir, agentName) {
1613
+ const removed = [];
1614
+ if (!fs7.existsSync(inboxDir)) return removed;
1615
+ const normalizedAgent = agentName.replace(/-/g, "_");
1616
+ const marker = `-headless-${normalizedAgent}-review-`;
1617
+ try {
1618
+ for (const file of fs7.readdirSync(inboxDir)) {
1619
+ if (file.includes(marker)) {
1620
+ fs7.unlinkSync(path7.join(inboxDir, file));
1621
+ removed.push(file);
1622
+ }
1623
+ }
1624
+ } catch {
1625
+ }
1626
+ return removed;
1627
+ }
1561
1628
  async function startBridge(options) {
1562
1629
  const {
1563
1630
  instanceId,
@@ -1568,7 +1635,10 @@ async function startBridge(options) {
1568
1635
  agentName,
1569
1636
  port
1570
1637
  } = options;
1571
- const resolvedAgent = agentName || process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;
1638
+ const resolvedAgent = resolveAgentName(instanceId, agentName, {
1639
+ repoRoot: options.repoRoot,
1640
+ stateDir
1641
+ });
1572
1642
  if (!resolvedAgent) {
1573
1643
  throw new Error(
1574
1644
  `No agent name for ${instanceId} bridge. Set TAP_AGENT_NAME env var or pass --agent-name flag.`
@@ -1720,6 +1790,35 @@ async function stopBridge(options) {
1720
1790
  clearBridgeState(stateDir, instanceId);
1721
1791
  return true;
1722
1792
  }
1793
+ async function restartBridge(options) {
1794
+ const { instanceId, stateDir, platform } = options;
1795
+ const drainTimeout = (options.drainTimeoutSeconds ?? 30) * 1e3;
1796
+ const repoRoot = options.repoRoot ?? stateDir.replace(/[\\/].tap-comms$/, "");
1797
+ const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
1798
+ const heartbeatPath = path7.join(runtimeStateDir, "heartbeat.json");
1799
+ if (fs7.existsSync(heartbeatPath)) {
1800
+ const startWait = Date.now();
1801
+ while (Date.now() - startWait < drainTimeout) {
1802
+ try {
1803
+ const hb = JSON.parse(fs7.readFileSync(heartbeatPath, "utf-8"));
1804
+ if (!hb.activeTurnId) break;
1805
+ } catch {
1806
+ break;
1807
+ }
1808
+ await new Promise((r) => setTimeout(r, 1e3));
1809
+ }
1810
+ }
1811
+ if (options.headless?.enabled && options.commsDir) {
1812
+ const agentName = options.agentName ?? instanceId;
1813
+ cleanupHeadlessDispatch(path7.join(options.commsDir, "inbox"), agentName);
1814
+ }
1815
+ await stopBridge({ instanceId, stateDir, platform });
1816
+ const restartOptions = {
1817
+ ...options,
1818
+ processExistingMessages: true
1819
+ };
1820
+ return startBridge(restartOptions);
1821
+ }
1723
1822
  function rotateLog(logPath) {
1724
1823
  if (!fs7.existsSync(logPath)) return;
1725
1824
  try {
@@ -1763,6 +1862,7 @@ var init_bridge = __esm({
1763
1862
  "use strict";
1764
1863
  init_common();
1765
1864
  init_runtime();
1865
+ init_state();
1766
1866
  DEFAULT_APP_SERVER_URL2 = "ws://127.0.0.1:4501";
1767
1867
  APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
1768
1868
  APP_SERVER_START_TIMEOUT_MS = 2e4;
@@ -3230,7 +3330,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3230
3330
  log(`TUI connect: ${bridge.appServer.url}`);
3231
3331
  }
3232
3332
  }
3233
- const updated = { ...instance, bridge };
3333
+ const updated = { ...instance, bridge, manageAppServer, noAuth };
3234
3334
  const newState = updateInstanceState(state, instanceId, updated);
3235
3335
  saveState(repoRoot, newState);
3236
3336
  return {
@@ -3721,6 +3821,121 @@ function bridgeStatusOne(identifier) {
3721
3821
  }
3722
3822
  };
3723
3823
  }
3824
+ async function bridgeRestart(identifier, flags) {
3825
+ const repoRoot = findRepoRoot();
3826
+ const state = loadState(repoRoot);
3827
+ if (!state) {
3828
+ return {
3829
+ ok: false,
3830
+ command: "bridge",
3831
+ code: "TAP_NOT_INITIALIZED",
3832
+ message: "Not initialized. Run: npx @hua-labs/tap init",
3833
+ warnings: [],
3834
+ data: {}
3835
+ };
3836
+ }
3837
+ const resolved = resolveInstanceId(identifier, state);
3838
+ if (!resolved.ok) {
3839
+ return {
3840
+ ok: false,
3841
+ command: "bridge",
3842
+ code: resolved.code,
3843
+ message: resolved.message,
3844
+ warnings: [],
3845
+ data: {}
3846
+ };
3847
+ }
3848
+ const instanceId = resolved.instanceId;
3849
+ const inst = state.instances[instanceId];
3850
+ if (!inst) {
3851
+ return {
3852
+ ok: false,
3853
+ command: "bridge",
3854
+ code: "TAP_INSTANCE_NOT_FOUND",
3855
+ message: `Instance not found: ${instanceId}`,
3856
+ warnings: [],
3857
+ data: {}
3858
+ };
3859
+ }
3860
+ const adapter = getAdapter(inst.runtime);
3861
+ const ctx = {
3862
+ ...createAdapterContext(state.commsDir, repoRoot),
3863
+ instanceId
3864
+ };
3865
+ const bridgeScript = adapter.resolveBridgeScript?.(ctx);
3866
+ if (!bridgeScript) {
3867
+ return {
3868
+ ok: false,
3869
+ command: "bridge",
3870
+ instanceId,
3871
+ code: "TAP_BRIDGE_SCRIPT_MISSING",
3872
+ message: `Bridge script not found for ${instanceId}`,
3873
+ warnings: [],
3874
+ data: {}
3875
+ };
3876
+ }
3877
+ const { config: resolvedConfig } = resolveConfig({}, repoRoot);
3878
+ const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
3879
+ const drainTimeout = parseInt(drainStr, 10) || 30;
3880
+ logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
3881
+ log(`Drain timeout: ${drainTimeout}s`);
3882
+ try {
3883
+ const currentBridgeState = loadBridgeState(ctx.stateDir, instanceId);
3884
+ const { manageAppServer, noAuth } = inferRestartMode(
3885
+ currentBridgeState,
3886
+ {
3887
+ noServer: flags["no-server"] === true ? true : void 0,
3888
+ noAuth: flags["no-auth"] === true ? true : void 0
3889
+ },
3890
+ {
3891
+ manageAppServer: inst.manageAppServer,
3892
+ noAuth: inst.noAuth
3893
+ }
3894
+ );
3895
+ const bridge = await restartBridge({
3896
+ instanceId,
3897
+ runtime: inst.runtime,
3898
+ stateDir: ctx.stateDir,
3899
+ commsDir: ctx.commsDir,
3900
+ bridgeScript,
3901
+ platform: ctx.platform,
3902
+ agentName: inst.agentName ?? void 0,
3903
+ runtimeCommand: resolvedConfig.runtimeCommand,
3904
+ appServerUrl: resolvedConfig.appServerUrl,
3905
+ repoRoot,
3906
+ port: inst.port ?? void 0,
3907
+ headless: inst.headless,
3908
+ drainTimeoutSeconds: drainTimeout,
3909
+ manageAppServer,
3910
+ noAuth
3911
+ });
3912
+ logSuccess(`Bridge restarted (PID: ${bridge.pid})`);
3913
+ const updated = { ...inst, bridge, manageAppServer, noAuth };
3914
+ const newState = updateInstanceState(state, instanceId, updated);
3915
+ saveState(repoRoot, newState);
3916
+ return {
3917
+ ok: true,
3918
+ command: "bridge",
3919
+ instanceId,
3920
+ code: "TAP_BRIDGE_START_OK",
3921
+ message: `Bridge for ${instanceId} restarted (PID: ${bridge.pid})`,
3922
+ warnings: [],
3923
+ data: { pid: bridge.pid }
3924
+ };
3925
+ } catch (err) {
3926
+ const msg = err instanceof Error ? err.message : String(err);
3927
+ logError(msg);
3928
+ return {
3929
+ ok: false,
3930
+ command: "bridge",
3931
+ instanceId,
3932
+ code: "TAP_BRIDGE_START_FAILED",
3933
+ message: msg,
3934
+ warnings: [],
3935
+ data: {}
3936
+ };
3937
+ }
3938
+ }
3724
3939
  async function bridgeCommand(args) {
3725
3940
  const { positional, flags } = parseArgs(args);
3726
3941
  const subcommand = positional[0];
@@ -3780,12 +3995,25 @@ async function bridgeCommand(args) {
3780
3995
  }
3781
3996
  return bridgeStatusAll();
3782
3997
  }
3998
+ case "restart": {
3999
+ if (!identifierArg) {
4000
+ return {
4001
+ ok: false,
4002
+ command: "bridge",
4003
+ code: "TAP_INVALID_ARGUMENT",
4004
+ message: "Missing instance. Usage: npx @hua-labs/tap bridge restart <instance>",
4005
+ warnings: [],
4006
+ data: {}
4007
+ };
4008
+ }
4009
+ return bridgeRestart(identifierArg, flags);
4010
+ }
3783
4011
  default:
3784
4012
  return {
3785
4013
  ok: false,
3786
4014
  command: "bridge",
3787
4015
  code: "TAP_INVALID_ARGUMENT",
3788
- message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, status`,
4016
+ message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status`,
3789
4017
  warnings: [],
3790
4018
  data: {}
3791
4019
  };
@@ -4003,6 +4231,9 @@ init_dashboard();
4003
4231
  init_dashboard();
4004
4232
  init_utils();
4005
4233
  init_config();
4234
+ init_state();
4235
+ import * as fs13 from "fs";
4236
+ import * as path14 from "path";
4006
4237
  function getDashboardSnapshot(options) {
4007
4238
  const repoRoot = options?.repoRoot ?? findRepoRoot();
4008
4239
  return collectDashboardSnapshot(repoRoot, options?.commsDir);
@@ -4049,6 +4280,60 @@ async function stopAgents() {
4049
4280
  commandResult: result
4050
4281
  };
4051
4282
  }
4283
+ function getHealthReport(options) {
4284
+ const repoRoot = options?.repoRoot ?? findRepoRoot();
4285
+ const snapshot = collectDashboardSnapshot(repoRoot, options?.commsDir);
4286
+ const headlessStates = [];
4287
+ try {
4288
+ const state = loadState(repoRoot);
4289
+ const activeMatchers = /* @__PURE__ */ new Set();
4290
+ if (state) {
4291
+ for (const [id, inst] of Object.entries(state.instances)) {
4292
+ if (inst?.installed && inst.bridgeMode === "app-server") {
4293
+ activeMatchers.add(id);
4294
+ if (inst.agentName) activeMatchers.add(inst.agentName);
4295
+ }
4296
+ }
4297
+ }
4298
+ const tmpDir = path14.join(repoRoot, ".tmp");
4299
+ if (fs13.existsSync(tmpDir)) {
4300
+ for (const dir of fs13.readdirSync(tmpDir)) {
4301
+ if (!dir.startsWith("codex-app-server-bridge")) continue;
4302
+ const suffix = dir.replace("codex-app-server-bridge-", "");
4303
+ if (activeMatchers.size > 0) {
4304
+ let matched = false;
4305
+ for (const matcher of activeMatchers) {
4306
+ if (suffix === matcher || suffix.startsWith(matcher)) {
4307
+ matched = true;
4308
+ break;
4309
+ }
4310
+ }
4311
+ if (!matched) continue;
4312
+ }
4313
+ const hsPath = path14.join(tmpDir, dir, "headless-state.json");
4314
+ if (!fs13.existsSync(hsPath)) continue;
4315
+ try {
4316
+ const hs = JSON.parse(fs13.readFileSync(hsPath, "utf-8"));
4317
+ headlessStates.push({ instanceDir: dir, ...hs });
4318
+ } catch {
4319
+ }
4320
+ }
4321
+ }
4322
+ } catch {
4323
+ }
4324
+ const hasFailures = snapshot.warnings.some((w) => w.level === "error");
4325
+ const hasBridgeDown = snapshot.bridges.some(
4326
+ (b) => b.status === "stale" || b.status === "stopped"
4327
+ );
4328
+ return {
4329
+ ok: !hasFailures && !hasBridgeDown,
4330
+ timestamp: snapshot.generatedAt,
4331
+ bridges: snapshot.bridges,
4332
+ agents: snapshot.agents,
4333
+ warnings: snapshot.warnings,
4334
+ headless: headlessStates
4335
+ };
4336
+ }
4052
4337
  function getConfig(options) {
4053
4338
  const repoRoot = options?.repoRoot ?? findRepoRoot();
4054
4339
  const { config } = resolveConfig({}, repoRoot);
@@ -4104,8 +4389,9 @@ async function handleEvents(req, res, apiOptions) {
4104
4389
  }
4105
4390
  res.end();
4106
4391
  }
4107
- function handleHealth(res) {
4108
- jsonResponse(res, { ok: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
4392
+ function handleHealth(res, apiOptions) {
4393
+ const report = getHealthReport(apiOptions);
4394
+ jsonResponse(res, report);
4109
4395
  }
4110
4396
  async function startHttpServer(options) {
4111
4397
  const port = options?.port ?? 4580;
@@ -4136,7 +4422,7 @@ async function startHttpServer(options) {
4136
4422
  handleConfig(res, apiOptions);
4137
4423
  return;
4138
4424
  case "/health":
4139
- handleHealth(res);
4425
+ handleHealth(res, apiOptions);
4140
4426
  return;
4141
4427
  }
4142
4428
  }
@@ -4192,6 +4478,7 @@ export {
4192
4478
  getConfig,
4193
4479
  getDashboardSnapshot,
4194
4480
  getFnmBinDir,
4481
+ getHealthReport,
4195
4482
  getHeartbeatAge,
4196
4483
  loadLocalConfig,
4197
4484
  loadSharedConfig,
@@ -4200,6 +4487,7 @@ export {
4200
4487
  readNodeVersion,
4201
4488
  resolveConfig,
4202
4489
  resolveNodeRuntime,
4490
+ restartBridge,
4203
4491
  rotateLog,
4204
4492
  saveLocalConfig,
4205
4493
  saveSharedConfig,