@hua-labs/tap 0.2.4 → 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/cli.mjs CHANGED
@@ -775,10 +775,10 @@ function parsePermissionMode(args) {
775
775
  }
776
776
  var INIT_HELP = `
777
777
  Usage:
778
- tap-comms init [options]
778
+ tap init [options]
779
779
 
780
780
  Description:
781
- Initialize the tap-comms directory structure, state file, and permissions.
781
+ Initialize the tap directory structure, state file, and permissions.
782
782
  Optionally clone a shared comms repository.
783
783
 
784
784
  Options:
@@ -1077,7 +1077,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1077
1077
  }
1078
1078
  if (!sourcePath) {
1079
1079
  issues.push(
1080
- "tap-comms MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
1080
+ "tap MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
1081
1081
  );
1082
1082
  return { command: null, args: [], env, sourcePath, warnings, issues };
1083
1083
  }
@@ -1107,7 +1107,7 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1107
1107
  }
1108
1108
  if (!command) {
1109
1109
  issues.push(
1110
- "bun is required to run the repo-local tap-comms MCP server (.ts source). Install bun: https://bun.sh"
1110
+ "bun is required to run the repo-local tap MCP server (.ts source). Install bun: https://bun.sh"
1111
1111
  );
1112
1112
  return { command: null, args: [], env, sourcePath, warnings, issues };
1113
1113
  }
@@ -1122,7 +1122,8 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1122
1122
  }
1123
1123
 
1124
1124
  // src/adapters/claude.ts
1125
- var MCP_SERVER_KEY = "tap-comms";
1125
+ var MCP_SERVER_KEY = "tap";
1126
+ var OLD_MCP_SERVER_KEY = "tap-comms";
1126
1127
  function findMcpJsonPath(ctx) {
1127
1128
  return path8.join(ctx.repoRoot, ".mcp.json");
1128
1129
  }
@@ -1200,6 +1201,11 @@ var claudeAdapter = {
1200
1201
  `Existing "${MCP_SERVER_KEY}" entry in .mcp.json will be overwritten.`
1201
1202
  );
1202
1203
  }
1204
+ if (config.mcpServers?.[OLD_MCP_SERVER_KEY]) {
1205
+ conflicts.push(
1206
+ `Legacy "${OLD_MCP_SERVER_KEY}" entry will be migrated to "${MCP_SERVER_KEY}".`
1207
+ );
1208
+ }
1203
1209
  } catch {
1204
1210
  warnings.push(
1205
1211
  ".mcp.json exists but is not valid JSON. Will be overwritten."
@@ -1209,7 +1215,7 @@ var claudeAdapter = {
1209
1215
  const serverEntry = buildMcpServerEntry(ctx);
1210
1216
  if (!serverEntry) {
1211
1217
  warnings.push(
1212
- "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."
1218
+ "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."
1213
1219
  );
1214
1220
  return {
1215
1221
  runtime: "claude",
@@ -1262,6 +1268,10 @@ var claudeAdapter = {
1262
1268
  );
1263
1269
  }
1264
1270
  }
1271
+ const servers = config.mcpServers;
1272
+ if (servers?.[OLD_MCP_SERVER_KEY]) {
1273
+ delete servers[OLD_MCP_SERVER_KEY];
1274
+ }
1265
1275
  if (op.key) {
1266
1276
  setNestedKey(config, op.key, op.value);
1267
1277
  }
@@ -1310,7 +1320,7 @@ var claudeAdapter = {
1310
1320
  checks.push({ name: "Config is valid JSON", passed: true });
1311
1321
  const entry = config.mcpServers?.[MCP_SERVER_KEY];
1312
1322
  checks.push({
1313
- name: "tap-comms entry present",
1323
+ name: "tap entry present",
1314
1324
  passed: !!entry,
1315
1325
  message: entry ? void 0 : `mcpServers.${MCP_SERVER_KEY} not found`
1316
1326
  });
@@ -1403,8 +1413,10 @@ function readArtifactBackup(backupPath) {
1403
1413
  }
1404
1414
 
1405
1415
  // src/adapters/codex.ts
1406
- var MCP_SELECTOR = "mcp_servers.tap-comms";
1407
- var ENV_SELECTOR = "mcp_servers.tap-comms.env";
1416
+ var MCP_SELECTOR = "mcp_servers.tap";
1417
+ var ENV_SELECTOR = "mcp_servers.tap.env";
1418
+ var OLD_MCP_SELECTOR = "mcp_servers.tap-comms";
1419
+ var OLD_ENV_SELECTOR = "mcp_servers.tap-comms.env";
1408
1420
  function findCodexConfigPath2() {
1409
1421
  return path10.join(getHomeDir(), ".codex", "config.toml");
1410
1422
  }
@@ -1458,12 +1470,12 @@ function verifyManagedToml(content, ctx, configPath) {
1458
1470
  message: fs10.existsSync(configPath) ? void 0 : `${configPath} not found`
1459
1471
  });
1460
1472
  checks.push({
1461
- name: "tap-comms MCP table present",
1473
+ name: "tap MCP table present",
1462
1474
  passed: !!mainTable,
1463
1475
  message: mainTable ? void 0 : `${MCP_SELECTOR} not found`
1464
1476
  });
1465
1477
  checks.push({
1466
- name: "tap-comms env table present",
1478
+ name: "tap env table present",
1467
1479
  passed: !!envTable,
1468
1480
  message: envTable ? void 0 : `${ENV_SELECTOR} not found`
1469
1481
  });
@@ -1483,7 +1495,7 @@ function verifyManagedToml(content, ctx, configPath) {
1483
1495
  passed: mainTable.includes(
1484
1496
  `command = "${managed.command.replace(/\\/g, "\\\\")}"`
1485
1497
  ) && mainTable.includes(`args = [${expectedArgs}]`),
1486
- message: "Managed tap-comms command/args do not match expected values"
1498
+ message: "Managed tap command/args do not match expected values"
1487
1499
  });
1488
1500
  }
1489
1501
  return checks;
@@ -1533,6 +1545,11 @@ var codexAdapter = {
1533
1545
  if (extractTomlTable(content, MCP_SELECTOR)) {
1534
1546
  conflicts.push(`Existing ${MCP_SELECTOR} table will be updated.`);
1535
1547
  }
1548
+ if (extractTomlTable(content, OLD_MCP_SELECTOR)) {
1549
+ conflicts.push(
1550
+ `Legacy ${OLD_MCP_SELECTOR} table will be migrated to ${MCP_SELECTOR}.`
1551
+ );
1552
+ }
1536
1553
  if (extractTomlTable(content, ENV_SELECTOR)) {
1537
1554
  conflicts.push(`Existing ${ENV_SELECTOR} table will be updated.`);
1538
1555
  }
@@ -1598,6 +1615,12 @@ var codexAdapter = {
1598
1615
  return { ...artifact, backupPath };
1599
1616
  });
1600
1617
  let nextContent = existingContent;
1618
+ if (extractTomlTable(nextContent, OLD_ENV_SELECTOR)) {
1619
+ nextContent = removeTomlTable(nextContent, OLD_ENV_SELECTOR);
1620
+ }
1621
+ if (extractTomlTable(nextContent, OLD_MCP_SELECTOR)) {
1622
+ nextContent = removeTomlTable(nextContent, OLD_MCP_SELECTOR);
1623
+ }
1601
1624
  nextContent = replaceTomlTable(
1602
1625
  nextContent,
1603
1626
  MCP_SELECTOR,
@@ -1711,7 +1734,8 @@ var codexAdapter = {
1711
1734
  // src/adapters/gemini.ts
1712
1735
  import * as fs11 from "fs";
1713
1736
  import * as path11 from "path";
1714
- var GEMINI_SELECTOR = "mcpServers.tap-comms";
1737
+ var GEMINI_SELECTOR = "mcpServers.tap";
1738
+ var OLD_GEMINI_SELECTOR = "mcpServers.tap-comms";
1715
1739
  function candidateConfigPaths(ctx) {
1716
1740
  const home = getHomeDir();
1717
1741
  return [
@@ -1773,7 +1797,7 @@ function verifyGeminiConfig(config, configPath, ctx) {
1773
1797
  message: fs11.existsSync(configPath) ? void 0 : `${configPath} not found`
1774
1798
  });
1775
1799
  checks.push({
1776
- name: "tap-comms entry present",
1800
+ name: "tap entry present",
1777
1801
  passed: !!entry,
1778
1802
  message: entry ? void 0 : `${GEMINI_SELECTOR} not found`
1779
1803
  });
@@ -1839,6 +1863,11 @@ var geminiAdapter = {
1839
1863
  if (readNestedKey(config, GEMINI_SELECTOR) !== void 0) {
1840
1864
  conflicts.push(`Existing ${GEMINI_SELECTOR} entry will be updated.`);
1841
1865
  }
1866
+ if (readNestedKey(config, OLD_GEMINI_SELECTOR) !== void 0) {
1867
+ conflicts.push(
1868
+ `Legacy ${OLD_GEMINI_SELECTOR} entry will be migrated to ${GEMINI_SELECTOR}.`
1869
+ );
1870
+ }
1842
1871
  } catch {
1843
1872
  warnings.push(
1844
1873
  `${configPath} exists but is not valid JSON. It will be replaced.`
@@ -1906,6 +1935,13 @@ var geminiAdapter = {
1906
1935
  existed: previousValue !== void 0,
1907
1936
  value: previousValue
1908
1937
  });
1938
+ const oldValue = readNestedKey(config, OLD_GEMINI_SELECTOR);
1939
+ if (oldValue !== void 0) {
1940
+ const servers = config.mcpServers;
1941
+ if (servers) {
1942
+ delete servers["tap-comms"];
1943
+ }
1944
+ }
1909
1945
  setNestedKey2(config, GEMINI_SELECTOR, {
1910
1946
  command: managed.command,
1911
1947
  args: managed.args,
@@ -1987,6 +2023,7 @@ function getAdapter(runtime) {
1987
2023
  // src/engine/bridge.ts
1988
2024
  import * as fs13 from "fs";
1989
2025
  import * as net from "net";
2026
+ import * as os3 from "os";
1990
2027
  import * as path13 from "path";
1991
2028
  import { randomBytes } from "crypto";
1992
2029
  import { spawn, spawnSync as spawnSync3, execSync as execSync3 } from "child_process";
@@ -2159,6 +2196,8 @@ var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
2159
2196
  var APP_SERVER_HEALTH_RETRY_MS = 250;
2160
2197
  var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
2161
2198
  var APP_SERVER_AUTH_FILE_MODE = 384;
2199
+ var WINDOWS_SPAWN_WRAPPER_PREFIX = "tap-spawn-";
2200
+ var WINDOWS_SPAWN_WRAPPER_STALE_MS = 60 * 60 * 1e3;
2162
2201
  function appServerLogFilePath(stateDir, instanceId) {
2163
2202
  return path13.join(stateDir, "logs", `app-server-${instanceId}.log`);
2164
2203
  }
@@ -2195,12 +2234,66 @@ function removeFileIfExists(filePath) {
2195
2234
  } catch {
2196
2235
  }
2197
2236
  }
2237
+ function toPowerShellSingleQuotedString(value) {
2238
+ return `'${value.replace(/'/g, "''")}'`;
2239
+ }
2240
+ function toPowerShellStringArrayLiteral(values) {
2241
+ return `@(${values.map(toPowerShellSingleQuotedString).join(", ")})`;
2242
+ }
2243
+ function cleanupStaleWindowsSpawnWrappers(now = Date.now()) {
2244
+ let entries;
2245
+ try {
2246
+ entries = fs13.readdirSync(os3.tmpdir());
2247
+ } catch {
2248
+ return;
2249
+ }
2250
+ for (const entry of entries) {
2251
+ if (!entry.startsWith(WINDOWS_SPAWN_WRAPPER_PREFIX) || !/\.(cmd|ps1)$/i.test(entry)) {
2252
+ continue;
2253
+ }
2254
+ const wrapperPath = path13.join(os3.tmpdir(), entry);
2255
+ try {
2256
+ const stats = fs13.statSync(wrapperPath);
2257
+ if (now - stats.mtimeMs < WINDOWS_SPAWN_WRAPPER_STALE_MS) {
2258
+ continue;
2259
+ }
2260
+ fs13.unlinkSync(wrapperPath);
2261
+ } catch {
2262
+ }
2263
+ }
2264
+ }
2265
+ function buildWindowsDetachedWrapperScript(command, args, logPath, stderrLogPath, env) {
2266
+ const lines = ["$ErrorActionPreference = 'Stop'"];
2267
+ for (const [key, value] of Object.entries(env)) {
2268
+ if (value !== void 0 && value !== process.env[key]) {
2269
+ lines.push(
2270
+ `[Environment]::SetEnvironmentVariable(${toPowerShellSingleQuotedString(key)}, ${toPowerShellSingleQuotedString(value)}, 'Process')`
2271
+ );
2272
+ }
2273
+ }
2274
+ lines.push(
2275
+ `$logPath = ${toPowerShellSingleQuotedString(logPath)}`,
2276
+ `$stderrLogPath = ${toPowerShellSingleQuotedString(stderrLogPath)}`,
2277
+ `$commandPath = ${toPowerShellSingleQuotedString(command)}`,
2278
+ `$commandArgs = ${toPowerShellStringArrayLiteral(args)}`,
2279
+ "$exitCode = 1",
2280
+ "try {",
2281
+ " & $commandPath @commandArgs >> $logPath 2>> $stderrLogPath",
2282
+ " $exitCode = if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } else { 0 }",
2283
+ "} finally {",
2284
+ " Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue",
2285
+ "}",
2286
+ "exit $exitCode"
2287
+ );
2288
+ return `${lines.join("\r\n")}\r
2289
+ `;
2290
+ }
2198
2291
  function getWebSocketCtor() {
2199
2292
  const candidate = globalThis.WebSocket;
2200
2293
  return typeof candidate === "function" ? candidate : null;
2201
2294
  }
2202
2295
  function delay(ms) {
2203
- return new Promise((resolve11) => setTimeout(resolve11, ms));
2296
+ return new Promise((resolve13) => setTimeout(resolve13, ms));
2204
2297
  }
2205
2298
  function isLoopbackHost(hostname) {
2206
2299
  return hostname === "127.0.0.1" || hostname === "localhost";
@@ -2252,7 +2345,7 @@ function getBridgeRuntimeStateDir(repoRoot, instanceId) {
2252
2345
  }
2253
2346
  async function allocateLoopbackPort(hostname) {
2254
2347
  const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
2255
- return await new Promise((resolve11, reject) => {
2348
+ return await new Promise((resolve13, reject) => {
2256
2349
  const server = net.createServer();
2257
2350
  server.unref();
2258
2351
  server.once("error", reject);
@@ -2270,7 +2363,7 @@ async function allocateLoopbackPort(hostname) {
2270
2363
  reject(error);
2271
2364
  return;
2272
2365
  }
2273
- resolve11(port);
2366
+ resolve13(port);
2274
2367
  });
2275
2368
  });
2276
2369
  });
@@ -2453,35 +2546,50 @@ function findReusableManagedAppServer(stateDir, publicUrl) {
2453
2546
  return null;
2454
2547
  }
2455
2548
  function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
2456
- const ext = path13.extname(command).toLowerCase();
2457
2549
  const stderrLogPath = stderrLogFilePath(logPath);
2458
- const stdoutFd = fs13.openSync(logPath, "a");
2459
- const stderrFd = fs13.openSync(stderrLogPath, "a");
2460
- try {
2461
- const child = ext === ".ps1" ? spawn(
2462
- resolvePowerShellCommand(),
2463
- ["-NoLogo", "-NoProfile", "-File", command, ...args],
2464
- {
2465
- cwd: repoRoot,
2466
- detached: true,
2467
- stdio: ["ignore", stdoutFd, stderrFd],
2468
- env,
2469
- windowsHide: true
2470
- }
2471
- ) : spawn(command, args, {
2472
- cwd: repoRoot,
2473
- detached: true,
2474
- stdio: ["ignore", stdoutFd, stderrFd],
2475
- env,
2476
- windowsHide: true,
2477
- shell: ext === ".cmd" || ext === ".bat"
2478
- });
2479
- child.unref();
2480
- return child.pid ?? null;
2481
- } finally {
2482
- fs13.closeSync(stdoutFd);
2483
- fs13.closeSync(stderrFd);
2550
+ const powerShellCommand = resolvePowerShellCommand();
2551
+ cleanupStaleWindowsSpawnWrappers();
2552
+ const wrapperPath = path13.join(
2553
+ os3.tmpdir(),
2554
+ `${WINDOWS_SPAWN_WRAPPER_PREFIX}${randomBytes(4).toString("hex")}.ps1`
2555
+ );
2556
+ fs13.writeFileSync(
2557
+ wrapperPath,
2558
+ buildWindowsDetachedWrapperScript(
2559
+ command,
2560
+ args,
2561
+ logPath,
2562
+ stderrLogPath,
2563
+ env
2564
+ )
2565
+ );
2566
+ const psCommand = [
2567
+ "$p = Start-Process",
2568
+ `-FilePath ${toPowerShellSingleQuotedString(powerShellCommand)}`,
2569
+ `-ArgumentList ${toPowerShellStringArrayLiteral(["-NoLogo", "-NoProfile", "-File", wrapperPath])}`,
2570
+ `-WorkingDirectory ${toPowerShellSingleQuotedString(repoRoot)}`,
2571
+ "-WindowStyle Hidden",
2572
+ "-PassThru",
2573
+ "; Write-Output $p.Id"
2574
+ ].join(" ");
2575
+ const result = spawnSync3(
2576
+ powerShellCommand,
2577
+ ["-NoLogo", "-NoProfile", "-Command", psCommand],
2578
+ {
2579
+ encoding: "utf-8",
2580
+ windowsHide: true
2581
+ }
2582
+ );
2583
+ if (result.status !== 0) {
2584
+ removeFileIfExists(wrapperPath);
2585
+ return null;
2586
+ }
2587
+ const pid = parseInt(result.stdout.trim(), 10);
2588
+ if (!Number.isFinite(pid)) {
2589
+ removeFileIfExists(wrapperPath);
2590
+ return null;
2484
2591
  }
2592
+ return pid;
2485
2593
  }
2486
2594
  function startWindowsCodexAppServer(command, url, repoRoot, logPath) {
2487
2595
  return startWindowsDetachedProcess(
@@ -2543,12 +2651,12 @@ function resolveAppServerUrl(baseUrl, port) {
2543
2651
  }
2544
2652
  async function isTcpPortAvailable(hostname, port) {
2545
2653
  const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
2546
- return await new Promise((resolve11) => {
2654
+ return await new Promise((resolve13) => {
2547
2655
  const server = net.createServer();
2548
2656
  server.unref();
2549
- server.once("error", () => resolve11(false));
2657
+ server.once("error", () => resolve13(false));
2550
2658
  server.listen(port, bindHost, () => {
2551
- server.close((error) => resolve11(!error));
2659
+ server.close((error) => resolve13(!error));
2552
2660
  });
2553
2661
  });
2554
2662
  }
@@ -2583,7 +2691,7 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
2583
2691
  if (!WebSocket) {
2584
2692
  return false;
2585
2693
  }
2586
- return new Promise((resolve11) => {
2694
+ return new Promise((resolve13) => {
2587
2695
  let settled = false;
2588
2696
  let socket = null;
2589
2697
  const finish = (healthy) => {
@@ -2596,7 +2704,7 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
2596
2704
  socket?.close();
2597
2705
  } catch {
2598
2706
  }
2599
- resolve11(healthy);
2707
+ resolve13(healthy);
2600
2708
  };
2601
2709
  const timer = setTimeout(() => finish(false), timeoutMs);
2602
2710
  try {
@@ -2920,7 +3028,11 @@ function logFilePath(stateDir, instanceId) {
2920
3028
  function runtimeHeartbeatFilePath(runtimeStateDir) {
2921
3029
  return path13.join(runtimeStateDir, "heartbeat.json");
2922
3030
  }
2923
- function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
3031
+ function runtimeThreadStateFilePath(runtimeStateDir) {
3032
+ return path13.join(runtimeStateDir, "thread.json");
3033
+ }
3034
+ function loadRuntimeBridgeHeartbeat(bridgeState) {
3035
+ const runtimeStateDir = bridgeState?.runtimeStateDir;
2924
3036
  if (!runtimeStateDir) {
2925
3037
  return null;
2926
3038
  }
@@ -2929,13 +3041,35 @@ function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
2929
3041
  return null;
2930
3042
  }
2931
3043
  try {
2932
- const raw = fs13.readFileSync(heartbeatPath, "utf-8");
2933
- const parsed = JSON.parse(raw);
2934
- return typeof parsed.updatedAt === "string" ? parsed.updatedAt : null;
3044
+ return JSON.parse(
3045
+ fs13.readFileSync(heartbeatPath, "utf-8")
3046
+ );
3047
+ } catch {
3048
+ return null;
3049
+ }
3050
+ }
3051
+ function loadRuntimeBridgeThreadState(bridgeState) {
3052
+ const runtimeStateDir = bridgeState?.runtimeStateDir;
3053
+ if (!runtimeStateDir) {
3054
+ return null;
3055
+ }
3056
+ const threadPath = runtimeThreadStateFilePath(runtimeStateDir);
3057
+ if (!fs13.existsSync(threadPath)) {
3058
+ return null;
3059
+ }
3060
+ try {
3061
+ const parsed = JSON.parse(
3062
+ fs13.readFileSync(threadPath, "utf-8")
3063
+ );
3064
+ return parsed.threadId ? parsed : null;
2935
3065
  } catch {
2936
3066
  return null;
2937
3067
  }
2938
3068
  }
3069
+ function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
3070
+ const heartbeat = loadRuntimeBridgeHeartbeat({ runtimeStateDir });
3071
+ return typeof heartbeat?.updatedAt === "string" ? heartbeat.updatedAt : null;
3072
+ }
2939
3073
  function resolveHeartbeatTimestamp(state) {
2940
3074
  return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
2941
3075
  }
@@ -3105,6 +3239,7 @@ async function startBridge(options) {
3105
3239
  options.messageLookbackMinutes
3106
3240
  )
3107
3241
  } : {},
3242
+ ...process.env.TAP_COLD_START_WARMUP === "true" ? { TAP_COLD_START_WARMUP: "true" } : {},
3108
3243
  ...options.threadId ? { TAP_THREAD_ID: options.threadId } : {},
3109
3244
  ...options.ephemeral ? { TAP_EPHEMERAL: "true" } : {},
3110
3245
  ...options.processExistingMessages ? { TAP_PROCESS_EXISTING: "true" } : {}
@@ -3238,10 +3373,10 @@ function getBridgeStatus(stateDir, instanceId) {
3238
3373
  // src/commands/add.ts
3239
3374
  var ADD_HELP = `
3240
3375
  Usage:
3241
- tap-comms add <claude|codex|gemini> [options]
3376
+ tap add <claude|codex|gemini> [options]
3242
3377
 
3243
3378
  Description:
3244
- Install a runtime instance and configure it to use tap-comms.
3379
+ Install a runtime instance and configure it to use tap.
3245
3380
 
3246
3381
  Options:
3247
3382
  --name <name> Instance name (default: runtime name)
@@ -3598,7 +3733,7 @@ async function addCommand(args) {
3598
3733
  // src/commands/status.ts
3599
3734
  var STATUS_HELP = `
3600
3735
  Usage:
3601
- tap-comms status
3736
+ tap status
3602
3737
 
3603
3738
  Description:
3604
3739
  Show all installed instances, their bridge status, and configuration info.
@@ -3874,7 +4009,7 @@ function cleanEmptyParents(obj, keyPath) {
3874
4009
  // src/commands/remove.ts
3875
4010
  var REMOVE_HELP = `
3876
4011
  Usage:
3877
- tap-comms remove <instance>
4012
+ tap remove <instance>
3878
4013
 
3879
4014
  Description:
3880
4015
  Remove a registered instance, stop its bridge, and rollback config changes.
@@ -4003,7 +4138,7 @@ function formatAge(seconds) {
4003
4138
  }
4004
4139
  var BRIDGE_HELP = `
4005
4140
  Usage:
4006
- tap-comms bridge <subcommand> [instance] [options]
4141
+ tap bridge <subcommand> [instance] [options]
4007
4142
 
4008
4143
  Subcommands:
4009
4144
  start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
@@ -4061,6 +4196,21 @@ function redactProtectedUrl(url) {
4061
4196
  function loadCurrentBridgeState(stateDir, instanceId, fallback) {
4062
4197
  return loadBridgeState(stateDir, instanceId) ?? fallback ?? null;
4063
4198
  }
4199
+ function formatThreadSummary(threadId, cwd) {
4200
+ if (!threadId) {
4201
+ return "-";
4202
+ }
4203
+ return cwd ? `${threadId} (${cwd})` : threadId;
4204
+ }
4205
+ function normalizeComparablePath(value) {
4206
+ return path14.resolve(value).replace(/\\/g, "/").toLowerCase();
4207
+ }
4208
+ function sameOptionalPath(left, right) {
4209
+ if (!left || !right) {
4210
+ return left === right;
4211
+ }
4212
+ return normalizeComparablePath(left) === normalizeComparablePath(right);
4213
+ }
4064
4214
  function getSharedAppServerUsers(state, stateDir, currentInstanceId, appServerUrl) {
4065
4215
  const shared = [];
4066
4216
  for (const [id, inst] of Object.entries(state.instances)) {
@@ -4673,12 +4823,18 @@ function bridgeStatusAll() {
4673
4823
  pid: null,
4674
4824
  port: inst.port,
4675
4825
  lastHeartbeat: null,
4826
+ threadId: null,
4827
+ threadCwd: null,
4828
+ savedThreadId: null,
4829
+ savedThreadCwd: null,
4676
4830
  appServer: null
4677
4831
  };
4678
4832
  continue;
4679
4833
  }
4680
4834
  const status = getBridgeStatus(stateDir, instanceId);
4681
4835
  const bridgeState = loadBridgeState(stateDir, instanceId);
4836
+ const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
4837
+ const savedThread = loadRuntimeBridgeThreadState(bridgeState);
4682
4838
  const age = getHeartbeatAge(stateDir, instanceId);
4683
4839
  const pid = bridgeState?.pid ?? null;
4684
4840
  const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
@@ -4700,12 +4856,26 @@ function bridgeStatusAll() {
4700
4856
  );
4701
4857
  }
4702
4858
  }
4859
+ if (runtimeHeartbeat?.threadId) {
4860
+ log(
4861
+ ` Thread: ${formatThreadSummary(runtimeHeartbeat.threadId, runtimeHeartbeat.threadCwd)}`
4862
+ );
4863
+ }
4864
+ if (savedThread?.threadId && (savedThread.threadId !== runtimeHeartbeat?.threadId || !sameOptionalPath(savedThread.cwd, runtimeHeartbeat?.threadCwd))) {
4865
+ log(
4866
+ ` Saved: ${formatThreadSummary(savedThread.threadId, savedThread.cwd)}`
4867
+ );
4868
+ }
4703
4869
  bridges[instanceId] = {
4704
4870
  status,
4705
4871
  runtime: inst.runtime,
4706
4872
  pid,
4707
4873
  port: inst.port,
4708
4874
  lastHeartbeat: heartbeat,
4875
+ threadId: runtimeHeartbeat?.threadId ?? null,
4876
+ threadCwd: runtimeHeartbeat?.threadCwd ?? null,
4877
+ savedThreadId: savedThread?.threadId ?? null,
4878
+ savedThreadCwd: savedThread?.cwd ?? null,
4709
4879
  appServer: bridgeState?.appServer ?? null
4710
4880
  };
4711
4881
  }
@@ -4781,6 +4951,10 @@ function bridgeStatusOne(identifier) {
4781
4951
  pid: null,
4782
4952
  port: inst.port,
4783
4953
  lastHeartbeat: null,
4954
+ threadId: null,
4955
+ threadCwd: null,
4956
+ savedThreadId: null,
4957
+ savedThreadCwd: null,
4784
4958
  appServer: null
4785
4959
  }
4786
4960
  };
@@ -4789,6 +4963,8 @@ function bridgeStatusOne(identifier) {
4789
4963
  const stateDir = resolvedCfg2.stateDir;
4790
4964
  const status = getBridgeStatus(stateDir, instanceId);
4791
4965
  const bridgeState = loadBridgeState(stateDir, instanceId);
4966
+ const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
4967
+ const savedThread = loadRuntimeBridgeThreadState(bridgeState);
4792
4968
  const age = getHeartbeatAge(stateDir, instanceId);
4793
4969
  const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
4794
4970
  log(`Status: ${status}`);
@@ -4797,6 +4973,16 @@ function bridgeStatusOne(identifier) {
4797
4973
  log(
4798
4974
  `Heartbeat: ${heartbeat ?? "-"}${age !== null ? ` (${formatAge(age)})` : ""}`
4799
4975
  );
4976
+ if (runtimeHeartbeat?.threadId) {
4977
+ log(
4978
+ `Thread: ${formatThreadSummary(runtimeHeartbeat.threadId, runtimeHeartbeat.threadCwd)}`
4979
+ );
4980
+ }
4981
+ if (savedThread?.threadId && (savedThread.threadId !== runtimeHeartbeat?.threadId || !sameOptionalPath(savedThread.cwd, runtimeHeartbeat?.threadCwd))) {
4982
+ log(
4983
+ `Saved: ${formatThreadSummary(savedThread.threadId, savedThread.cwd)}`
4984
+ );
4985
+ }
4800
4986
  log(
4801
4987
  `Log: ${path14.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
4802
4988
  );
@@ -4845,6 +5031,10 @@ function bridgeStatusOne(identifier) {
4845
5031
  pid: bridgeState?.pid ?? null,
4846
5032
  port: inst.port,
4847
5033
  lastHeartbeat: heartbeat,
5034
+ threadId: runtimeHeartbeat?.threadId ?? null,
5035
+ threadCwd: runtimeHeartbeat?.threadCwd ?? null,
5036
+ savedThreadId: savedThread?.threadId ?? null,
5037
+ savedThreadCwd: savedThread?.cwd ?? null,
4848
5038
  appServer: bridgeState?.appServer ?? null
4849
5039
  }
4850
5040
  };
@@ -5214,7 +5404,7 @@ function collectDashboardSnapshot(repoRoot, commsDirOverride) {
5214
5404
  // src/commands/up.ts
5215
5405
  var UP_HELP = `
5216
5406
  Usage:
5217
- tap-comms up [bridge-start options]
5407
+ tap up [bridge-start options]
5218
5408
 
5219
5409
  Description:
5220
5410
  Start all registered app-server bridge daemons with one command.
@@ -5238,7 +5428,18 @@ async function upCommand(args) {
5238
5428
  };
5239
5429
  }
5240
5430
  const repoRoot = findRepoRoot();
5241
- const result = await bridgeCommand(["start", "--all", ...args]);
5431
+ const previousColdStartWarmup = process.env.TAP_COLD_START_WARMUP;
5432
+ process.env.TAP_COLD_START_WARMUP = "true";
5433
+ let result;
5434
+ try {
5435
+ result = await bridgeCommand(["start", "--all", ...args]);
5436
+ } finally {
5437
+ if (previousColdStartWarmup === void 0) {
5438
+ delete process.env.TAP_COLD_START_WARMUP;
5439
+ } else {
5440
+ process.env.TAP_COLD_START_WARMUP = previousColdStartWarmup;
5441
+ }
5442
+ }
5242
5443
  const snapshot = collectDashboardSnapshot(repoRoot);
5243
5444
  const activeBridges = snapshot.bridges.filter(
5244
5445
  (bridge) => bridge.status === "running"
@@ -5269,7 +5470,7 @@ async function upCommand(args) {
5269
5470
  // src/commands/down.ts
5270
5471
  var DOWN_HELP = `
5271
5472
  Usage:
5272
- tap-comms down
5473
+ tap down
5273
5474
 
5274
5475
  Description:
5275
5476
  Stop all running bridge daemons and managed app-servers.
@@ -5320,10 +5521,10 @@ import * as path16 from "path";
5320
5521
  import { spawn as spawn2 } from "child_process";
5321
5522
  var SERVE_HELP = `
5322
5523
  Usage:
5323
- tap-comms serve [options]
5524
+ tap serve [options]
5324
5525
 
5325
5526
  Description:
5326
- Start the tap-comms MCP server over stdio. This command takes over the
5527
+ Start the tap MCP server over stdio. This command takes over the
5327
5528
  process \u2014 it is intended to be launched by an MCP host (e.g. Claude Code).
5328
5529
 
5329
5530
  Options:
@@ -5393,9 +5594,9 @@ async function serveCommand(args) {
5393
5594
  TAP_COMMS_DIR: commsDir
5394
5595
  }
5395
5596
  });
5396
- return new Promise((resolve11) => {
5597
+ return new Promise((resolve13) => {
5397
5598
  child.on("error", (err) => {
5398
- resolve11({
5599
+ resolve13({
5399
5600
  ok: false,
5400
5601
  command: "serve",
5401
5602
  code: "TAP_INTERNAL_ERROR",
@@ -5405,7 +5606,7 @@ async function serveCommand(args) {
5405
5606
  });
5406
5607
  });
5407
5608
  child.on("exit", (code) => {
5408
- resolve11({
5609
+ resolve13({
5409
5610
  ok: code === 0,
5410
5611
  command: "serve",
5411
5612
  code: code === 0 ? "TAP_SERVE_OK" : "TAP_INTERNAL_ERROR",
@@ -5423,7 +5624,7 @@ import * as path17 from "path";
5423
5624
  import { execSync as execSync5 } from "child_process";
5424
5625
  var INIT_WORKTREE_HELP = `
5425
5626
  Usage:
5426
- tap-comms init-worktree [options]
5627
+ tap init-worktree [options]
5427
5628
 
5428
5629
  Options:
5429
5630
  --path <dir> Worktree directory (required, e.g. ../hua-wt-3)
@@ -5606,7 +5807,7 @@ function step4GenerateMcpJson(opts, warnings) {
5606
5807
  );
5607
5808
  const mcpConfig = {
5608
5809
  mcpServers: {
5609
- "tap-comms": {
5810
+ tap: {
5610
5811
  command: bunAbs,
5611
5812
  args: [channelEntry],
5612
5813
  cwd: wtAbs,
@@ -5870,7 +6071,7 @@ function renderSnapshot(snapshot) {
5870
6071
  }
5871
6072
  var DASHBOARD_HELP = `
5872
6073
  Usage:
5873
- tap-comms dashboard [options]
6074
+ tap dashboard [options]
5874
6075
 
5875
6076
  Description:
5876
6077
  Display a unified ops dashboard: agents, bridges, PRs, and warnings.
@@ -5959,14 +6160,150 @@ import {
5959
6160
  mkdirSync as mkdirSync10,
5960
6161
  readdirSync as readdirSync4,
5961
6162
  readFileSync as readFileSync14,
6163
+ renameSync as renameSync10,
5962
6164
  statSync as statSync2,
5963
- unlinkSync as unlinkSync3
6165
+ unlinkSync as unlinkSync3,
6166
+ writeFileSync as writeFileSync12
5964
6167
  } from "fs";
5965
- import { execSync as execSync6 } from "child_process";
5966
- import { join as join17 } from "path";
6168
+ import { homedir as homedir3 } from "os";
6169
+ import { spawnSync as spawnSync4 } from "child_process";
6170
+ import { dirname as dirname11, join as join17, resolve as resolve12 } from "path";
5967
6171
  var PASS = "pass";
5968
6172
  var WARN = "warn";
5969
6173
  var FAIL = "fail";
6174
+ var CODEX_ENV_DRIFT_KEYS = [
6175
+ "TAP_COMMS_DIR",
6176
+ "TAP_STATE_DIR",
6177
+ "TAP_REPO_ROOT"
6178
+ ];
6179
+ function normalizeComparablePath2(value) {
6180
+ return resolve12(value).replace(/\\/g, "/").toLowerCase();
6181
+ }
6182
+ function samePath(left, right) {
6183
+ return normalizeComparablePath2(left) === normalizeComparablePath2(right);
6184
+ }
6185
+ function looksLikePathToken(value) {
6186
+ return /^[A-Za-z]:[\\/]/.test(value) || value.startsWith("/") || value.startsWith("\\") || value.startsWith(".") || value.includes("/") || value.includes("\\");
6187
+ }
6188
+ function sameCommandToken(left, right) {
6189
+ return looksLikePathToken(left) || looksLikePathToken(right) ? samePath(left, right) : left === right;
6190
+ }
6191
+ function sameStringArray(left, right) {
6192
+ return left.length === right.length && left.every((value, index) => sameCommandToken(value, right[index] ?? ""));
6193
+ }
6194
+ function appendWarningMessage(message, extra) {
6195
+ return message.includes(extra) ? message : `${message}; ${extra}`;
6196
+ }
6197
+ function findCodexConfigPath3() {
6198
+ return join17(homedir3(), ".codex", "config.toml");
6199
+ }
6200
+ function canonicalizeTrustPath3(targetPath) {
6201
+ let resolved = resolve12(targetPath).replace(/\//g, "\\");
6202
+ const driveRoot = /^[A-Za-z]:\\$/;
6203
+ if (!driveRoot.test(resolved)) {
6204
+ resolved = resolved.replace(/\\+$/g, "");
6205
+ }
6206
+ return resolved.startsWith("\\\\?\\") ? resolved : `\\\\?\\${resolved}`;
6207
+ }
6208
+ function trustSelector2(targetPath) {
6209
+ return `projects.'${canonicalizeTrustPath3(targetPath)}'`;
6210
+ }
6211
+ function writeTomlAtomically(filePath, content) {
6212
+ const dir = dirname11(filePath);
6213
+ mkdirSync10(dir, { recursive: true });
6214
+ const tmp = `${filePath}.tmp.${process.pid}`;
6215
+ writeFileSync12(tmp, content, "utf-8");
6216
+ renameSync10(tmp, filePath);
6217
+ }
6218
+ function hasInstalledCodexInstance(state) {
6219
+ return !!state ? Object.values(state.instances).some(
6220
+ (instance2) => instance2.runtime === "codex" && instance2.installed
6221
+ ) : false;
6222
+ }
6223
+ function getCodexTrustTargets(repoRoot) {
6224
+ return [...new Set([repoRoot, process.cwd()].map((value) => resolve12(value)))];
6225
+ }
6226
+ function buildCodexDoctorSpec(repoRoot, commsDir) {
6227
+ const state = loadState(repoRoot);
6228
+ if (!hasInstalledCodexInstance(state)) {
6229
+ return null;
6230
+ }
6231
+ const ctx = createAdapterContext(commsDir, repoRoot);
6232
+ const managed = buildManagedMcpServerSpec(ctx);
6233
+ return {
6234
+ configPath: findCodexConfigPath3(),
6235
+ trustTargets: getCodexTrustTargets(repoRoot),
6236
+ managed
6237
+ };
6238
+ }
6239
+ function repairCodexConfig(repoRoot, commsDir) {
6240
+ const spec = buildCodexDoctorSpec(repoRoot, commsDir);
6241
+ if (!spec) {
6242
+ throw new Error("No installed Codex instance found in tap state.");
6243
+ }
6244
+ if (!spec.managed.command || spec.managed.issues.length > 0) {
6245
+ throw new Error(
6246
+ spec.managed.issues[0] ?? "Unable to resolve the managed tap MCP server for Codex."
6247
+ );
6248
+ }
6249
+ const existingContent = existsSync16(spec.configPath) ? readFileSync14(spec.configPath, "utf-8") : "";
6250
+ const existingTapEnvTable = extractTomlTable(existingContent, "mcp_servers.tap.env");
6251
+ const existingLegacyEnvTable = extractTomlTable(
6252
+ existingContent,
6253
+ "mcp_servers.tap-comms.env"
6254
+ );
6255
+ const preservedEnv = parseTomlAssignments(
6256
+ existingTapEnvTable ?? existingLegacyEnvTable ?? ""
6257
+ );
6258
+ const repairedEnv = {
6259
+ ...preservedEnv,
6260
+ ...Object.fromEntries(
6261
+ CODEX_ENV_DRIFT_KEYS.map((key) => [key, spec.managed.env[key]])
6262
+ )
6263
+ };
6264
+ let nextContent = existingContent;
6265
+ if (extractTomlTable(nextContent, "mcp_servers.tap-comms.env")) {
6266
+ nextContent = removeTomlTable(nextContent, "mcp_servers.tap-comms.env");
6267
+ }
6268
+ if (extractTomlTable(nextContent, "mcp_servers.tap-comms")) {
6269
+ nextContent = removeTomlTable(nextContent, "mcp_servers.tap-comms");
6270
+ }
6271
+ nextContent = replaceTomlTable(
6272
+ nextContent,
6273
+ "mcp_servers.tap",
6274
+ renderTomlTable(
6275
+ "mcp_servers.tap",
6276
+ {
6277
+ command: spec.managed.command,
6278
+ args: spec.managed.args
6279
+ },
6280
+ extractTomlTable(existingContent, "mcp_servers.tap")
6281
+ )
6282
+ );
6283
+ nextContent = replaceTomlTable(
6284
+ nextContent,
6285
+ "mcp_servers.tap.env",
6286
+ renderTomlTable(
6287
+ "mcp_servers.tap.env",
6288
+ repairedEnv,
6289
+ existingTapEnvTable ?? existingLegacyEnvTable
6290
+ )
6291
+ );
6292
+ for (const trustTarget of spec.trustTargets) {
6293
+ const selector = trustSelector2(trustTarget);
6294
+ nextContent = replaceTomlTable(
6295
+ nextContent,
6296
+ selector,
6297
+ renderTomlTable(
6298
+ selector,
6299
+ { trust_level: "trusted" },
6300
+ extractTomlTable(existingContent, selector)
6301
+ )
6302
+ );
6303
+ }
6304
+ writeTomlAtomically(spec.configPath, nextContent);
6305
+ return `Repaired Codex config at ${spec.configPath}. Restart Codex to reload MCP settings.`;
6306
+ }
5970
6307
  function countFiles(dir, ext = ".md") {
5971
6308
  if (!existsSync16(dir)) return 0;
5972
6309
  try {
@@ -5991,21 +6328,6 @@ function recentFileCount(dir, withinMs) {
5991
6328
  }
5992
6329
  return count;
5993
6330
  }
5994
- function loadBridgeRuntimeHeartbeat(bridgeState) {
5995
- const runtimeStateDir = bridgeState?.runtimeStateDir;
5996
- if (!runtimeStateDir) {
5997
- return null;
5998
- }
5999
- const heartbeatPath = join17(runtimeStateDir, "heartbeat.json");
6000
- if (!existsSync16(heartbeatPath)) {
6001
- return null;
6002
- }
6003
- try {
6004
- return JSON.parse(readFileSync14(heartbeatPath, "utf-8"));
6005
- } catch {
6006
- return null;
6007
- }
6008
- }
6009
6331
  function checkComms(commsDir) {
6010
6332
  const checks = [];
6011
6333
  checks.push({
@@ -6089,7 +6411,8 @@ function checkInstances(repoRoot, stateDir) {
6089
6411
  const running = isBridgeRunning(stateDir, id);
6090
6412
  const bridgeState = loadBridgeState(stateDir, id);
6091
6413
  const heartbeatAge = getHeartbeatAge(stateDir, id);
6092
- const runtimeHeartbeat = loadBridgeRuntimeHeartbeat(bridgeState);
6414
+ const runtimeHeartbeat = loadRuntimeBridgeHeartbeat(bridgeState);
6415
+ const savedThread = loadRuntimeBridgeThreadState(bridgeState);
6093
6416
  let status;
6094
6417
  let message;
6095
6418
  let fix;
@@ -6135,9 +6458,30 @@ function checkInstances(repoRoot, stateDir) {
6135
6458
  }
6136
6459
  const lastRuntimeError = runtimeHeartbeat?.lastError?.trim();
6137
6460
  if (lastRuntimeError) {
6138
- status = status === FAIL ? FAIL : WARN;
6461
+ status = WARN;
6139
6462
  message = `${message}; bridge last error: ${lastRuntimeError}`;
6140
6463
  }
6464
+ if (savedThread?.threadId && savedThread.cwd && !samePath(savedThread.cwd, repoRoot)) {
6465
+ status = WARN;
6466
+ message = appendWarningMessage(
6467
+ message,
6468
+ `saved thread cwd mismatch (${savedThread.cwd})`
6469
+ );
6470
+ }
6471
+ if (runtimeHeartbeat?.threadId && savedThread?.threadId && runtimeHeartbeat.threadId !== savedThread.threadId) {
6472
+ status = WARN;
6473
+ message = appendWarningMessage(
6474
+ message,
6475
+ `saved thread ${savedThread.threadId} differs from active thread ${runtimeHeartbeat.threadId}`
6476
+ );
6477
+ }
6478
+ if (runtimeHeartbeat?.threadCwd && !samePath(runtimeHeartbeat.threadCwd, repoRoot)) {
6479
+ status = WARN;
6480
+ message = appendWarningMessage(
6481
+ message,
6482
+ `active thread cwd mismatch (${runtimeHeartbeat.threadCwd})`
6483
+ );
6484
+ }
6141
6485
  checks.push({ name: `bridge: ${id}`, status, message, fix });
6142
6486
  } else {
6143
6487
  checks.push({
@@ -6210,15 +6554,25 @@ function checkMcpServer(repoRoot) {
6210
6554
  });
6211
6555
  return checks;
6212
6556
  }
6213
- const hasTapComms = config?.mcpServers?.["tap-comms"];
6214
- if (!hasTapComms) {
6557
+ const mcpServers = config?.mcpServers;
6558
+ const hasTap = mcpServers?.["tap"];
6559
+ const hasOldKey = mcpServers?.["tap-comms"];
6560
+ if (hasOldKey) {
6561
+ checks.push({
6562
+ name: "MCP config (.mcp.json)",
6563
+ status: WARN,
6564
+ message: 'Legacy "tap-comms" key found. Run "tap add claude" to migrate to the new "tap" key.'
6565
+ });
6566
+ }
6567
+ if (!hasTap && !hasOldKey) {
6215
6568
  checks.push({
6216
6569
  name: "MCP config (.mcp.json)",
6217
6570
  status: WARN,
6218
- message: "tap-comms not configured"
6571
+ message: "tap not configured"
6219
6572
  });
6220
6573
  return checks;
6221
6574
  }
6575
+ const hasTapComms = hasTap ?? hasOldKey;
6222
6576
  checks.push({
6223
6577
  name: "MCP config (.mcp.json)",
6224
6578
  status: PASS,
@@ -6229,11 +6583,12 @@ function checkMcpServer(repoRoot) {
6229
6583
  let cmdAvailable = existsSync16(cmd);
6230
6584
  if (!cmdAvailable) {
6231
6585
  try {
6232
- execSync6(`"${cmd}" --version`, {
6586
+ const result = spawnSync4(cmd, ["--version"], {
6233
6587
  stdio: "pipe",
6234
- timeout: 5e3
6588
+ timeout: 5e3,
6589
+ shell: process.platform === "win32"
6235
6590
  });
6236
- cmdAvailable = true;
6591
+ cmdAvailable = result.status === 0;
6237
6592
  } catch {
6238
6593
  }
6239
6594
  }
@@ -6292,6 +6647,88 @@ function checkMcpServer(repoRoot) {
6292
6647
  });
6293
6648
  return checks;
6294
6649
  }
6650
+ function checkCodexConfig(repoRoot, commsDir) {
6651
+ const spec = buildCodexDoctorSpec(repoRoot, commsDir);
6652
+ if (!spec) {
6653
+ return [];
6654
+ }
6655
+ const checks = [];
6656
+ const fixHint = 'Run "tap doctor --fix" or "tap add codex --force".';
6657
+ if (!existsSync16(spec.configPath)) {
6658
+ checks.push({
6659
+ name: "MCP config (~/.codex/config.toml)",
6660
+ status: WARN,
6661
+ message: `${spec.configPath} not found. ${fixHint}`,
6662
+ fix: () => repairCodexConfig(repoRoot, commsDir)
6663
+ });
6664
+ return checks;
6665
+ }
6666
+ const content = readFileSync14(spec.configPath, "utf-8");
6667
+ const tapTable = extractTomlTable(content, "mcp_servers.tap");
6668
+ const tapEnvTable = extractTomlTable(content, "mcp_servers.tap.env");
6669
+ const legacyTable = extractTomlTable(content, "mcp_servers.tap-comms");
6670
+ const legacyEnvTable = extractTomlTable(content, "mcp_servers.tap-comms.env");
6671
+ const selectedMain = parseTomlAssignments(tapTable ?? "");
6672
+ const selectedEnv = parseTomlAssignments(
6673
+ tapEnvTable ?? legacyEnvTable ?? ""
6674
+ );
6675
+ const issues = [];
6676
+ if (legacyTable || legacyEnvTable) {
6677
+ issues.push('legacy "tap-comms" key present');
6678
+ }
6679
+ if (!tapTable && !legacyTable) {
6680
+ issues.push("tap MCP table missing");
6681
+ }
6682
+ if (!tapEnvTable && !legacyEnvTable) {
6683
+ issues.push("tap MCP env table missing");
6684
+ }
6685
+ if (tapTable && spec.managed.command) {
6686
+ const actualCommand = selectedMain.command;
6687
+ if (typeof actualCommand !== "string") {
6688
+ issues.push("tap MCP command missing");
6689
+ } else if (!sameCommandToken(actualCommand, spec.managed.command)) {
6690
+ issues.push(`tap MCP command drift (${actualCommand})`);
6691
+ }
6692
+ const actualArgs = selectedMain.args;
6693
+ if (!Array.isArray(actualArgs)) {
6694
+ issues.push("tap MCP args missing");
6695
+ } else if (!sameStringArray(actualArgs, spec.managed.args)) {
6696
+ issues.push(`tap MCP args drift (${JSON.stringify(actualArgs)})`);
6697
+ }
6698
+ }
6699
+ for (const key of CODEX_ENV_DRIFT_KEYS) {
6700
+ const expected = spec.managed.env[key];
6701
+ const actual = selectedEnv[key];
6702
+ if (typeof actual !== "string") {
6703
+ issues.push(`${key} missing`);
6704
+ continue;
6705
+ }
6706
+ if (!samePath(actual, expected)) {
6707
+ issues.push(`${key} drift (${actual})`);
6708
+ }
6709
+ }
6710
+ for (const trustTarget of spec.trustTargets) {
6711
+ const trustTable = extractTomlTable(content, trustSelector2(trustTarget));
6712
+ if (!trustTable || !trustTable.includes('trust_level = "trusted"')) {
6713
+ issues.push(`missing trust for ${trustTarget}`);
6714
+ }
6715
+ }
6716
+ if (issues.length === 0) {
6717
+ checks.push({
6718
+ name: "MCP config (~/.codex/config.toml)",
6719
+ status: PASS,
6720
+ message: spec.configPath
6721
+ });
6722
+ return checks;
6723
+ }
6724
+ checks.push({
6725
+ name: "MCP config (~/.codex/config.toml)",
6726
+ status: WARN,
6727
+ message: `${issues.join("; ")}. ${fixHint}`,
6728
+ fix: () => repairCodexConfig(repoRoot, commsDir)
6729
+ });
6730
+ return checks;
6731
+ }
6295
6732
  function checkBridgeTurnHealth(repoRoot) {
6296
6733
  const checks = [];
6297
6734
  const tmpDir = join17(repoRoot, ".tmp");
@@ -6408,7 +6845,7 @@ function renderCheck(check, fixMode) {
6408
6845
  }
6409
6846
  var DOCTOR_HELP = `
6410
6847
  Usage:
6411
- tap-comms doctor [options]
6848
+ tap doctor [options]
6412
6849
 
6413
6850
  Description:
6414
6851
  Diagnose tap infrastructure health: comms directory, instances, bridges,
@@ -6456,6 +6893,7 @@ async function doctorCommand(args) {
6456
6893
  checks.push(...checkInstances(repoRoot, config.stateDir));
6457
6894
  checks.push(...checkMessageLifecycle(commsDir));
6458
6895
  checks.push(...checkMcpServer(repoRoot));
6896
+ checks.push(...checkCodexConfig(repoRoot, commsDir));
6459
6897
  checks.push(...checkBridgeTurnHealth(repoRoot));
6460
6898
  return checks;
6461
6899
  }
@@ -6547,12 +6985,12 @@ async function doctorCommand(args) {
6547
6985
  }
6548
6986
 
6549
6987
  // src/commands/comms.ts
6550
- import { execSync as execSync7, spawnSync as spawnSync4 } from "child_process";
6988
+ import { execSync as execSync6, spawnSync as spawnSync5 } from "child_process";
6551
6989
  import * as fs17 from "fs";
6552
6990
  import * as path18 from "path";
6553
6991
  var COMMS_HELP = `
6554
6992
  Usage:
6555
- tap-comms comms <subcommand>
6993
+ tap comms <subcommand>
6556
6994
 
6557
6995
  Subcommands:
6558
6996
  pull Pull latest changes from comms remote repo
@@ -6579,7 +7017,7 @@ function commsPull(commsDir) {
6579
7017
  };
6580
7018
  }
6581
7019
  try {
6582
- const output = execSync7("git pull --rebase", {
7020
+ const output = execSync6("git pull --rebase", {
6583
7021
  cwd: commsDir,
6584
7022
  encoding: "utf-8",
6585
7023
  stdio: "pipe"
@@ -6621,8 +7059,8 @@ function commsPush(commsDir) {
6621
7059
  };
6622
7060
  }
6623
7061
  try {
6624
- execSync7("git add -A", { cwd: commsDir, stdio: "pipe" });
6625
- const status = execSync7("git status --porcelain", {
7062
+ execSync6("git add -A", { cwd: commsDir, stdio: "pipe" });
7063
+ const status = execSync6("git status --porcelain", {
6626
7064
  cwd: commsDir,
6627
7065
  encoding: "utf-8",
6628
7066
  stdio: "pipe"
@@ -6639,7 +7077,7 @@ function commsPush(commsDir) {
6639
7077
  };
6640
7078
  }
6641
7079
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6642
- const commitResult = spawnSync4(
7080
+ const commitResult = spawnSync5(
6643
7081
  "git",
6644
7082
  ["commit", "-m", `chore(comms): sync ${timestamp}`],
6645
7083
  { cwd: commsDir, stdio: "pipe", encoding: "utf-8" }
@@ -6655,7 +7093,7 @@ function commsPush(commsDir) {
6655
7093
  data: { commsDir }
6656
7094
  };
6657
7095
  }
6658
- execSync7("git push", { cwd: commsDir, stdio: "pipe" });
7096
+ execSync6("git push", { cwd: commsDir, stdio: "pipe" });
6659
7097
  logSuccess("Comms push complete");
6660
7098
  return {
6661
7099
  ok: true,
@@ -6739,16 +7177,61 @@ function extractJsonFlag(args) {
6739
7177
  return { jsonMode, cleanArgs };
6740
7178
  }
6741
7179
 
7180
+ // src/cli-suggest.ts
7181
+ var COMMANDS = [
7182
+ "init",
7183
+ "init-worktree",
7184
+ "add",
7185
+ "remove",
7186
+ "status",
7187
+ "bridge",
7188
+ "up",
7189
+ "down",
7190
+ "comms",
7191
+ "dashboard",
7192
+ "doctor",
7193
+ "serve",
7194
+ "version"
7195
+ ];
7196
+ function suggestCommand(input) {
7197
+ let best = null;
7198
+ let bestDist = Infinity;
7199
+ for (const cmd of COMMANDS) {
7200
+ const d = levenshtein(input.toLowerCase(), cmd);
7201
+ if (d < bestDist && d <= Math.max(2, Math.floor(cmd.length / 2))) {
7202
+ bestDist = d;
7203
+ best = cmd;
7204
+ }
7205
+ }
7206
+ return best;
7207
+ }
7208
+ function levenshtein(a, b) {
7209
+ const m = a.length;
7210
+ const n = b.length;
7211
+ const dp = Array.from(
7212
+ { length: m + 1 },
7213
+ () => Array.from({ length: n + 1 }).fill(0)
7214
+ );
7215
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
7216
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
7217
+ for (let i = 1; i <= m; i++) {
7218
+ for (let j = 1; j <= n; j++) {
7219
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
7220
+ }
7221
+ }
7222
+ return dp[m][n];
7223
+ }
7224
+
6742
7225
  // src/cli.ts
6743
7226
  var HELP = `
6744
7227
  @hua-labs/tap \u2014 Cross-model AI agent communication setup
6745
7228
 
6746
7229
  Usage:
6747
- tap-comms <command> [options]
7230
+ tap <command> [options]
6748
7231
 
6749
7232
  Commands:
6750
7233
  init Initialize comms directory and state
6751
- init-worktree Set up a new git worktree with tap-comms
7234
+ init-worktree Set up a new git worktree with tap
6752
7235
  add <runtime> Add a runtime instance (claude, codex, gemini)
6753
7236
  remove <instance> Remove an instance and rollback config
6754
7237
  status Show installed instances and bridge status
@@ -6758,7 +7241,7 @@ Commands:
6758
7241
  comms <pull|push> Sync comms directory with remote repo
6759
7242
  dashboard Show unified ops dashboard
6760
7243
  doctor Diagnose tap infrastructure health
6761
- serve Start tap-comms MCP server (stdio)
7244
+ serve Start tap MCP server (stdio)
6762
7245
  version Show version
6763
7246
 
6764
7247
  Options:
@@ -6859,15 +7342,21 @@ async function main() {
6859
7342
  process.exit(exitCode(serveResult));
6860
7343
  break;
6861
7344
  }
6862
- default:
7345
+ default: {
7346
+ const suggestion = suggestCommand(command);
7347
+ const hint = suggestion ? `
7348
+
7349
+ Did you mean: tap ${suggestion}?` : "\n\nRun tap --help for a list of commands.";
6863
7350
  result = {
6864
7351
  ok: false,
6865
7352
  command: "unknown",
6866
7353
  code: "TAP_INVALID_ARGUMENT",
6867
- message: `Unknown command: ${command}`,
7354
+ message: `Unknown command: ${command}${hint}`,
6868
7355
  warnings: [],
6869
- data: { requestedCommand: command }
7356
+ data: { requestedCommand: command, suggestion }
6870
7357
  };
7358
+ break;
7359
+ }
6871
7360
  }
6872
7361
  } catch (err) {
6873
7362
  const message = err instanceof Error ? err.message : String(err);