@hua-labs/tap 0.2.1 → 0.2.3

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;
@@ -541,9 +556,25 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
541
556
  return { command: null, args: [], env, sourcePath, warnings, issues };
542
557
  }
543
558
  const isBundled = sourcePath.endsWith(".mjs");
559
+ const isEphemeralSource = isEphemeralPath(sourcePath);
544
560
  let command = bunCommand;
545
- if (!command && isBundled) {
546
- command = process.execPath;
561
+ let args = [toForwardSlashPath(sourcePath)];
562
+ if (isEphemeralSource && isBundled) {
563
+ command = "npx";
564
+ args = ["@hua-labs/tap", "serve"];
565
+ warnings.push(
566
+ "Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
567
+ );
568
+ } else if (!command && isBundled) {
569
+ const isEphemeralNode = isEphemeralPath(process.execPath);
570
+ 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
+ }
547
578
  warnings.push(
548
579
  "bun not found; using node to run the compiled MCP server. Install bun for better performance."
549
580
  );
@@ -555,8 +586,8 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
555
586
  return { command: null, args: [], env, sourcePath, warnings, issues };
556
587
  }
557
588
  return {
558
- command: isBundled && command === process.execPath ? toForwardSlashPath(command) : command,
559
- args: [toForwardSlashPath(sourcePath)],
589
+ command,
590
+ args,
560
591
  env,
561
592
  sourcePath,
562
593
  warnings,
@@ -1558,6 +1589,40 @@ function isBridgeRunning(stateDir, instanceId) {
1558
1589
  if (!state) return false;
1559
1590
  return isProcessAlive(state.pid);
1560
1591
  }
1592
+ function resolveAgentName(instanceId, explicit, context) {
1593
+ if (explicit) return explicit;
1594
+ try {
1595
+ const repoRoot = context?.repoRoot ?? context?.stateDir?.replace(/[\\/].tap-comms$/, "") ?? process.cwd();
1596
+ const state = loadState(repoRoot);
1597
+ const stateAgent = state?.instances[instanceId]?.agentName;
1598
+ if (stateAgent) return stateAgent;
1599
+ } catch {
1600
+ }
1601
+ return process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME || null;
1602
+ }
1603
+ function inferRestartMode(bridgeState, flags, savedMode) {
1604
+ const wasManaged = bridgeState?.appServer != null;
1605
+ const hadAuth = bridgeState?.appServer?.auth != null;
1606
+ const manageAppServer = flags?.noServer === true ? false : flags?.noServer === void 0 ? savedMode?.manageAppServer ?? wasManaged : true;
1607
+ const noAuth = flags?.noAuth === true ? true : flags?.noAuth === void 0 ? savedMode?.noAuth ?? !hadAuth : false;
1608
+ return { manageAppServer, noAuth };
1609
+ }
1610
+ function cleanupHeadlessDispatch(inboxDir, agentName) {
1611
+ const removed = [];
1612
+ if (!fs7.existsSync(inboxDir)) return removed;
1613
+ const normalizedAgent = agentName.replace(/-/g, "_");
1614
+ const marker = `-headless-${normalizedAgent}-review-`;
1615
+ try {
1616
+ for (const file of fs7.readdirSync(inboxDir)) {
1617
+ if (file.includes(marker)) {
1618
+ fs7.unlinkSync(path7.join(inboxDir, file));
1619
+ removed.push(file);
1620
+ }
1621
+ }
1622
+ } catch {
1623
+ }
1624
+ return removed;
1625
+ }
1561
1626
  async function startBridge(options) {
1562
1627
  const {
1563
1628
  instanceId,
@@ -1568,7 +1633,10 @@ async function startBridge(options) {
1568
1633
  agentName,
1569
1634
  port
1570
1635
  } = options;
1571
- const resolvedAgent = agentName || process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;
1636
+ const resolvedAgent = resolveAgentName(instanceId, agentName, {
1637
+ repoRoot: options.repoRoot,
1638
+ stateDir
1639
+ });
1572
1640
  if (!resolvedAgent) {
1573
1641
  throw new Error(
1574
1642
  `No agent name for ${instanceId} bridge. Set TAP_AGENT_NAME env var or pass --agent-name flag.`
@@ -1720,6 +1788,35 @@ async function stopBridge(options) {
1720
1788
  clearBridgeState(stateDir, instanceId);
1721
1789
  return true;
1722
1790
  }
1791
+ async function restartBridge(options) {
1792
+ const { instanceId, stateDir, platform } = options;
1793
+ const drainTimeout = (options.drainTimeoutSeconds ?? 30) * 1e3;
1794
+ const repoRoot = options.repoRoot ?? stateDir.replace(/[\\/].tap-comms$/, "");
1795
+ const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
1796
+ const heartbeatPath = path7.join(runtimeStateDir, "heartbeat.json");
1797
+ if (fs7.existsSync(heartbeatPath)) {
1798
+ const startWait = Date.now();
1799
+ while (Date.now() - startWait < drainTimeout) {
1800
+ try {
1801
+ const hb = JSON.parse(fs7.readFileSync(heartbeatPath, "utf-8"));
1802
+ if (!hb.activeTurnId) break;
1803
+ } catch {
1804
+ break;
1805
+ }
1806
+ await new Promise((r) => setTimeout(r, 1e3));
1807
+ }
1808
+ }
1809
+ if (options.headless?.enabled && options.commsDir) {
1810
+ const agentName = options.agentName ?? instanceId;
1811
+ cleanupHeadlessDispatch(path7.join(options.commsDir, "inbox"), agentName);
1812
+ }
1813
+ await stopBridge({ instanceId, stateDir, platform });
1814
+ const restartOptions = {
1815
+ ...options,
1816
+ processExistingMessages: true
1817
+ };
1818
+ return startBridge(restartOptions);
1819
+ }
1723
1820
  function rotateLog(logPath) {
1724
1821
  if (!fs7.existsSync(logPath)) return;
1725
1822
  try {
@@ -1763,6 +1860,7 @@ var init_bridge = __esm({
1763
1860
  "use strict";
1764
1861
  init_common();
1765
1862
  init_runtime();
1863
+ init_state();
1766
1864
  DEFAULT_APP_SERVER_URL2 = "ws://127.0.0.1:4501";
1767
1865
  APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
1768
1866
  APP_SERVER_START_TIMEOUT_MS = 2e4;
@@ -2396,13 +2494,12 @@ function verifyManagedToml(content, ctx, configPath) {
2396
2494
  });
2397
2495
  }
2398
2496
  if (mainTable && managed.command) {
2497
+ const expectedArgs = managed.args.map((a) => `"${a.replace(/\\/g, "\\\\")}"`).join(", ");
2399
2498
  checks.push({
2400
2499
  name: "Managed command configured",
2401
2500
  passed: mainTable.includes(
2402
2501
  `command = "${managed.command.replace(/\\/g, "\\\\")}"`
2403
- ) && mainTable.includes(
2404
- `args = ["${managed.args[0]?.replace(/\\/g, "\\\\") ?? ""}"]`
2405
- ),
2502
+ ) && mainTable.includes(`args = [${expectedArgs}]`),
2406
2503
  message: "Managed tap-comms command/args do not match expected values"
2407
2504
  });
2408
2505
  }
@@ -3230,7 +3327,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3230
3327
  log(`TUI connect: ${bridge.appServer.url}`);
3231
3328
  }
3232
3329
  }
3233
- const updated = { ...instance, bridge };
3330
+ const updated = { ...instance, bridge, manageAppServer, noAuth };
3234
3331
  const newState = updateInstanceState(state, instanceId, updated);
3235
3332
  saveState(repoRoot, newState);
3236
3333
  return {
@@ -3721,6 +3818,121 @@ function bridgeStatusOne(identifier) {
3721
3818
  }
3722
3819
  };
3723
3820
  }
3821
+ async function bridgeRestart(identifier, flags) {
3822
+ const repoRoot = findRepoRoot();
3823
+ const state = loadState(repoRoot);
3824
+ if (!state) {
3825
+ return {
3826
+ ok: false,
3827
+ command: "bridge",
3828
+ code: "TAP_NOT_INITIALIZED",
3829
+ message: "Not initialized. Run: npx @hua-labs/tap init",
3830
+ warnings: [],
3831
+ data: {}
3832
+ };
3833
+ }
3834
+ const resolved = resolveInstanceId(identifier, state);
3835
+ if (!resolved.ok) {
3836
+ return {
3837
+ ok: false,
3838
+ command: "bridge",
3839
+ code: resolved.code,
3840
+ message: resolved.message,
3841
+ warnings: [],
3842
+ data: {}
3843
+ };
3844
+ }
3845
+ const instanceId = resolved.instanceId;
3846
+ const inst = state.instances[instanceId];
3847
+ if (!inst) {
3848
+ return {
3849
+ ok: false,
3850
+ command: "bridge",
3851
+ code: "TAP_INSTANCE_NOT_FOUND",
3852
+ message: `Instance not found: ${instanceId}`,
3853
+ warnings: [],
3854
+ data: {}
3855
+ };
3856
+ }
3857
+ const adapter = getAdapter(inst.runtime);
3858
+ const ctx = {
3859
+ ...createAdapterContext(state.commsDir, repoRoot),
3860
+ instanceId
3861
+ };
3862
+ const bridgeScript = adapter.resolveBridgeScript?.(ctx);
3863
+ if (!bridgeScript) {
3864
+ return {
3865
+ ok: false,
3866
+ command: "bridge",
3867
+ instanceId,
3868
+ code: "TAP_BRIDGE_SCRIPT_MISSING",
3869
+ message: `Bridge script not found for ${instanceId}`,
3870
+ warnings: [],
3871
+ data: {}
3872
+ };
3873
+ }
3874
+ const { config: resolvedConfig } = resolveConfig({}, repoRoot);
3875
+ const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
3876
+ const drainTimeout = parseInt(drainStr, 10) || 30;
3877
+ logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
3878
+ log(`Drain timeout: ${drainTimeout}s`);
3879
+ try {
3880
+ const currentBridgeState = loadBridgeState(ctx.stateDir, instanceId);
3881
+ const { manageAppServer, noAuth } = inferRestartMode(
3882
+ currentBridgeState,
3883
+ {
3884
+ noServer: flags["no-server"] === true ? true : void 0,
3885
+ noAuth: flags["no-auth"] === true ? true : void 0
3886
+ },
3887
+ {
3888
+ manageAppServer: inst.manageAppServer,
3889
+ noAuth: inst.noAuth
3890
+ }
3891
+ );
3892
+ const bridge = await restartBridge({
3893
+ instanceId,
3894
+ runtime: inst.runtime,
3895
+ stateDir: ctx.stateDir,
3896
+ commsDir: ctx.commsDir,
3897
+ bridgeScript,
3898
+ platform: ctx.platform,
3899
+ agentName: inst.agentName ?? void 0,
3900
+ runtimeCommand: resolvedConfig.runtimeCommand,
3901
+ appServerUrl: resolvedConfig.appServerUrl,
3902
+ repoRoot,
3903
+ port: inst.port ?? void 0,
3904
+ headless: inst.headless,
3905
+ drainTimeoutSeconds: drainTimeout,
3906
+ manageAppServer,
3907
+ noAuth
3908
+ });
3909
+ logSuccess(`Bridge restarted (PID: ${bridge.pid})`);
3910
+ const updated = { ...inst, bridge, manageAppServer, noAuth };
3911
+ const newState = updateInstanceState(state, instanceId, updated);
3912
+ saveState(repoRoot, newState);
3913
+ return {
3914
+ ok: true,
3915
+ command: "bridge",
3916
+ instanceId,
3917
+ code: "TAP_BRIDGE_START_OK",
3918
+ message: `Bridge for ${instanceId} restarted (PID: ${bridge.pid})`,
3919
+ warnings: [],
3920
+ data: { pid: bridge.pid }
3921
+ };
3922
+ } catch (err) {
3923
+ const msg = err instanceof Error ? err.message : String(err);
3924
+ logError(msg);
3925
+ return {
3926
+ ok: false,
3927
+ command: "bridge",
3928
+ instanceId,
3929
+ code: "TAP_BRIDGE_START_FAILED",
3930
+ message: msg,
3931
+ warnings: [],
3932
+ data: {}
3933
+ };
3934
+ }
3935
+ }
3724
3936
  async function bridgeCommand(args) {
3725
3937
  const { positional, flags } = parseArgs(args);
3726
3938
  const subcommand = positional[0];
@@ -3780,12 +3992,25 @@ async function bridgeCommand(args) {
3780
3992
  }
3781
3993
  return bridgeStatusAll();
3782
3994
  }
3995
+ case "restart": {
3996
+ if (!identifierArg) {
3997
+ return {
3998
+ ok: false,
3999
+ command: "bridge",
4000
+ code: "TAP_INVALID_ARGUMENT",
4001
+ message: "Missing instance. Usage: npx @hua-labs/tap bridge restart <instance>",
4002
+ warnings: [],
4003
+ data: {}
4004
+ };
4005
+ }
4006
+ return bridgeRestart(identifierArg, flags);
4007
+ }
3783
4008
  default:
3784
4009
  return {
3785
4010
  ok: false,
3786
4011
  command: "bridge",
3787
4012
  code: "TAP_INVALID_ARGUMENT",
3788
- message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, status`,
4013
+ message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, restart, status`,
3789
4014
  warnings: [],
3790
4015
  data: {}
3791
4016
  };
@@ -4003,6 +4228,9 @@ init_dashboard();
4003
4228
  init_dashboard();
4004
4229
  init_utils();
4005
4230
  init_config();
4231
+ init_state();
4232
+ import * as fs13 from "fs";
4233
+ import * as path14 from "path";
4006
4234
  function getDashboardSnapshot(options) {
4007
4235
  const repoRoot = options?.repoRoot ?? findRepoRoot();
4008
4236
  return collectDashboardSnapshot(repoRoot, options?.commsDir);
@@ -4049,6 +4277,60 @@ async function stopAgents() {
4049
4277
  commandResult: result
4050
4278
  };
4051
4279
  }
4280
+ function getHealthReport(options) {
4281
+ const repoRoot = options?.repoRoot ?? findRepoRoot();
4282
+ const snapshot = collectDashboardSnapshot(repoRoot, options?.commsDir);
4283
+ const headlessStates = [];
4284
+ try {
4285
+ const state = loadState(repoRoot);
4286
+ const activeMatchers = /* @__PURE__ */ new Set();
4287
+ if (state) {
4288
+ for (const [id, inst] of Object.entries(state.instances)) {
4289
+ if (inst?.installed && inst.bridgeMode === "app-server") {
4290
+ activeMatchers.add(id);
4291
+ if (inst.agentName) activeMatchers.add(inst.agentName);
4292
+ }
4293
+ }
4294
+ }
4295
+ const tmpDir = path14.join(repoRoot, ".tmp");
4296
+ if (fs13.existsSync(tmpDir)) {
4297
+ for (const dir of fs13.readdirSync(tmpDir)) {
4298
+ if (!dir.startsWith("codex-app-server-bridge")) continue;
4299
+ const suffix = dir.replace("codex-app-server-bridge-", "");
4300
+ if (activeMatchers.size > 0) {
4301
+ let matched = false;
4302
+ for (const matcher of activeMatchers) {
4303
+ if (suffix === matcher || suffix.startsWith(matcher)) {
4304
+ matched = true;
4305
+ break;
4306
+ }
4307
+ }
4308
+ if (!matched) continue;
4309
+ }
4310
+ const hsPath = path14.join(tmpDir, dir, "headless-state.json");
4311
+ if (!fs13.existsSync(hsPath)) continue;
4312
+ try {
4313
+ const hs = JSON.parse(fs13.readFileSync(hsPath, "utf-8"));
4314
+ headlessStates.push({ instanceDir: dir, ...hs });
4315
+ } catch {
4316
+ }
4317
+ }
4318
+ }
4319
+ } catch {
4320
+ }
4321
+ const hasFailures = snapshot.warnings.some((w) => w.level === "error");
4322
+ const hasBridgeDown = snapshot.bridges.some(
4323
+ (b) => b.status === "stale" || b.status === "stopped"
4324
+ );
4325
+ return {
4326
+ ok: !hasFailures && !hasBridgeDown,
4327
+ timestamp: snapshot.generatedAt,
4328
+ bridges: snapshot.bridges,
4329
+ agents: snapshot.agents,
4330
+ warnings: snapshot.warnings,
4331
+ headless: headlessStates
4332
+ };
4333
+ }
4052
4334
  function getConfig(options) {
4053
4335
  const repoRoot = options?.repoRoot ?? findRepoRoot();
4054
4336
  const { config } = resolveConfig({}, repoRoot);
@@ -4104,8 +4386,9 @@ async function handleEvents(req, res, apiOptions) {
4104
4386
  }
4105
4387
  res.end();
4106
4388
  }
4107
- function handleHealth(res) {
4108
- jsonResponse(res, { ok: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
4389
+ function handleHealth(res, apiOptions) {
4390
+ const report = getHealthReport(apiOptions);
4391
+ jsonResponse(res, report);
4109
4392
  }
4110
4393
  async function startHttpServer(options) {
4111
4394
  const port = options?.port ?? 4580;
@@ -4136,7 +4419,7 @@ async function startHttpServer(options) {
4136
4419
  handleConfig(res, apiOptions);
4137
4420
  return;
4138
4421
  case "/health":
4139
- handleHealth(res);
4422
+ handleHealth(res, apiOptions);
4140
4423
  return;
4141
4424
  }
4142
4425
  }
@@ -4192,6 +4475,7 @@ export {
4192
4475
  getConfig,
4193
4476
  getDashboardSnapshot,
4194
4477
  getFnmBinDir,
4478
+ getHealthReport,
4195
4479
  getHeartbeatAge,
4196
4480
  loadLocalConfig,
4197
4481
  loadSharedConfig,
@@ -4200,6 +4484,7 @@ export {
4200
4484
  readNodeVersion,
4201
4485
  resolveConfig,
4202
4486
  resolveNodeRuntime,
4487
+ restartBridge,
4203
4488
  rotateLog,
4204
4489
  saveLocalConfig,
4205
4490
  saveSharedConfig,