@hua-labs/tap 0.2.3 → 0.2.5

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.mjs CHANGED
@@ -27,8 +27,8 @@ function findRepoRoot(startDir = process.cwd()) {
27
27
  if (fs.existsSync(path.join(dir, "package.json"))) {
28
28
  if (!_noGitWarned) {
29
29
  _setNoGitWarned();
30
- logWarn(
31
- "No .git directory found. Resolved repo root via package.json \u2014 comms directory may be created in an unexpected location. Use --comms-dir to specify explicitly."
30
+ log(
31
+ "No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
32
32
  );
33
33
  }
34
34
  return dir;
@@ -39,8 +39,8 @@ function findRepoRoot(startDir = process.cwd()) {
39
39
  }
40
40
  if (!_noGitWarned) {
41
41
  _setNoGitWarned();
42
- logWarn(
43
- "No git repository or package.json found. Using current directory as root. Run 'git init' first, or use --comms-dir to specify the comms path."
42
+ log(
43
+ "No git repository or package.json found. Using the current directory as tap root. That's fine outside git; use --comms-dir to choose a different comms location."
44
44
  );
45
45
  }
46
46
  return process.cwd();
@@ -82,9 +82,6 @@ function log(message) {
82
82
  function logSuccess(message) {
83
83
  if (!_jsonMode) console.log(` + ${message}`);
84
84
  }
85
- function logWarn(message) {
86
- if (!_jsonMode) console.log(` ! ${message}`);
87
- }
88
85
  function logError(message) {
89
86
  if (!_jsonMode) console.error(` x ${message}`);
90
87
  }
@@ -93,6 +90,16 @@ function logHeader(message) {
93
90
  ${message}
94
91
  `);
95
92
  }
93
+ function parseIntFlag(value, name, min, max) {
94
+ if (value === void 0) return void 0;
95
+ const parsed = Number(value);
96
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
97
+ throw new RangeError(
98
+ `Invalid ${name}: ${value}. Must be an integer between ${min} and ${max}.`
99
+ );
100
+ }
101
+ return parsed;
102
+ }
96
103
  function resolveInstanceId(identifier, state) {
97
104
  if (state.instances[identifier]) {
98
105
  return { ok: true, instanceId: identifier };
@@ -140,8 +147,8 @@ function findRepoRoot2(startDir = process.cwd()) {
140
147
  if (fs2.existsSync(path2.join(dir, "package.json"))) {
141
148
  if (!_noGitWarned) {
142
149
  _setNoGitWarned();
143
- console.error(
144
- "[tap] warning: No .git directory found. Resolved via package.json. Use --comms-dir to specify explicitly."
150
+ log(
151
+ "No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
145
152
  );
146
153
  }
147
154
  return dir;
@@ -152,8 +159,8 @@ function findRepoRoot2(startDir = process.cwd()) {
152
159
  }
153
160
  if (!_noGitWarned) {
154
161
  _setNoGitWarned();
155
- console.error(
156
- "[tap] warning: No git repository found. Using cwd as root. Run 'git init' or use --comms-dir."
162
+ log(
163
+ "No git repository or package.json found. Using the current directory as tap root. That's fine outside git; use --comms-dir to choose a different comms location."
157
164
  );
158
165
  }
159
166
  return process.cwd();
@@ -551,7 +558,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
551
558
  }
552
559
  if (!sourcePath) {
553
560
  issues.push(
554
- "tap-comms MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
561
+ "tap MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
555
562
  );
556
563
  return { command: null, args: [], env, sourcePath, warnings, issues };
557
564
  }
@@ -581,7 +588,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
581
588
  }
582
589
  if (!command) {
583
590
  issues.push(
584
- "bun is required to run the repo-local tap-comms MCP server (.ts source). Install bun: https://bun.sh"
591
+ "bun is required to run the repo-local tap MCP server (.ts source). Install bun: https://bun.sh"
585
592
  );
586
593
  return { command: null, args: [], env, sourcePath, warnings, issues };
587
594
  }
@@ -775,6 +782,7 @@ var init_runtime = __esm({
775
782
  // src/engine/bridge.ts
776
783
  import * as fs7 from "fs";
777
784
  import * as net from "net";
785
+ import * as os2 from "os";
778
786
  import * as path7 from "path";
779
787
  import { randomBytes } from "crypto";
780
788
  import { spawn, spawnSync as spawnSync2, execSync as execSync2 } from "child_process";
@@ -815,12 +823,66 @@ function removeFileIfExists(filePath) {
815
823
  } catch {
816
824
  }
817
825
  }
826
+ function toPowerShellSingleQuotedString(value) {
827
+ return `'${value.replace(/'/g, "''")}'`;
828
+ }
829
+ function toPowerShellStringArrayLiteral(values) {
830
+ return `@(${values.map(toPowerShellSingleQuotedString).join(", ")})`;
831
+ }
832
+ function cleanupStaleWindowsSpawnWrappers(now = Date.now()) {
833
+ let entries;
834
+ try {
835
+ entries = fs7.readdirSync(os2.tmpdir());
836
+ } catch {
837
+ return;
838
+ }
839
+ for (const entry of entries) {
840
+ if (!entry.startsWith(WINDOWS_SPAWN_WRAPPER_PREFIX) || !/\.(cmd|ps1)$/i.test(entry)) {
841
+ continue;
842
+ }
843
+ const wrapperPath = path7.join(os2.tmpdir(), entry);
844
+ try {
845
+ const stats = fs7.statSync(wrapperPath);
846
+ if (now - stats.mtimeMs < WINDOWS_SPAWN_WRAPPER_STALE_MS) {
847
+ continue;
848
+ }
849
+ fs7.unlinkSync(wrapperPath);
850
+ } catch {
851
+ }
852
+ }
853
+ }
854
+ function buildWindowsDetachedWrapperScript(command, args, logPath, stderrLogPath, env) {
855
+ const lines = ["$ErrorActionPreference = 'Stop'"];
856
+ for (const [key, value] of Object.entries(env)) {
857
+ if (value !== void 0 && value !== process.env[key]) {
858
+ lines.push(
859
+ `[Environment]::SetEnvironmentVariable(${toPowerShellSingleQuotedString(key)}, ${toPowerShellSingleQuotedString(value)}, 'Process')`
860
+ );
861
+ }
862
+ }
863
+ lines.push(
864
+ `$logPath = ${toPowerShellSingleQuotedString(logPath)}`,
865
+ `$stderrLogPath = ${toPowerShellSingleQuotedString(stderrLogPath)}`,
866
+ `$commandPath = ${toPowerShellSingleQuotedString(command)}`,
867
+ `$commandArgs = ${toPowerShellStringArrayLiteral(args)}`,
868
+ "$exitCode = 1",
869
+ "try {",
870
+ " & $commandPath @commandArgs >> $logPath 2>> $stderrLogPath",
871
+ " $exitCode = if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } else { 0 }",
872
+ "} finally {",
873
+ " Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue",
874
+ "}",
875
+ "exit $exitCode"
876
+ );
877
+ return `${lines.join("\r\n")}\r
878
+ `;
879
+ }
818
880
  function getWebSocketCtor() {
819
881
  const candidate = globalThis.WebSocket;
820
882
  return typeof candidate === "function" ? candidate : null;
821
883
  }
822
884
  function delay(ms) {
823
- return new Promise((resolve8) => setTimeout(resolve8, ms));
885
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
824
886
  }
825
887
  function isLoopbackHost(hostname) {
826
888
  return hostname === "127.0.0.1" || hostname === "localhost";
@@ -838,8 +900,11 @@ function resolvePowerShellCommand() {
838
900
  function resolveAuthGatewayScript(repoRoot) {
839
901
  const moduleDir = path7.dirname(fileURLToPath3(import.meta.url));
840
902
  const candidates = [
841
- path7.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.mjs"),
842
- path7.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.ts"),
903
+ // Bundled: dist/bridges/ sibling (npm install / built package)
904
+ path7.join(moduleDir, "bridges", "codex-app-server-auth-gateway.mjs"),
905
+ // Source: src/bridges/ sibling (monorepo dev with ts runner)
906
+ path7.join(moduleDir, "bridges", "codex-app-server-auth-gateway.ts"),
907
+ // Monorepo dist fallback
843
908
  path7.join(
844
909
  repoRoot,
845
910
  "packages",
@@ -869,7 +934,7 @@ function getBridgeRuntimeStateDir(repoRoot, instanceId) {
869
934
  }
870
935
  async function allocateLoopbackPort(hostname) {
871
936
  const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
872
- return await new Promise((resolve8, reject) => {
937
+ return await new Promise((resolve9, reject) => {
873
938
  const server = net.createServer();
874
939
  server.unref();
875
940
  server.once("error", reject);
@@ -887,15 +952,13 @@ async function allocateLoopbackPort(hostname) {
887
952
  reject(error);
888
953
  return;
889
954
  }
890
- resolve8(port);
955
+ resolve9(port);
891
956
  });
892
957
  });
893
958
  });
894
959
  }
895
- function buildProtectedAppServerUrl(publicUrl, token) {
896
- const url = new URL(publicUrl);
897
- url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
898
- return url.toString().replace(/\/(?=\?|$)/, "");
960
+ function buildProtectedAppServerUrl(publicUrl, _token) {
961
+ return publicUrl;
899
962
  }
900
963
  function readGatewayTokenFromPath(tokenPath) {
901
964
  return fs7.readFileSync(tokenPath, "utf8").trim();
@@ -1010,7 +1073,7 @@ async function createManagedAppServerAuth(options) {
1010
1073
  throw new Error("Failed to spawn app-server auth gateway");
1011
1074
  }
1012
1075
  return {
1013
- mode: "query-token",
1076
+ mode: "subprotocol",
1014
1077
  protectedUrl,
1015
1078
  upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
1016
1079
  tokenPath,
@@ -1072,35 +1135,50 @@ function findReusableManagedAppServer(stateDir, publicUrl) {
1072
1135
  return null;
1073
1136
  }
1074
1137
  function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
1075
- const ext = path7.extname(command).toLowerCase();
1076
1138
  const stderrLogPath = stderrLogFilePath(logPath);
1077
- const stdoutFd = fs7.openSync(logPath, "a");
1078
- const stderrFd = fs7.openSync(stderrLogPath, "a");
1079
- try {
1080
- const child = ext === ".ps1" ? spawn(
1081
- resolvePowerShellCommand(),
1082
- ["-NoLogo", "-NoProfile", "-File", command, ...args],
1083
- {
1084
- cwd: repoRoot,
1085
- detached: true,
1086
- stdio: ["ignore", stdoutFd, stderrFd],
1087
- env,
1088
- windowsHide: true
1089
- }
1090
- ) : spawn(command, args, {
1091
- cwd: repoRoot,
1092
- detached: true,
1093
- stdio: ["ignore", stdoutFd, stderrFd],
1094
- env,
1095
- windowsHide: true,
1096
- shell: ext === ".cmd" || ext === ".bat"
1097
- });
1098
- child.unref();
1099
- return child.pid ?? null;
1100
- } finally {
1101
- fs7.closeSync(stdoutFd);
1102
- fs7.closeSync(stderrFd);
1139
+ const powerShellCommand = resolvePowerShellCommand();
1140
+ cleanupStaleWindowsSpawnWrappers();
1141
+ const wrapperPath = path7.join(
1142
+ os2.tmpdir(),
1143
+ `${WINDOWS_SPAWN_WRAPPER_PREFIX}${randomBytes(4).toString("hex")}.ps1`
1144
+ );
1145
+ fs7.writeFileSync(
1146
+ wrapperPath,
1147
+ buildWindowsDetachedWrapperScript(
1148
+ command,
1149
+ args,
1150
+ logPath,
1151
+ stderrLogPath,
1152
+ env
1153
+ )
1154
+ );
1155
+ const psCommand = [
1156
+ "$p = Start-Process",
1157
+ `-FilePath ${toPowerShellSingleQuotedString(powerShellCommand)}`,
1158
+ `-ArgumentList ${toPowerShellStringArrayLiteral(["-NoLogo", "-NoProfile", "-File", wrapperPath])}`,
1159
+ `-WorkingDirectory ${toPowerShellSingleQuotedString(repoRoot)}`,
1160
+ "-WindowStyle Hidden",
1161
+ "-PassThru",
1162
+ "; Write-Output $p.Id"
1163
+ ].join(" ");
1164
+ const result = spawnSync2(
1165
+ powerShellCommand,
1166
+ ["-NoLogo", "-NoProfile", "-Command", psCommand],
1167
+ {
1168
+ encoding: "utf-8",
1169
+ windowsHide: true
1170
+ }
1171
+ );
1172
+ if (result.status !== 0) {
1173
+ removeFileIfExists(wrapperPath);
1174
+ return null;
1175
+ }
1176
+ const pid = parseInt(result.stdout.trim(), 10);
1177
+ if (!Number.isFinite(pid)) {
1178
+ removeFileIfExists(wrapperPath);
1179
+ return null;
1103
1180
  }
1181
+ return pid;
1104
1182
  }
1105
1183
  function startWindowsCodexAppServer(command, url, repoRoot, logPath) {
1106
1184
  return startWindowsDetachedProcess(
@@ -1162,12 +1240,12 @@ function resolveAppServerUrl(baseUrl, port) {
1162
1240
  }
1163
1241
  async function isTcpPortAvailable(hostname, port) {
1164
1242
  const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
1165
- return await new Promise((resolve8) => {
1243
+ return await new Promise((resolve9) => {
1166
1244
  const server = net.createServer();
1167
1245
  server.unref();
1168
- server.once("error", () => resolve8(false));
1246
+ server.once("error", () => resolve9(false));
1169
1247
  server.listen(port, bindHost, () => {
1170
- server.close((error) => resolve8(!error));
1248
+ server.close((error) => resolve9(!error));
1171
1249
  });
1172
1250
  });
1173
1251
  }
@@ -1197,12 +1275,12 @@ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, e
1197
1275
  `Failed to find a free app-server port starting at ${basePort}`
1198
1276
  );
1199
1277
  }
1200
- async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
1278
+ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
1201
1279
  const WebSocket = getWebSocketCtor();
1202
1280
  if (!WebSocket) {
1203
1281
  return false;
1204
1282
  }
1205
- return new Promise((resolve8) => {
1283
+ return new Promise((resolve9) => {
1206
1284
  let settled = false;
1207
1285
  let socket = null;
1208
1286
  const finish = (healthy) => {
@@ -1215,11 +1293,12 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
1215
1293
  socket?.close();
1216
1294
  } catch {
1217
1295
  }
1218
- resolve8(healthy);
1296
+ resolve9(healthy);
1219
1297
  };
1220
1298
  const timer = setTimeout(() => finish(false), timeoutMs);
1221
1299
  try {
1222
- socket = new WebSocket(url);
1300
+ const protocols = gatewayToken ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`] : void 0;
1301
+ socket = new WebSocket(url, protocols);
1223
1302
  socket.addEventListener("open", () => finish(true), { once: true });
1224
1303
  socket.addEventListener("error", () => finish(false), { once: true });
1225
1304
  socket.addEventListener("close", () => finish(false), { once: true });
@@ -1228,10 +1307,14 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
1228
1307
  }
1229
1308
  });
1230
1309
  }
1231
- async function waitForAppServerHealth(url, timeoutMs) {
1310
+ async function waitForAppServerHealth(url, timeoutMs, gatewayToken) {
1232
1311
  const deadline = Date.now() + timeoutMs;
1233
1312
  while (Date.now() < deadline) {
1234
- if (await checkAppServerHealth(url)) {
1313
+ if (await checkAppServerHealth(
1314
+ url,
1315
+ APP_SERVER_HEALTH_TIMEOUT_MS,
1316
+ gatewayToken
1317
+ )) {
1235
1318
  return true;
1236
1319
  }
1237
1320
  await delay(APP_SERVER_HEALTH_RETRY_MS);
@@ -1496,8 +1579,9 @@ Or start it manually:
1496
1579
  throw new Error("Tap auth gateway token is missing after startup.");
1497
1580
  }
1498
1581
  const gatewayHealthy = await waitForAppServerHealth(
1499
- buildProtectedAppServerUrl(effectiveUrl, gatewayToken),
1500
- APP_SERVER_GATEWAY_START_TIMEOUT_MS
1582
+ effectiveUrl,
1583
+ APP_SERVER_GATEWAY_START_TIMEOUT_MS,
1584
+ gatewayToken
1501
1585
  );
1502
1586
  if (!gatewayHealthy) {
1503
1587
  await terminateProcess(pid, options.platform);
@@ -1533,7 +1617,11 @@ function logFilePath(stateDir, instanceId) {
1533
1617
  function runtimeHeartbeatFilePath(runtimeStateDir) {
1534
1618
  return path7.join(runtimeStateDir, "heartbeat.json");
1535
1619
  }
1536
- function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
1620
+ function runtimeThreadStateFilePath(runtimeStateDir) {
1621
+ return path7.join(runtimeStateDir, "thread.json");
1622
+ }
1623
+ function loadRuntimeBridgeHeartbeat(bridgeState) {
1624
+ const runtimeStateDir = bridgeState?.runtimeStateDir;
1537
1625
  if (!runtimeStateDir) {
1538
1626
  return null;
1539
1627
  }
@@ -1542,13 +1630,35 @@ function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
1542
1630
  return null;
1543
1631
  }
1544
1632
  try {
1545
- const raw = fs7.readFileSync(heartbeatPath, "utf-8");
1546
- const parsed = JSON.parse(raw);
1547
- return typeof parsed.updatedAt === "string" ? parsed.updatedAt : null;
1633
+ return JSON.parse(
1634
+ fs7.readFileSync(heartbeatPath, "utf-8")
1635
+ );
1548
1636
  } catch {
1549
1637
  return null;
1550
1638
  }
1551
1639
  }
1640
+ function loadRuntimeBridgeThreadState(bridgeState) {
1641
+ const runtimeStateDir = bridgeState?.runtimeStateDir;
1642
+ if (!runtimeStateDir) {
1643
+ return null;
1644
+ }
1645
+ const threadPath = runtimeThreadStateFilePath(runtimeStateDir);
1646
+ if (!fs7.existsSync(threadPath)) {
1647
+ return null;
1648
+ }
1649
+ try {
1650
+ const parsed = JSON.parse(
1651
+ fs7.readFileSync(threadPath, "utf-8")
1652
+ );
1653
+ return parsed.threadId ? parsed : null;
1654
+ } catch {
1655
+ return null;
1656
+ }
1657
+ }
1658
+ function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
1659
+ const heartbeat = loadRuntimeBridgeHeartbeat({ runtimeStateDir });
1660
+ return typeof heartbeat?.updatedAt === "string" ? heartbeat.updatedAt : null;
1661
+ }
1552
1662
  function resolveHeartbeatTimestamp(state) {
1553
1663
  return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
1554
1664
  }
@@ -1718,6 +1828,7 @@ async function startBridge(options) {
1718
1828
  options.messageLookbackMinutes
1719
1829
  )
1720
1830
  } : {},
1831
+ ...process.env.TAP_COLD_START_WARMUP === "true" ? { TAP_COLD_START_WARMUP: "true" } : {},
1721
1832
  ...options.threadId ? { TAP_THREAD_ID: options.threadId } : {},
1722
1833
  ...options.ephemeral ? { TAP_EPHEMERAL: "true" } : {},
1723
1834
  ...options.processExistingMessages ? { TAP_PROCESS_EXISTING: "true" } : {}
@@ -1854,7 +1965,7 @@ function getBridgeStatus(stateDir, instanceId) {
1854
1965
  }
1855
1966
  return "running";
1856
1967
  }
1857
- var DEFAULT_APP_SERVER_URL2, APP_SERVER_HEALTH_TIMEOUT_MS, APP_SERVER_START_TIMEOUT_MS, APP_SERVER_GATEWAY_START_TIMEOUT_MS, APP_SERVER_HEALTH_RETRY_MS, APP_SERVER_AUTH_QUERY_PARAM, APP_SERVER_AUTH_FILE_MODE;
1968
+ var DEFAULT_APP_SERVER_URL2, APP_SERVER_HEALTH_TIMEOUT_MS, APP_SERVER_START_TIMEOUT_MS, APP_SERVER_GATEWAY_START_TIMEOUT_MS, APP_SERVER_HEALTH_RETRY_MS, AUTH_SUBPROTOCOL_PREFIX, APP_SERVER_AUTH_FILE_MODE, WINDOWS_SPAWN_WRAPPER_PREFIX, WINDOWS_SPAWN_WRAPPER_STALE_MS;
1858
1969
  var init_bridge = __esm({
1859
1970
  "src/engine/bridge.ts"() {
1860
1971
  "use strict";
@@ -1866,8 +1977,10 @@ var init_bridge = __esm({
1866
1977
  APP_SERVER_START_TIMEOUT_MS = 2e4;
1867
1978
  APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
1868
1979
  APP_SERVER_HEALTH_RETRY_MS = 250;
1869
- APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
1980
+ AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
1870
1981
  APP_SERVER_AUTH_FILE_MODE = 384;
1982
+ WINDOWS_SPAWN_WRAPPER_PREFIX = "tap-spawn-";
1983
+ WINDOWS_SPAWN_WRAPPER_STALE_MS = 60 * 60 * 1e3;
1871
1984
  }
1872
1985
  });
1873
1986
 
@@ -2068,13 +2181,14 @@ function setNestedKey(obj, keyPath, value) {
2068
2181
  function normalizeTapCommsDir(value) {
2069
2182
  return typeof value === "string" ? path9.resolve(value).replace(/\\/g, "/") : "";
2070
2183
  }
2071
- var MCP_SERVER_KEY, claudeAdapter;
2184
+ var MCP_SERVER_KEY, OLD_MCP_SERVER_KEY, claudeAdapter;
2072
2185
  var init_claude = __esm({
2073
2186
  "src/adapters/claude.ts"() {
2074
2187
  "use strict";
2075
2188
  init_state();
2076
2189
  init_common();
2077
- MCP_SERVER_KEY = "tap-comms";
2190
+ MCP_SERVER_KEY = "tap";
2191
+ OLD_MCP_SERVER_KEY = "tap-comms";
2078
2192
  claudeAdapter = {
2079
2193
  runtime: "claude",
2080
2194
  async probe(ctx) {
@@ -2131,6 +2245,11 @@ var init_claude = __esm({
2131
2245
  `Existing "${MCP_SERVER_KEY}" entry in .mcp.json will be overwritten.`
2132
2246
  );
2133
2247
  }
2248
+ if (config.mcpServers?.[OLD_MCP_SERVER_KEY]) {
2249
+ conflicts.push(
2250
+ `Legacy "${OLD_MCP_SERVER_KEY}" entry will be migrated to "${MCP_SERVER_KEY}".`
2251
+ );
2252
+ }
2134
2253
  } catch {
2135
2254
  warnings.push(
2136
2255
  ".mcp.json exists but is not valid JSON. Will be overwritten."
@@ -2140,7 +2259,7 @@ var init_claude = __esm({
2140
2259
  const serverEntry = buildMcpServerEntry(ctx);
2141
2260
  if (!serverEntry) {
2142
2261
  warnings.push(
2143
- "tap-comms MCP server entry not found. Skipping .mcp.json patch. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
2262
+ "tap MCP server entry not found. Skipping .mcp.json patch. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
2144
2263
  );
2145
2264
  return {
2146
2265
  runtime: "claude",
@@ -2193,6 +2312,10 @@ var init_claude = __esm({
2193
2312
  );
2194
2313
  }
2195
2314
  }
2315
+ const servers = config.mcpServers;
2316
+ if (servers?.[OLD_MCP_SERVER_KEY]) {
2317
+ delete servers[OLD_MCP_SERVER_KEY];
2318
+ }
2196
2319
  if (op.key) {
2197
2320
  setNestedKey(config, op.key, op.value);
2198
2321
  }
@@ -2241,7 +2364,7 @@ var init_claude = __esm({
2241
2364
  checks.push({ name: "Config is valid JSON", passed: true });
2242
2365
  const entry = config.mcpServers?.[MCP_SERVER_KEY];
2243
2366
  checks.push({
2244
- name: "tap-comms entry present",
2367
+ name: "tap entry present",
2245
2368
  passed: !!entry,
2246
2369
  message: entry ? void 0 : `mcpServers.${MCP_SERVER_KEY} not found`
2247
2370
  });
@@ -2350,6 +2473,14 @@ function extractTomlTable(content, selector) {
2350
2473
  return `${lines.slice(range.start, range.end).join("\n")}
2351
2474
  `;
2352
2475
  }
2476
+ function removeTomlTable(content, selector) {
2477
+ const lines = splitLines(content);
2478
+ const range = findTableRange(lines, selector);
2479
+ if (!range) return content;
2480
+ const next = [...lines.slice(0, range.start), ...lines.slice(range.end)];
2481
+ return `${trimTomlDocument(next.join("\n"))}
2482
+ `;
2483
+ }
2353
2484
  function replaceTomlTable(content, selector, replacement) {
2354
2485
  const lines = splitLines(content);
2355
2486
  const range = findTableRange(lines, selector);
@@ -2475,12 +2606,12 @@ function verifyManagedToml(content, ctx, configPath) {
2475
2606
  message: fs11.existsSync(configPath) ? void 0 : `${configPath} not found`
2476
2607
  });
2477
2608
  checks.push({
2478
- name: "tap-comms MCP table present",
2609
+ name: "tap MCP table present",
2479
2610
  passed: !!mainTable,
2480
2611
  message: mainTable ? void 0 : `${MCP_SELECTOR} not found`
2481
2612
  });
2482
2613
  checks.push({
2483
- name: "tap-comms env table present",
2614
+ name: "tap env table present",
2484
2615
  passed: !!envTable,
2485
2616
  message: envTable ? void 0 : `${ENV_SELECTOR} not found`
2486
2617
  });
@@ -2500,12 +2631,12 @@ function verifyManagedToml(content, ctx, configPath) {
2500
2631
  passed: mainTable.includes(
2501
2632
  `command = "${managed.command.replace(/\\/g, "\\\\")}"`
2502
2633
  ) && mainTable.includes(`args = [${expectedArgs}]`),
2503
- message: "Managed tap-comms command/args do not match expected values"
2634
+ message: "Managed tap command/args do not match expected values"
2504
2635
  });
2505
2636
  }
2506
2637
  return checks;
2507
2638
  }
2508
- var MCP_SELECTOR, ENV_SELECTOR, codexAdapter;
2639
+ var MCP_SELECTOR, ENV_SELECTOR, OLD_MCP_SELECTOR, OLD_ENV_SELECTOR, codexAdapter;
2509
2640
  var init_codex = __esm({
2510
2641
  "src/adapters/codex.ts"() {
2511
2642
  "use strict";
@@ -2513,8 +2644,10 @@ var init_codex = __esm({
2513
2644
  init_artifact_backups();
2514
2645
  init_toml();
2515
2646
  init_common();
2516
- MCP_SELECTOR = "mcp_servers.tap-comms";
2517
- ENV_SELECTOR = "mcp_servers.tap-comms.env";
2647
+ MCP_SELECTOR = "mcp_servers.tap";
2648
+ ENV_SELECTOR = "mcp_servers.tap.env";
2649
+ OLD_MCP_SELECTOR = "mcp_servers.tap-comms";
2650
+ OLD_ENV_SELECTOR = "mcp_servers.tap-comms.env";
2518
2651
  codexAdapter = {
2519
2652
  runtime: "codex",
2520
2653
  async probe(ctx) {
@@ -2560,6 +2693,11 @@ var init_codex = __esm({
2560
2693
  if (extractTomlTable(content, MCP_SELECTOR)) {
2561
2694
  conflicts.push(`Existing ${MCP_SELECTOR} table will be updated.`);
2562
2695
  }
2696
+ if (extractTomlTable(content, OLD_MCP_SELECTOR)) {
2697
+ conflicts.push(
2698
+ `Legacy ${OLD_MCP_SELECTOR} table will be migrated to ${MCP_SELECTOR}.`
2699
+ );
2700
+ }
2563
2701
  if (extractTomlTable(content, ENV_SELECTOR)) {
2564
2702
  conflicts.push(`Existing ${ENV_SELECTOR} table will be updated.`);
2565
2703
  }
@@ -2625,6 +2763,12 @@ var init_codex = __esm({
2625
2763
  return { ...artifact, backupPath };
2626
2764
  });
2627
2765
  let nextContent = existingContent;
2766
+ if (extractTomlTable(nextContent, OLD_ENV_SELECTOR)) {
2767
+ nextContent = removeTomlTable(nextContent, OLD_ENV_SELECTOR);
2768
+ }
2769
+ if (extractTomlTable(nextContent, OLD_MCP_SELECTOR)) {
2770
+ nextContent = removeTomlTable(nextContent, OLD_MCP_SELECTOR);
2771
+ }
2628
2772
  nextContent = replaceTomlTable(
2629
2773
  nextContent,
2630
2774
  MCP_SELECTOR,
@@ -2801,7 +2945,7 @@ function verifyGeminiConfig(config, configPath, ctx) {
2801
2945
  message: fs12.existsSync(configPath) ? void 0 : `${configPath} not found`
2802
2946
  });
2803
2947
  checks.push({
2804
- name: "tap-comms entry present",
2948
+ name: "tap entry present",
2805
2949
  passed: !!entry,
2806
2950
  message: entry ? void 0 : `${GEMINI_SELECTOR} not found`
2807
2951
  });
@@ -2819,14 +2963,15 @@ function verifyGeminiConfig(config, configPath, ctx) {
2819
2963
  }
2820
2964
  return checks;
2821
2965
  }
2822
- var GEMINI_SELECTOR, geminiAdapter;
2966
+ var GEMINI_SELECTOR, OLD_GEMINI_SELECTOR, geminiAdapter;
2823
2967
  var init_gemini = __esm({
2824
2968
  "src/adapters/gemini.ts"() {
2825
2969
  "use strict";
2826
2970
  init_state();
2827
2971
  init_artifact_backups();
2828
2972
  init_common();
2829
- GEMINI_SELECTOR = "mcpServers.tap-comms";
2973
+ GEMINI_SELECTOR = "mcpServers.tap";
2974
+ OLD_GEMINI_SELECTOR = "mcpServers.tap-comms";
2830
2975
  geminiAdapter = {
2831
2976
  runtime: "gemini",
2832
2977
  async probe(ctx) {
@@ -2875,6 +3020,11 @@ var init_gemini = __esm({
2875
3020
  if (readNestedKey(config, GEMINI_SELECTOR) !== void 0) {
2876
3021
  conflicts.push(`Existing ${GEMINI_SELECTOR} entry will be updated.`);
2877
3022
  }
3023
+ if (readNestedKey(config, OLD_GEMINI_SELECTOR) !== void 0) {
3024
+ conflicts.push(
3025
+ `Legacy ${OLD_GEMINI_SELECTOR} entry will be migrated to ${GEMINI_SELECTOR}.`
3026
+ );
3027
+ }
2878
3028
  } catch {
2879
3029
  warnings.push(
2880
3030
  `${configPath} exists but is not valid JSON. It will be replaced.`
@@ -2942,6 +3092,13 @@ var init_gemini = __esm({
2942
3092
  existed: previousValue !== void 0,
2943
3093
  value: previousValue
2944
3094
  });
3095
+ const oldValue = readNestedKey(config, OLD_GEMINI_SELECTOR);
3096
+ if (oldValue !== void 0) {
3097
+ const servers = config.mcpServers;
3098
+ if (servers) {
3099
+ delete servers["tap-comms"];
3100
+ }
3101
+ }
2945
3102
  setNestedKey2(config, GEMINI_SELECTOR, {
2946
3103
  command: managed.command,
2947
3104
  args: managed.args,
@@ -3049,16 +3206,31 @@ function redactProtectedUrl(url) {
3049
3206
  try {
3050
3207
  const parsed = new URL(url);
3051
3208
  if (parsed.searchParams.has("tap_token")) {
3052
- parsed.searchParams.set("tap_token", "***");
3209
+ parsed.searchParams.delete("tap_token");
3053
3210
  }
3054
3211
  return parsed.toString().replace(/\/$/, "");
3055
3212
  } catch {
3056
- return url.replace(/tap_token=[^&]+/g, "tap_token=***");
3213
+ return url.replace(/[?&]tap_token=[^&]+/g, "");
3057
3214
  }
3058
3215
  }
3059
3216
  function loadCurrentBridgeState(stateDir, instanceId, fallback) {
3060
3217
  return loadBridgeState(stateDir, instanceId) ?? fallback ?? null;
3061
3218
  }
3219
+ function formatThreadSummary(threadId, cwd) {
3220
+ if (!threadId) {
3221
+ return "-";
3222
+ }
3223
+ return cwd ? `${threadId} (${cwd})` : threadId;
3224
+ }
3225
+ function normalizeComparablePath(value) {
3226
+ return path13.resolve(value).replace(/\\/g, "/").toLowerCase();
3227
+ }
3228
+ function sameOptionalPath(left, right) {
3229
+ if (!left || !right) {
3230
+ return left === right;
3231
+ }
3232
+ return normalizeComparablePath(left) === normalizeComparablePath(right);
3233
+ }
3062
3234
  function getSharedAppServerUsers(state, stateDir, currentInstanceId, appServerUrl) {
3063
3235
  const shared = [];
3064
3236
  for (const [id, inst] of Object.entries(state.instances)) {
@@ -3136,37 +3308,37 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3136
3308
  };
3137
3309
  }
3138
3310
  const instanceId = resolved.instanceId;
3139
- let instance = state.instances[instanceId];
3140
- if (!instance?.installed) {
3311
+ let instance2 = state.instances[instanceId];
3312
+ if (!instance2?.installed) {
3141
3313
  return {
3142
3314
  ok: false,
3143
3315
  command: "bridge",
3144
3316
  instanceId,
3145
- runtime: instance?.runtime,
3317
+ runtime: instance2?.runtime,
3146
3318
  code: "TAP_INSTANCE_NOT_FOUND",
3147
- message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance?.runtime ?? identifier}`,
3319
+ message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance2?.runtime ?? identifier}`,
3148
3320
  warnings: [],
3149
3321
  data: {}
3150
3322
  };
3151
3323
  }
3152
- const adapter = getAdapter(instance.runtime);
3324
+ const adapter = getAdapter(instance2.runtime);
3153
3325
  const mode = adapter.bridgeMode();
3154
3326
  if (mode !== "app-server") {
3155
3327
  return {
3156
3328
  ok: true,
3157
3329
  command: "bridge",
3158
3330
  instanceId,
3159
- runtime: instance.runtime,
3331
+ runtime: instance2.runtime,
3160
3332
  code: "TAP_NO_OP",
3161
3333
  message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
3162
3334
  warnings: [],
3163
3335
  data: { bridgeMode: mode }
3164
3336
  };
3165
3337
  }
3166
- const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
3167
- if (agentName && agentName !== instance.agentName) {
3168
- instance = { ...instance, agentName };
3169
- const updatedState = updateInstanceState(state, instanceId, instance);
3338
+ const resolvedAgentName = agentName ?? instance2.agentName ?? void 0;
3339
+ if (agentName && agentName !== instance2.agentName) {
3340
+ instance2 = { ...instance2, agentName };
3341
+ const updatedState = updateInstanceState(state, instanceId, instance2);
3170
3342
  saveState(repoRoot, updatedState);
3171
3343
  state = updatedState;
3172
3344
  }
@@ -3177,7 +3349,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3177
3349
  ok: false,
3178
3350
  command: "bridge",
3179
3351
  instanceId,
3180
- runtime: instance.runtime,
3352
+ runtime: instance2.runtime,
3181
3353
  code: "TAP_BRIDGE_SCRIPT_MISSING",
3182
3354
  message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
3183
3355
  warnings: [],
@@ -3186,8 +3358,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3186
3358
  }
3187
3359
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
3188
3360
  const runtimeCommand = resolvedConfig.runtimeCommand;
3189
- const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
3190
- let effectivePort = instance.port;
3361
+ const manageAppServer = instance2.runtime === "codex" && flags["no-server"] !== true;
3362
+ let effectivePort = instance2.port;
3191
3363
  if (effectivePort == null && manageAppServer) {
3192
3364
  effectivePort = await findNextAvailableAppServerPort(
3193
3365
  state,
@@ -3195,8 +3367,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3195
3367
  4501,
3196
3368
  instanceId
3197
3369
  );
3198
- instance = { ...instance, port: effectivePort };
3199
- const updatedState = updateInstanceState(state, instanceId, instance);
3370
+ instance2 = { ...instance2, port: effectivePort };
3371
+ const updatedState = updateInstanceState(state, instanceId, instance2);
3200
3372
  saveState(repoRoot, updatedState);
3201
3373
  state = updatedState;
3202
3374
  }
@@ -3212,19 +3384,19 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3212
3384
  if (effectivePort != null) log(`Port: ${effectivePort}`);
3213
3385
  if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
3214
3386
  const noAuth = flags["no-auth"] === true;
3215
- if (!manageAppServer && instance.runtime === "codex") {
3387
+ if (!manageAppServer && instance2.runtime === "codex") {
3216
3388
  log("Auto server: disabled (--no-server)");
3217
3389
  }
3218
3390
  if (noAuth && manageAppServer) {
3219
3391
  log("Auth gateway: disabled (--no-auth)");
3220
3392
  }
3221
- const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
3393
+ const willBeHeadless = flags["headless"] === true || instance2.headless?.enabled;
3222
3394
  if (willBeHeadless) {
3223
- const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance.headless?.role ?? "reviewer";
3395
+ const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance2.headless?.role ?? "reviewer";
3224
3396
  log(`Headless: ${role}`);
3225
3397
  }
3226
3398
  try {
3227
- if (!manageAppServer && instance.runtime === "codex") {
3399
+ if (!manageAppServer && instance2.runtime === "codex") {
3228
3400
  log("Checking app-server health...");
3229
3401
  const healthy = await checkAppServerHealth(appServerUrl);
3230
3402
  if (healthy) {
@@ -3235,7 +3407,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3235
3407
  ok: false,
3236
3408
  command: "bridge",
3237
3409
  instanceId,
3238
- runtime: instance.runtime,
3410
+ runtime: instance2.runtime,
3239
3411
  code: "TAP_BRIDGE_START_FAILED",
3240
3412
  message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
3241
3413
  warnings: [],
@@ -3249,7 +3421,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3249
3421
  ok: false,
3250
3422
  command: "bridge",
3251
3423
  instanceId,
3252
- runtime: instance.runtime,
3424
+ runtime: instance2.runtime,
3253
3425
  code: "TAP_INVALID_ARGUMENT",
3254
3426
  message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
3255
3427
  warnings: [],
@@ -3257,9 +3429,38 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3257
3429
  };
3258
3430
  }
3259
3431
  const busyMode = busyModeRaw;
3260
- const pollSeconds = typeof flags["poll-seconds"] === "string" ? parseInt(flags["poll-seconds"], 10) : void 0;
3261
- const reconnectSeconds = typeof flags["reconnect-seconds"] === "string" ? parseInt(flags["reconnect-seconds"], 10) : void 0;
3262
- const messageLookbackMinutes = typeof flags["message-lookback-minutes"] === "string" ? parseInt(flags["message-lookback-minutes"], 10) : void 0;
3432
+ const pollSecondsRaw = typeof flags["poll-seconds"] === "string" ? flags["poll-seconds"] : void 0;
3433
+ const reconnectSecondsRaw = typeof flags["reconnect-seconds"] === "string" ? flags["reconnect-seconds"] : void 0;
3434
+ const lookbackRaw = typeof flags["message-lookback-minutes"] === "string" ? flags["message-lookback-minutes"] : void 0;
3435
+ let pollSeconds;
3436
+ let reconnectSeconds;
3437
+ let messageLookbackMinutes;
3438
+ try {
3439
+ pollSeconds = parseIntFlag(pollSecondsRaw, "--poll-seconds", 1, 3600);
3440
+ reconnectSeconds = parseIntFlag(
3441
+ reconnectSecondsRaw,
3442
+ "--reconnect-seconds",
3443
+ 1,
3444
+ 3600
3445
+ );
3446
+ messageLookbackMinutes = parseIntFlag(
3447
+ lookbackRaw,
3448
+ "--message-lookback-minutes",
3449
+ 1,
3450
+ 10080
3451
+ );
3452
+ } catch (err) {
3453
+ return {
3454
+ ok: false,
3455
+ command: "bridge",
3456
+ instanceId,
3457
+ runtime: instance2.runtime,
3458
+ code: "TAP_INVALID_ARGUMENT",
3459
+ message: err instanceof Error ? err.message : String(err),
3460
+ warnings: [],
3461
+ data: {}
3462
+ };
3463
+ }
3263
3464
  const threadId = typeof flags["thread-id"] === "string" ? flags["thread-id"] : void 0;
3264
3465
  const ephemeral = flags["ephemeral"] === true;
3265
3466
  const processExistingMessages = flags["process-existing-messages"] === true;
@@ -3271,7 +3472,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3271
3472
  ok: false,
3272
3473
  command: "bridge",
3273
3474
  instanceId,
3274
- runtime: instance.runtime,
3475
+ runtime: instance2.runtime,
3275
3476
  code: "TAP_INVALID_ARGUMENT",
3276
3477
  message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
3277
3478
  warnings: [],
@@ -3283,10 +3484,10 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3283
3484
  role: roleArg ?? "reviewer",
3284
3485
  maxRounds: 5,
3285
3486
  qualitySeverityFloor: "high"
3286
- } : instance.headless;
3487
+ } : instance2.headless;
3287
3488
  const bridge = await startBridge({
3288
3489
  instanceId,
3289
- runtime: instance.runtime,
3490
+ runtime: instance2.runtime,
3290
3491
  stateDir: ctx.stateDir,
3291
3492
  commsDir: ctx.commsDir,
3292
3493
  bridgeScript,
@@ -3327,14 +3528,14 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3327
3528
  log(`TUI connect: ${bridge.appServer.url}`);
3328
3529
  }
3329
3530
  }
3330
- const updated = { ...instance, bridge, manageAppServer, noAuth };
3531
+ const updated = { ...instance2, bridge, manageAppServer, noAuth };
3331
3532
  const newState = updateInstanceState(state, instanceId, updated);
3332
3533
  saveState(repoRoot, newState);
3333
3534
  return {
3334
3535
  ok: true,
3335
3536
  command: "bridge",
3336
3537
  instanceId,
3337
- runtime: instance.runtime,
3538
+ runtime: instance2.runtime,
3338
3539
  code: "TAP_BRIDGE_START_OK",
3339
3540
  message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
3340
3541
  warnings: [],
@@ -3347,7 +3548,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3347
3548
  ok: false,
3348
3549
  command: "bridge",
3349
3550
  instanceId,
3350
- runtime: instance.runtime,
3551
+ runtime: instance2.runtime,
3351
3552
  code: "TAP_BRIDGE_START_FAILED",
3352
3553
  message: msg,
3353
3554
  warnings: [],
@@ -3449,11 +3650,11 @@ async function bridgeStopOne(identifier) {
3449
3650
  }
3450
3651
  const instanceId = resolved.instanceId;
3451
3652
  const ctx = createAdapterContext(state.commsDir, repoRoot);
3452
- const instance = state.instances[instanceId];
3653
+ const instance2 = state.instances[instanceId];
3453
3654
  const bridgeState = loadCurrentBridgeState(
3454
3655
  ctx.stateDir,
3455
3656
  instanceId,
3456
- instance?.bridge
3657
+ instance2?.bridge
3457
3658
  );
3458
3659
  const appServer = bridgeState?.appServer ?? null;
3459
3660
  logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
@@ -3501,8 +3702,8 @@ async function bridgeStopOne(identifier) {
3501
3702
  }
3502
3703
  }
3503
3704
  }
3504
- if (instance) {
3505
- const updated = { ...instance, bridge: null };
3705
+ if (instance2) {
3706
+ const updated = { ...instance2, bridge: null };
3506
3707
  const newState = updateInstanceState(state, instanceId, updated);
3507
3708
  saveState(repoRoot, newState);
3508
3709
  }
@@ -3574,9 +3775,9 @@ async function bridgeStopAll() {
3574
3775
  logSuccess(`Stopped bridge for ${instanceId}`);
3575
3776
  stopped.push(instanceId);
3576
3777
  }
3577
- const instance = state.instances[instanceId];
3578
- if (instance?.bridge) {
3579
- state.instances[instanceId] = { ...instance, bridge: null };
3778
+ const instance2 = state.instances[instanceId];
3779
+ if (instance2?.bridge) {
3780
+ state.instances[instanceId] = { ...instance2, bridge: null };
3580
3781
  stateChanged = true;
3581
3782
  }
3582
3783
  }
@@ -3642,12 +3843,18 @@ function bridgeStatusAll() {
3642
3843
  pid: null,
3643
3844
  port: inst.port,
3644
3845
  lastHeartbeat: null,
3846
+ threadId: null,
3847
+ threadCwd: null,
3848
+ savedThreadId: null,
3849
+ savedThreadCwd: null,
3645
3850
  appServer: null
3646
3851
  };
3647
3852
  continue;
3648
3853
  }
3649
3854
  const status = getBridgeStatus(stateDir, instanceId);
3650
3855
  const bridgeState = loadBridgeState(stateDir, instanceId);
3856
+ const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
3857
+ const savedThread = loadRuntimeBridgeThreadState(bridgeState);
3651
3858
  const age = getHeartbeatAge(stateDir, instanceId);
3652
3859
  const pid = bridgeState?.pid ?? null;
3653
3860
  const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
@@ -3669,12 +3876,26 @@ function bridgeStatusAll() {
3669
3876
  );
3670
3877
  }
3671
3878
  }
3879
+ if (runtimeHeartbeat?.threadId) {
3880
+ log(
3881
+ ` Thread: ${formatThreadSummary(runtimeHeartbeat.threadId, runtimeHeartbeat.threadCwd)}`
3882
+ );
3883
+ }
3884
+ if (savedThread?.threadId && (savedThread.threadId !== runtimeHeartbeat?.threadId || !sameOptionalPath(savedThread.cwd, runtimeHeartbeat?.threadCwd))) {
3885
+ log(
3886
+ ` Saved: ${formatThreadSummary(savedThread.threadId, savedThread.cwd)}`
3887
+ );
3888
+ }
3672
3889
  bridges[instanceId] = {
3673
3890
  status,
3674
3891
  runtime: inst.runtime,
3675
3892
  pid,
3676
3893
  port: inst.port,
3677
3894
  lastHeartbeat: heartbeat,
3895
+ threadId: runtimeHeartbeat?.threadId ?? null,
3896
+ threadCwd: runtimeHeartbeat?.threadCwd ?? null,
3897
+ savedThreadId: savedThread?.threadId ?? null,
3898
+ savedThreadCwd: savedThread?.cwd ?? null,
3678
3899
  appServer: bridgeState?.appServer ?? null
3679
3900
  };
3680
3901
  }
@@ -3750,6 +3971,10 @@ function bridgeStatusOne(identifier) {
3750
3971
  pid: null,
3751
3972
  port: inst.port,
3752
3973
  lastHeartbeat: null,
3974
+ threadId: null,
3975
+ threadCwd: null,
3976
+ savedThreadId: null,
3977
+ savedThreadCwd: null,
3753
3978
  appServer: null
3754
3979
  }
3755
3980
  };
@@ -3758,6 +3983,8 @@ function bridgeStatusOne(identifier) {
3758
3983
  const stateDir = resolvedCfg2.stateDir;
3759
3984
  const status = getBridgeStatus(stateDir, instanceId);
3760
3985
  const bridgeState = loadBridgeState(stateDir, instanceId);
3986
+ const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
3987
+ const savedThread = loadRuntimeBridgeThreadState(bridgeState);
3761
3988
  const age = getHeartbeatAge(stateDir, instanceId);
3762
3989
  const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
3763
3990
  log(`Status: ${status}`);
@@ -3766,6 +3993,16 @@ function bridgeStatusOne(identifier) {
3766
3993
  log(
3767
3994
  `Heartbeat: ${heartbeat ?? "-"}${age !== null ? ` (${formatAge(age)})` : ""}`
3768
3995
  );
3996
+ if (runtimeHeartbeat?.threadId) {
3997
+ log(
3998
+ `Thread: ${formatThreadSummary(runtimeHeartbeat.threadId, runtimeHeartbeat.threadCwd)}`
3999
+ );
4000
+ }
4001
+ if (savedThread?.threadId && (savedThread.threadId !== runtimeHeartbeat?.threadId || !sameOptionalPath(savedThread.cwd, runtimeHeartbeat?.threadCwd))) {
4002
+ log(
4003
+ `Saved: ${formatThreadSummary(savedThread.threadId, savedThread.cwd)}`
4004
+ );
4005
+ }
3769
4006
  log(
3770
4007
  `Log: ${path13.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
3771
4008
  );
@@ -3814,6 +4051,10 @@ function bridgeStatusOne(identifier) {
3814
4051
  pid: bridgeState?.pid ?? null,
3815
4052
  port: inst.port,
3816
4053
  lastHeartbeat: heartbeat,
4054
+ threadId: runtimeHeartbeat?.threadId ?? null,
4055
+ threadCwd: runtimeHeartbeat?.threadCwd ?? null,
4056
+ savedThreadId: savedThread?.threadId ?? null,
4057
+ savedThreadCwd: savedThread?.cwd ?? null,
3817
4058
  appServer: bridgeState?.appServer ?? null
3818
4059
  }
3819
4060
  };
@@ -3872,8 +4113,22 @@ async function bridgeRestart(identifier, flags) {
3872
4113
  };
3873
4114
  }
3874
4115
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
3875
- const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
3876
- const drainTimeout = parseInt(drainStr, 10) || 30;
4116
+ const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : void 0;
4117
+ let drainTimeout;
4118
+ try {
4119
+ drainTimeout = parseIntFlag(drainStr, "--drain-timeout", 1, 300) ?? 30;
4120
+ } catch (err) {
4121
+ return {
4122
+ ok: false,
4123
+ command: "bridge",
4124
+ instanceId,
4125
+ runtime: instance.runtime,
4126
+ code: "TAP_INVALID_ARGUMENT",
4127
+ message: err instanceof Error ? err.message : String(err),
4128
+ warnings: [],
4129
+ data: {}
4130
+ };
4131
+ }
3877
4132
  logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
3878
4133
  log(`Drain timeout: ${drainTimeout}s`);
3879
4134
  try {
@@ -4027,7 +4282,7 @@ var init_bridge2 = __esm({
4027
4282
  init_utils();
4028
4283
  BRIDGE_HELP = `
4029
4284
  Usage:
4030
- tap-comms bridge <subcommand> [instance] [options]
4285
+ tap bridge <subcommand> [instance] [options]
4031
4286
 
4032
4287
  Subcommands:
4033
4288
  start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
@@ -4039,7 +4294,7 @@ Subcommands:
4039
4294
 
4040
4295
  Options:
4041
4296
  --agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
4042
- Saved to state \u2014 only needed on first start
4297
+ Overrides the stored name from 'tap add' when needed
4043
4298
  --all Start all registered app-server instances
4044
4299
  --busy-mode <steer|wait> How to handle active turns (default: steer)
4045
4300
  --poll-seconds <n> Inbox poll interval (default: 5)
@@ -4085,7 +4340,18 @@ async function upCommand(args) {
4085
4340
  };
4086
4341
  }
4087
4342
  const repoRoot = findRepoRoot();
4088
- const result = await bridgeCommand(["start", "--all", ...args]);
4343
+ const previousColdStartWarmup = process.env.TAP_COLD_START_WARMUP;
4344
+ process.env.TAP_COLD_START_WARMUP = "true";
4345
+ let result;
4346
+ try {
4347
+ result = await bridgeCommand(["start", "--all", ...args]);
4348
+ } finally {
4349
+ if (previousColdStartWarmup === void 0) {
4350
+ delete process.env.TAP_COLD_START_WARMUP;
4351
+ } else {
4352
+ process.env.TAP_COLD_START_WARMUP = previousColdStartWarmup;
4353
+ }
4354
+ }
4089
4355
  const snapshot = collectDashboardSnapshot(repoRoot);
4090
4356
  const activeBridges = snapshot.bridges.filter(
4091
4357
  (bridge) => bridge.status === "running"
@@ -4121,7 +4387,7 @@ var init_up = __esm({
4121
4387
  init_utils();
4122
4388
  UP_HELP = `
4123
4389
  Usage:
4124
- tap-comms up [bridge-start options]
4390
+ tap up [bridge-start options]
4125
4391
 
4126
4392
  Description:
4127
4393
  Start all registered app-server bridge daemons with one command.
@@ -4186,7 +4452,7 @@ var init_down = __esm({
4186
4452
  init_utils();
4187
4453
  DOWN_HELP = `
4188
4454
  Usage:
4189
- tap-comms down
4455
+ tap down
4190
4456
 
4191
4457
  Description:
4192
4458
  Stop all running bridge daemons and managed app-servers.
@@ -4240,14 +4506,14 @@ async function* streamEvents(options) {
4240
4506
  const repoRoot = options?.repoRoot ?? findRepoRoot();
4241
4507
  while (!options?.signal?.aborted) {
4242
4508
  yield collectDashboardSnapshot(repoRoot, options?.commsDir);
4243
- await new Promise((resolve8) => {
4509
+ await new Promise((resolve9) => {
4244
4510
  const onAbort = () => {
4245
4511
  clearTimeout(timer);
4246
- resolve8();
4512
+ resolve9();
4247
4513
  };
4248
4514
  const timer = setTimeout(() => {
4249
4515
  options?.signal?.removeEventListener("abort", onAbort);
4250
- resolve8();
4516
+ resolve9();
4251
4517
  }, intervalMs);
4252
4518
  options?.signal?.addEventListener("abort", onAbort, { once: true });
4253
4519
  });
@@ -4346,11 +4612,38 @@ function getConfig(options) {
4346
4612
  import {
4347
4613
  createServer as createServer2
4348
4614
  } from "http";
4615
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
4349
4616
  var CORS_HEADERS = {
4350
4617
  "Access-Control-Allow-Origin": "http://localhost:3000",
4351
4618
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
4352
- "Access-Control-Allow-Headers": "Content-Type"
4619
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
4353
4620
  };
4621
+ function tokensMatch(presentedToken, expectedToken) {
4622
+ if (!presentedToken) {
4623
+ return false;
4624
+ }
4625
+ const presented = Buffer.from(presentedToken, "utf8");
4626
+ const expected = Buffer.from(expectedToken, "utf8");
4627
+ if (presented.length !== expected.length) {
4628
+ return false;
4629
+ }
4630
+ return timingSafeEqual(presented, expected);
4631
+ }
4632
+ function verifyBearerToken(req, expectedToken) {
4633
+ const header = req.headers.authorization;
4634
+ if (!header?.startsWith("Bearer ")) {
4635
+ return false;
4636
+ }
4637
+ return tokensMatch(header.slice(7), expectedToken);
4638
+ }
4639
+ function verifySseToken(req, expectedToken, serverUrl) {
4640
+ if (verifyBearerToken(req, expectedToken)) {
4641
+ return true;
4642
+ }
4643
+ const url = new URL(req.url ?? "/", serverUrl);
4644
+ const queryToken = url.searchParams.get("token");
4645
+ return tokensMatch(queryToken, expectedToken);
4646
+ }
4354
4647
  function jsonResponse(res, data, status = 200) {
4355
4648
  res.writeHead(status, {
4356
4649
  "Content-Type": "application/json",
@@ -4393,6 +4686,7 @@ function handleHealth(res, apiOptions) {
4393
4686
  async function startHttpServer(options) {
4394
4687
  const port = options?.port ?? 4580;
4395
4688
  const host = "127.0.0.1";
4689
+ const token = options?.token ?? randomBytes2(24).toString("base64url");
4396
4690
  const apiOptions = {
4397
4691
  repoRoot: options?.repoRoot,
4398
4692
  commsDir: options?.commsDir
@@ -4406,21 +4700,32 @@ async function startHttpServer(options) {
4406
4700
  res.end();
4407
4701
  return;
4408
4702
  }
4703
+ if (req.method === "GET" && pathname === "/health") {
4704
+ handleHealth(res, apiOptions);
4705
+ return;
4706
+ }
4707
+ if (req.method === "GET" && pathname === "/api/events") {
4708
+ const serverUrl = `http://${host}:${port}`;
4709
+ if (!verifySseToken(req, token, serverUrl)) {
4710
+ jsonResponse(res, { error: "Unauthorized" }, 401);
4711
+ return;
4712
+ }
4713
+ await handleEvents(req, res, apiOptions);
4714
+ return;
4715
+ }
4716
+ if (!verifyBearerToken(req, token)) {
4717
+ jsonResponse(res, { error: "Unauthorized" }, 401);
4718
+ return;
4719
+ }
4409
4720
  try {
4410
4721
  if (req.method === "GET") {
4411
4722
  switch (pathname) {
4412
4723
  case "/api/snapshot":
4413
4724
  handleSnapshot(res, apiOptions);
4414
4725
  return;
4415
- case "/api/events":
4416
- await handleEvents(req, res, apiOptions);
4417
- return;
4418
4726
  case "/api/config":
4419
4727
  handleConfig(res, apiOptions);
4420
4728
  return;
4421
- case "/health":
4422
- handleHealth(res, apiOptions);
4423
- return;
4424
4729
  }
4425
4730
  }
4426
4731
  if (req.method === "POST") {
@@ -4449,17 +4754,18 @@ async function startHttpServer(options) {
4449
4754
  }
4450
4755
  }
4451
4756
  );
4452
- await new Promise((resolve8, reject) => {
4757
+ await new Promise((resolve9, reject) => {
4453
4758
  server.once("error", reject);
4454
4759
  server.listen(port, host, () => {
4455
4760
  server.removeListener("error", reject);
4456
- resolve8();
4761
+ resolve9();
4457
4762
  });
4458
4763
  });
4459
4764
  return {
4460
4765
  port,
4461
- close: () => new Promise((resolve8, reject) => {
4462
- server.close((err) => err ? reject(err) : resolve8());
4766
+ token,
4767
+ close: () => new Promise((resolve9, reject) => {
4768
+ server.close((err) => err ? reject(err) : resolve9());
4463
4769
  })
4464
4770
  };
4465
4771
  }