@hua-labs/tap 0.2.2 → 0.2.4

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
@@ -1,7 +1,7 @@
1
1
  // src/commands/init.ts
2
2
  import * as fs6 from "fs";
3
3
  import * as path6 from "path";
4
- import { execSync } from "child_process";
4
+ import { spawnSync } from "child_process";
5
5
 
6
6
  // src/state.ts
7
7
  import * as fs3 from "fs";
@@ -23,9 +23,16 @@ function detectPlatform() {
23
23
  return process.platform;
24
24
  }
25
25
  var _noGitWarned = false;
26
+ var _loggedWarnings = /* @__PURE__ */ new Set();
26
27
  function _setNoGitWarned() {
27
28
  _noGitWarned = true;
28
29
  }
30
+ function resetLoggedWarnings() {
31
+ _loggedWarnings.clear();
32
+ }
33
+ function wasWarningLogged(message) {
34
+ return _loggedWarnings.has(message);
35
+ }
29
36
  function findRepoRoot(startDir = process.cwd()) {
30
37
  let dir = path.resolve(startDir);
31
38
  while (true) {
@@ -33,8 +40,8 @@ function findRepoRoot(startDir = process.cwd()) {
33
40
  if (fs.existsSync(path.join(dir, "package.json"))) {
34
41
  if (!_noGitWarned) {
35
42
  _setNoGitWarned();
36
- logWarn(
37
- "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."
43
+ log(
44
+ "No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
38
45
  );
39
46
  }
40
47
  return dir;
@@ -45,8 +52,8 @@ function findRepoRoot(startDir = process.cwd()) {
45
52
  }
46
53
  if (!_noGitWarned) {
47
54
  _setNoGitWarned();
48
- logWarn(
49
- "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."
55
+ log(
56
+ "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."
50
57
  );
51
58
  }
52
59
  return process.cwd();
@@ -101,7 +108,9 @@ function logSuccess(message) {
101
108
  if (!_jsonMode) console.log(` + ${message}`);
102
109
  }
103
110
  function logWarn(message) {
104
- if (!_jsonMode) console.log(` ! ${message}`);
111
+ if (_jsonMode) return;
112
+ _loggedWarnings.add(message);
113
+ console.log(` ! ${message}`);
105
114
  }
106
115
  function logError(message) {
107
116
  if (!_jsonMode) console.error(` x ${message}`);
@@ -111,6 +120,16 @@ function logHeader(message) {
111
120
  ${message}
112
121
  `);
113
122
  }
123
+ function parseIntFlag(value, name, min, max) {
124
+ if (value === void 0) return void 0;
125
+ const parsed = Number(value);
126
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
127
+ throw new RangeError(
128
+ `Invalid ${name}: ${value}. Must be an integer between ${min} and ${max}.`
129
+ );
130
+ }
131
+ return parsed;
132
+ }
114
133
  function resolveInstanceId(identifier, state) {
115
134
  if (state.instances[identifier]) {
116
135
  return { ok: true, instanceId: identifier };
@@ -160,8 +179,8 @@ function findRepoRoot2(startDir = process.cwd()) {
160
179
  if (fs2.existsSync(path2.join(dir, "package.json"))) {
161
180
  if (!_noGitWarned) {
162
181
  _setNoGitWarned();
163
- console.error(
164
- "[tap] warning: No .git directory found. Resolved via package.json. Use --comms-dir to specify explicitly."
182
+ log(
183
+ "No .git directory found. Resolved tap root via package.json. That's fine outside git; use --comms-dir to choose a different comms location."
165
184
  );
166
185
  }
167
186
  return dir;
@@ -172,8 +191,8 @@ function findRepoRoot2(startDir = process.cwd()) {
172
191
  }
173
192
  if (!_noGitWarned) {
174
193
  _setNoGitWarned();
175
- console.error(
176
- "[tap] warning: No git repository found. Using cwd as root. Run 'git init' or use --comms-dir."
194
+ log(
195
+ "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."
177
196
  );
178
197
  }
179
198
  return process.cwd();
@@ -187,7 +206,7 @@ function loadJsonFile(filePath) {
187
206
  return null;
188
207
  }
189
208
  }
190
- function loadSharedConfig2(repoRoot) {
209
+ function loadSharedConfig(repoRoot) {
191
210
  return loadJsonFile(path2.join(repoRoot, SHARED_CONFIG_FILE));
192
211
  }
193
212
  function loadLocalConfig(repoRoot) {
@@ -211,7 +230,7 @@ function loadLegacyShellConfig(repoRoot) {
211
230
  }
212
231
  function resolveConfig(overrides = {}, startDir) {
213
232
  const repoRoot = findRepoRoot2(startDir);
214
- const shared = loadSharedConfig2(repoRoot) ?? {};
233
+ const shared = loadSharedConfig(repoRoot) ?? {};
215
234
  const local = loadLocalConfig(repoRoot) ?? {};
216
235
  const legacy = loadLegacyShellConfig(repoRoot) ?? {};
217
236
  const sources = {
@@ -754,7 +773,39 @@ function parsePermissionMode(args) {
754
773
  }
755
774
  return "safe";
756
775
  }
776
+ var INIT_HELP = `
777
+ Usage:
778
+ tap-comms init [options]
779
+
780
+ Description:
781
+ Initialize the tap-comms directory structure, state file, and permissions.
782
+ Optionally clone a shared comms repository.
783
+
784
+ Options:
785
+ --comms-dir <path> Override comms directory (default: tap-comms/)
786
+ --comms-repo <url> Clone a shared comms git repo into comms directory
787
+ --permissions <mode> Permission mode: safe (default) or full
788
+ --force Re-initialize even if already set up
789
+ --help, -h Show help
790
+
791
+ Examples:
792
+ npx @hua-labs/tap init
793
+ npx @hua-labs/tap init --permissions full
794
+ npx @hua-labs/tap init --comms-repo https://github.com/org/comms.git
795
+ npx @hua-labs/tap init --comms-dir /shared/comms --force
796
+ `.trim();
757
797
  async function initCommand(args) {
798
+ if (args.includes("--help") || args.includes("-h")) {
799
+ log(INIT_HELP);
800
+ return {
801
+ ok: true,
802
+ command: "init",
803
+ code: "TAP_NO_OP",
804
+ message: INIT_HELP,
805
+ warnings: [],
806
+ data: {}
807
+ };
808
+ }
758
809
  const repoRoot = findRepoRoot();
759
810
  const commsDir = resolveCommsDir(args, repoRoot);
760
811
  const permMode = parsePermissionMode(args);
@@ -791,10 +842,19 @@ async function initCommand(args) {
791
842
  } else {
792
843
  log(`Cloning comms repo: ${commsRepoUrl}`);
793
844
  try {
794
- execSync(`git clone "${commsRepoUrl}" "${commsDir}"`, {
795
- stdio: "pipe",
796
- encoding: "utf-8"
797
- });
845
+ const cloneResult = spawnSync(
846
+ "git",
847
+ ["clone", commsRepoUrl, commsDir],
848
+ {
849
+ stdio: "pipe",
850
+ encoding: "utf-8"
851
+ }
852
+ );
853
+ if (cloneResult.status !== 0) {
854
+ throw new Error(
855
+ cloneResult.stderr || `git clone exited with code ${cloneResult.status}`
856
+ );
857
+ }
798
858
  logSuccess(`Cloned comms repo to ${commsDir}`);
799
859
  } catch (err) {
800
860
  const msg = err instanceof Error ? err.message : String(err);
@@ -811,7 +871,7 @@ async function initCommand(args) {
811
871
  }
812
872
  }
813
873
  {
814
- const sharedConfig = loadSharedConfig2(repoRoot) ?? {};
874
+ const sharedConfig = loadSharedConfig(repoRoot) ?? {};
815
875
  let configChanged = false;
816
876
  if (commsRepoUrl) {
817
877
  sharedConfig.commsRepoUrl = commsRepoUrl;
@@ -903,17 +963,17 @@ ${entry}
903
963
  // src/adapters/claude.ts
904
964
  import * as fs8 from "fs";
905
965
  import * as path8 from "path";
906
- import { execSync as execSync2 } from "child_process";
966
+ import { execSync } from "child_process";
907
967
 
908
968
  // src/adapters/common.ts
909
969
  import * as fs7 from "fs";
910
970
  import * as os2 from "os";
911
971
  import * as path7 from "path";
912
- import { spawnSync } from "child_process";
972
+ import { spawnSync as spawnSync2 } from "child_process";
913
973
  import { fileURLToPath as fileURLToPath2 } from "url";
914
974
  function probeCommand(candidates) {
915
975
  for (const candidate of candidates) {
916
- const result = spawnSync(candidate, ["--version"], {
976
+ const result = spawnSync2(candidate, ["--version"], {
917
977
  encoding: "utf-8",
918
978
  shell: process.platform === "win32"
919
979
  });
@@ -991,7 +1051,7 @@ function findPreferredBunCommand() {
991
1051
  const candidates = process.platform === "win32" ? [path7.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path7.join(home, ".bun", "bin", "bun"), "bun"];
992
1052
  for (const candidate of candidates) {
993
1053
  if (path7.isAbsolute(candidate) && !fs7.existsSync(candidate)) continue;
994
- const result = spawnSync(candidate, ["--version"], {
1054
+ const result = spawnSync2(candidate, ["--version"], {
995
1055
  encoding: "utf-8",
996
1056
  shell: process.platform === "win32"
997
1057
  });
@@ -1022,18 +1082,18 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1022
1082
  return { command: null, args: [], env, sourcePath, warnings, issues };
1023
1083
  }
1024
1084
  const isBundled = sourcePath.endsWith(".mjs");
1085
+ const isEphemeralSource = isEphemeralPath(sourcePath);
1025
1086
  let command = bunCommand;
1026
1087
  let args = [toForwardSlashPath(sourcePath)];
1027
- if (!command && isBundled) {
1028
- const isEphemeralSource = isEphemeralPath(sourcePath);
1088
+ if (isEphemeralSource && isBundled) {
1089
+ command = "npx";
1090
+ args = ["@hua-labs/tap", "serve"];
1091
+ warnings.push(
1092
+ "Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
1093
+ );
1094
+ } else if (!command && isBundled) {
1029
1095
  const isEphemeralNode = isEphemeralPath(process.execPath);
1030
- if (isEphemeralSource) {
1031
- command = "npx";
1032
- args = ["@hua-labs/tap", "serve"];
1033
- warnings.push(
1034
- "Detected npx cache path. Using `npx @hua-labs/tap serve` as stable MCP launcher."
1035
- );
1036
- } else if (isEphemeralNode) {
1096
+ if (isEphemeralNode) {
1037
1097
  command = "node";
1038
1098
  warnings.push(
1039
1099
  "Detected ephemeral node path. Using `node` from PATH for MCP config stability."
@@ -1041,11 +1101,9 @@ function buildManagedMcpServerSpec(ctx, instanceId) {
1041
1101
  } else {
1042
1102
  command = toForwardSlashPath(process.execPath);
1043
1103
  }
1044
- if (!isEphemeralSource) {
1045
- warnings.push(
1046
- "bun not found; using node to run the compiled MCP server. Install bun for better performance."
1047
- );
1048
- }
1104
+ warnings.push(
1105
+ "bun not found; using node to run the compiled MCP server. Install bun for better performance."
1106
+ );
1049
1107
  }
1050
1108
  if (!command) {
1051
1109
  issues.push(
@@ -1070,7 +1128,7 @@ function findMcpJsonPath(ctx) {
1070
1128
  }
1071
1129
  function findClaudeCommand() {
1072
1130
  try {
1073
- execSync2("claude --version", { stdio: "pipe" });
1131
+ execSync("claude --version", { stdio: "pipe" });
1074
1132
  return "claude";
1075
1133
  } catch {
1076
1134
  return null;
@@ -1419,13 +1477,12 @@ function verifyManagedToml(content, ctx, configPath) {
1419
1477
  });
1420
1478
  }
1421
1479
  if (mainTable && managed.command) {
1480
+ const expectedArgs = managed.args.map((a) => `"${a.replace(/\\/g, "\\\\")}"`).join(", ");
1422
1481
  checks.push({
1423
1482
  name: "Managed command configured",
1424
1483
  passed: mainTable.includes(
1425
1484
  `command = "${managed.command.replace(/\\/g, "\\\\")}"`
1426
- ) && mainTable.includes(
1427
- `args = ["${managed.args[0]?.replace(/\\/g, "\\\\") ?? ""}"]`
1428
- ),
1485
+ ) && mainTable.includes(`args = [${expectedArgs}]`),
1429
1486
  message: "Managed tap-comms command/args do not match expected values"
1430
1487
  });
1431
1488
  }
@@ -1932,13 +1989,13 @@ import * as fs13 from "fs";
1932
1989
  import * as net from "net";
1933
1990
  import * as path13 from "path";
1934
1991
  import { randomBytes } from "crypto";
1935
- import { spawn, spawnSync as spawnSync2, execSync as execSync4 } from "child_process";
1992
+ import { spawn, spawnSync as spawnSync3, execSync as execSync3 } from "child_process";
1936
1993
  import { fileURLToPath as fileURLToPath4 } from "url";
1937
1994
 
1938
1995
  // src/runtime/resolve-node.ts
1939
1996
  import * as fs12 from "fs";
1940
1997
  import * as path12 from "path";
1941
- import { execSync as execSync3 } from "child_process";
1998
+ import { execSync as execSync2 } from "child_process";
1942
1999
  function readNodeVersion(repoRoot) {
1943
2000
  const nvFile = path12.join(repoRoot, ".node-version");
1944
2001
  if (!fs12.existsSync(nvFile)) return null;
@@ -1981,7 +2038,7 @@ function probeFnmNode(desiredVersion) {
1981
2038
  );
1982
2039
  if (!fs12.existsSync(candidate)) continue;
1983
2040
  try {
1984
- const v = execSync3(`"${candidate}" --version`, {
2041
+ const v = execSync2(`"${candidate}" --version`, {
1985
2042
  encoding: "utf-8",
1986
2043
  timeout: 5e3
1987
2044
  }).trim();
@@ -1995,7 +2052,7 @@ function probeFnmNode(desiredVersion) {
1995
2052
  }
1996
2053
  function detectNodeMajorVersion(command) {
1997
2054
  try {
1998
- const version2 = execSync3(`"${command}" --version`, {
2055
+ const version2 = execSync2(`"${command}" --version`, {
1999
2056
  encoding: "utf-8",
2000
2057
  timeout: 5e3
2001
2058
  }).trim();
@@ -2009,7 +2066,7 @@ function checkStripTypesSupport(command) {
2009
2066
  const major = detectNodeMajorVersion(command);
2010
2067
  if (major !== null && major >= 22) return true;
2011
2068
  try {
2012
- execSync3(`"${command}" --experimental-strip-types -e ""`, {
2069
+ execSync2(`"${command}" --experimental-strip-types -e ""`, {
2013
2070
  timeout: 5e3,
2014
2071
  stdio: "pipe"
2015
2072
  });
@@ -2100,7 +2157,7 @@ var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
2100
2157
  var APP_SERVER_START_TIMEOUT_MS = 2e4;
2101
2158
  var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
2102
2159
  var APP_SERVER_HEALTH_RETRY_MS = 250;
2103
- var APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
2160
+ var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
2104
2161
  var APP_SERVER_AUTH_FILE_MODE = 384;
2105
2162
  function appServerLogFilePath(stateDir, instanceId) {
2106
2163
  return path13.join(stateDir, "logs", `app-server-${instanceId}.log`);
@@ -2161,8 +2218,11 @@ function resolvePowerShellCommand() {
2161
2218
  function resolveAuthGatewayScript(repoRoot) {
2162
2219
  const moduleDir = path13.dirname(fileURLToPath4(import.meta.url));
2163
2220
  const candidates = [
2164
- path13.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.mjs"),
2165
- path13.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.ts"),
2221
+ // Bundled: dist/bridges/ sibling (npm install / built package)
2222
+ path13.join(moduleDir, "bridges", "codex-app-server-auth-gateway.mjs"),
2223
+ // Source: src/bridges/ sibling (monorepo dev with ts runner)
2224
+ path13.join(moduleDir, "bridges", "codex-app-server-auth-gateway.ts"),
2225
+ // Monorepo dist fallback
2166
2226
  path13.join(
2167
2227
  repoRoot,
2168
2228
  "packages",
@@ -2215,10 +2275,8 @@ async function allocateLoopbackPort(hostname) {
2215
2275
  });
2216
2276
  });
2217
2277
  }
2218
- function buildProtectedAppServerUrl(publicUrl, token) {
2219
- const url = new URL(publicUrl);
2220
- url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
2221
- return url.toString().replace(/\/(?=\?|$)/, "");
2278
+ function buildProtectedAppServerUrl(publicUrl, _token) {
2279
+ return publicUrl;
2222
2280
  }
2223
2281
  function readGatewayTokenFromPath(tokenPath) {
2224
2282
  return fs13.readFileSync(tokenPath, "utf8").trim();
@@ -2333,7 +2391,7 @@ async function createManagedAppServerAuth(options) {
2333
2391
  throw new Error("Failed to spawn app-server auth gateway");
2334
2392
  }
2335
2393
  return {
2336
- mode: "query-token",
2394
+ mode: "subprotocol",
2337
2395
  protectedUrl,
2338
2396
  upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
2339
2397
  tokenPath,
@@ -2447,7 +2505,7 @@ function findListeningProcessId(url, platform) {
2447
2505
  if (port == null || !Number.isFinite(port)) {
2448
2506
  return null;
2449
2507
  }
2450
- const result = spawnSync2(
2508
+ const result = spawnSync3(
2451
2509
  resolvePowerShellCommand(),
2452
2510
  [
2453
2511
  "-NoLogo",
@@ -2520,7 +2578,7 @@ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, e
2520
2578
  `Failed to find a free app-server port starting at ${basePort}`
2521
2579
  );
2522
2580
  }
2523
- async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
2581
+ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
2524
2582
  const WebSocket = getWebSocketCtor();
2525
2583
  if (!WebSocket) {
2526
2584
  return false;
@@ -2542,7 +2600,8 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
2542
2600
  };
2543
2601
  const timer = setTimeout(() => finish(false), timeoutMs);
2544
2602
  try {
2545
- socket = new WebSocket(url);
2603
+ const protocols = gatewayToken ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`] : void 0;
2604
+ socket = new WebSocket(url, protocols);
2546
2605
  socket.addEventListener("open", () => finish(true), { once: true });
2547
2606
  socket.addEventListener("error", () => finish(false), { once: true });
2548
2607
  socket.addEventListener("close", () => finish(false), { once: true });
@@ -2551,10 +2610,14 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
2551
2610
  }
2552
2611
  });
2553
2612
  }
2554
- async function waitForAppServerHealth(url, timeoutMs) {
2613
+ async function waitForAppServerHealth(url, timeoutMs, gatewayToken) {
2555
2614
  const deadline = Date.now() + timeoutMs;
2556
2615
  while (Date.now() < deadline) {
2557
- if (await checkAppServerHealth(url)) {
2616
+ if (await checkAppServerHealth(
2617
+ url,
2618
+ APP_SERVER_HEALTH_TIMEOUT_MS,
2619
+ gatewayToken
2620
+ )) {
2558
2621
  return true;
2559
2622
  }
2560
2623
  await delay(APP_SERVER_HEALTH_RETRY_MS);
@@ -2567,7 +2630,7 @@ async function terminateProcess(pid, platform) {
2567
2630
  }
2568
2631
  try {
2569
2632
  if (platform === "win32") {
2570
- execSync4(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
2633
+ execSync3(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
2571
2634
  } else {
2572
2635
  process.kill(pid, "SIGTERM");
2573
2636
  await delay(2e3);
@@ -2819,8 +2882,9 @@ Or start it manually:
2819
2882
  throw new Error("Tap auth gateway token is missing after startup.");
2820
2883
  }
2821
2884
  const gatewayHealthy = await waitForAppServerHealth(
2822
- buildProtectedAppServerUrl(effectiveUrl, gatewayToken),
2823
- APP_SERVER_GATEWAY_START_TIMEOUT_MS
2885
+ effectiveUrl,
2886
+ APP_SERVER_GATEWAY_START_TIMEOUT_MS,
2887
+ gatewayToken
2824
2888
  );
2825
2889
  if (!gatewayHealthy) {
2826
2890
  await terminateProcess(pid, options.platform);
@@ -3172,8 +3236,49 @@ function getBridgeStatus(stateDir, instanceId) {
3172
3236
  }
3173
3237
 
3174
3238
  // src/commands/add.ts
3239
+ var ADD_HELP = `
3240
+ Usage:
3241
+ tap-comms add <claude|codex|gemini> [options]
3242
+
3243
+ Description:
3244
+ Install a runtime instance and configure it to use tap-comms.
3245
+
3246
+ Options:
3247
+ --name <name> Instance name (default: runtime name)
3248
+ --port <port> Port for app-server bridge
3249
+ --agent-name <name> Agent display name for bridge identification
3250
+ --force Re-install even if already configured
3251
+ --headless Enable headless reviewer mode (requires --name)
3252
+ --role <role> Headless role: reviewer, validator, long-running
3253
+ --help, -h Show help
3254
+
3255
+ Examples:
3256
+ npx @hua-labs/tap add claude
3257
+ npx @hua-labs/tap add codex --name reviewer --port 4501 --headless --role reviewer
3258
+ `.trim();
3259
+ function normalizeAgentName(value) {
3260
+ if (typeof value !== "string") {
3261
+ return null;
3262
+ }
3263
+ const trimmed = value.trim();
3264
+ return trimmed ? trimmed : null;
3265
+ }
3266
+ function resolveAgentName2(options) {
3267
+ return normalizeAgentName(options.explicit) ?? normalizeAgentName(options.stored) ?? normalizeAgentName(options.env) ?? normalizeAgentName(options.fallback) ?? null;
3268
+ }
3175
3269
  async function addCommand(args) {
3176
3270
  const { positional, flags } = parseArgs(args);
3271
+ if (flags["help"] === true || flags["h"] === true) {
3272
+ log(ADD_HELP);
3273
+ return {
3274
+ ok: true,
3275
+ command: "add",
3276
+ code: "TAP_NO_OP",
3277
+ message: ADD_HELP,
3278
+ warnings: [],
3279
+ data: {}
3280
+ };
3281
+ }
3177
3282
  const runtimeArg = positional[0];
3178
3283
  if (!runtimeArg) {
3179
3284
  return {
@@ -3199,8 +3304,10 @@ async function addCommand(args) {
3199
3304
  const instanceName = typeof flags["name"] === "string" ? flags["name"] : void 0;
3200
3305
  const instanceId = buildInstanceId(runtime, instanceName);
3201
3306
  const portStr = typeof flags["port"] === "string" ? flags["port"] : void 0;
3202
- const port = portStr ? parseInt(portStr, 10) : null;
3203
- const agentNameFlag = typeof flags["agent-name"] === "string" ? flags["agent-name"] : null;
3307
+ const port = portStr ? Number(portStr) : null;
3308
+ const agentNameFlag = normalizeAgentName(
3309
+ typeof flags["agent-name"] === "string" ? flags["agent-name"] : null
3310
+ );
3204
3311
  const force = flags["force"] === true;
3205
3312
  const headlessFlag = flags["headless"] === true;
3206
3313
  const roleArg = typeof flags["role"] === "string" ? flags["role"] : void 0;
@@ -3235,20 +3342,21 @@ async function addCommand(args) {
3235
3342
  maxRounds: 5,
3236
3343
  qualitySeverityFloor: "high"
3237
3344
  } : null;
3238
- if (portStr && (port === null || isNaN(port))) {
3345
+ if (portStr && (port === null || isNaN(port) || port < 1 || port > 65535)) {
3239
3346
  return {
3240
3347
  ok: false,
3241
3348
  command: "add",
3242
3349
  runtime,
3243
3350
  instanceId,
3244
3351
  code: "TAP_INVALID_ARGUMENT",
3245
- message: `Invalid port: ${portStr}`,
3352
+ message: `Invalid port: ${portStr}. Must be between 1 and 65535.`,
3246
3353
  warnings: [],
3247
3354
  data: {}
3248
3355
  };
3249
3356
  }
3250
3357
  const repoRoot = findRepoRoot();
3251
3358
  const state = loadState(repoRoot);
3359
+ const adapter = getAdapter(runtime);
3252
3360
  if (!state) {
3253
3361
  return {
3254
3362
  ok: false,
@@ -3261,7 +3369,39 @@ async function addCommand(args) {
3261
3369
  data: {}
3262
3370
  };
3263
3371
  }
3264
- if (state.instances[instanceId]?.installed && !force) {
3372
+ const existingInstance = state.instances[instanceId];
3373
+ const mode = adapter.bridgeMode();
3374
+ const envAgentName = normalizeAgentName(
3375
+ process.env.TAP_AGENT_NAME ?? process.env.CODEX_TAP_AGENT_NAME
3376
+ );
3377
+ const defaultAgentName = mode === "app-server" ? instanceId : null;
3378
+ const resolvedAgentName = resolveAgentName2({
3379
+ explicit: agentNameFlag,
3380
+ env: envAgentName,
3381
+ stored: existingInstance?.agentName ?? null,
3382
+ fallback: defaultAgentName
3383
+ });
3384
+ if (existingInstance?.installed && !force) {
3385
+ if (resolvedAgentName !== existingInstance.agentName) {
3386
+ const updatedState = updateInstanceState(state, instanceId, {
3387
+ ...existingInstance,
3388
+ agentName: resolvedAgentName
3389
+ });
3390
+ saveState(repoRoot, updatedState);
3391
+ return {
3392
+ ok: true,
3393
+ command: "add",
3394
+ runtime,
3395
+ instanceId,
3396
+ code: "TAP_ADD_OK",
3397
+ message: resolvedAgentName === null ? `${instanceId} updated` : `${instanceId} agent name updated to "${resolvedAgentName}".`,
3398
+ warnings: [],
3399
+ data: {
3400
+ updatedFields: ["agentName"],
3401
+ agentName: resolvedAgentName
3402
+ }
3403
+ };
3404
+ }
3265
3405
  return {
3266
3406
  ok: true,
3267
3407
  command: "add",
@@ -3291,14 +3431,12 @@ async function addCommand(args) {
3291
3431
  logHeader(`@hua-labs/tap add ${instanceId}`);
3292
3432
  if (instanceName) log(`Instance name: ${instanceName}`);
3293
3433
  if (port !== null) log(`Port: ${port}`);
3294
- const existingAgentName = state.instances[instanceId]?.agentName ?? null;
3295
- const effectiveAgentName = agentNameFlag ?? existingAgentName ?? void 0;
3434
+ if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
3296
3435
  const ctx = {
3297
3436
  ...createAdapterContext(state.commsDir, repoRoot),
3298
3437
  instanceId,
3299
- agentName: effectiveAgentName
3438
+ agentName: resolvedAgentName ?? void 0
3300
3439
  };
3301
- const adapter = getAdapter(runtime);
3302
3440
  const warnings = [];
3303
3441
  log("Probing runtime...");
3304
3442
  const probe = await adapter.probe(ctx);
@@ -3378,7 +3516,6 @@ async function addCommand(args) {
3378
3516
  );
3379
3517
  }
3380
3518
  let bridge = null;
3381
- const mode = adapter.bridgeMode();
3382
3519
  if (mode === "app-server") {
3383
3520
  const bridgeScript = adapter.resolveBridgeScript?.(ctx);
3384
3521
  if (!bridgeScript) {
@@ -3386,36 +3523,34 @@ async function addCommand(args) {
3386
3523
  warnings.push("Bridge script not found. Run bridge manually.");
3387
3524
  } else {
3388
3525
  const { config: resolvedCfg } = resolveConfig({}, repoRoot);
3389
- {
3390
- log(`Starting bridge: ${bridgeScript}`);
3391
- try {
3392
- bridge = await startBridge({
3393
- instanceId,
3394
- runtime,
3395
- stateDir: ctx.stateDir,
3396
- commsDir: ctx.commsDir,
3397
- bridgeScript,
3398
- platform: ctx.platform,
3399
- agentName: agentNameFlag ?? void 0,
3400
- runtimeCommand: resolvedCfg.runtimeCommand,
3401
- appServerUrl: resolvedCfg.appServerUrl,
3402
- repoRoot,
3403
- port: port ?? void 0,
3404
- headless
3405
- });
3406
- logSuccess(`Bridge started (PID: ${bridge.pid})`);
3407
- } catch (err) {
3408
- const msg = err instanceof Error ? err.message : String(err);
3409
- logWarn(`Bridge not started: ${msg}`);
3410
- warnings.push(`Bridge not started: ${msg}`);
3411
- }
3526
+ log(`Starting bridge: ${bridgeScript}`);
3527
+ try {
3528
+ bridge = await startBridge({
3529
+ instanceId,
3530
+ runtime,
3531
+ stateDir: ctx.stateDir,
3532
+ commsDir: ctx.commsDir,
3533
+ bridgeScript,
3534
+ platform: ctx.platform,
3535
+ agentName: resolvedAgentName ?? void 0,
3536
+ runtimeCommand: resolvedCfg.runtimeCommand,
3537
+ appServerUrl: resolvedCfg.appServerUrl,
3538
+ repoRoot,
3539
+ port: port ?? void 0,
3540
+ headless
3541
+ });
3542
+ logSuccess(`Bridge started (PID: ${bridge.pid})`);
3543
+ } catch (err) {
3544
+ const msg = err instanceof Error ? err.message : String(err);
3545
+ logWarn(`Bridge not started: ${msg}`);
3546
+ warnings.push(`Bridge not started: ${msg}`);
3412
3547
  }
3413
3548
  }
3414
3549
  }
3415
3550
  const instanceState = {
3416
3551
  instanceId,
3417
3552
  runtime,
3418
- agentName: agentNameFlag ?? existingAgentName,
3553
+ agentName: resolvedAgentName,
3419
3554
  port,
3420
3555
  installed: true,
3421
3556
  configPath: probe.configPath ?? "",
@@ -3427,7 +3562,7 @@ async function addCommand(args) {
3427
3562
  lastVerifiedAt: verify.ok ? (/* @__PURE__ */ new Date()).toISOString() : null,
3428
3563
  bridge,
3429
3564
  headless,
3430
- warnings: [...result.warnings, ...verify.warnings]
3565
+ warnings: Array.from(/* @__PURE__ */ new Set([...result.warnings, ...verify.warnings]))
3431
3566
  };
3432
3567
  const newState = updateInstanceState(state, instanceId, instanceState);
3433
3568
  saveState(repoRoot, newState);
@@ -3435,6 +3570,13 @@ async function addCommand(args) {
3435
3570
  if (result.restartRequired) {
3436
3571
  logWarn(`Restart ${runtime} to pick up the new configuration.`);
3437
3572
  }
3573
+ if (runtime === "claude") {
3574
+ log("");
3575
+ log("For real-time notifications:");
3576
+ log(" claude --dangerously-load-development-channels server:tap-comms");
3577
+ log("Or polling mode (tools still work):");
3578
+ log(" claude");
3579
+ }
3438
3580
  logHeader("Done!");
3439
3581
  return {
3440
3582
  ok: true,
@@ -3454,6 +3596,16 @@ async function addCommand(args) {
3454
3596
  }
3455
3597
 
3456
3598
  // src/commands/status.ts
3599
+ var STATUS_HELP = `
3600
+ Usage:
3601
+ tap-comms status
3602
+
3603
+ Description:
3604
+ Show all installed instances, their bridge status, and configuration info.
3605
+
3606
+ Examples:
3607
+ npx @hua-labs/tap status
3608
+ `.trim();
3457
3609
  function resolveStatus(inst, stateDir) {
3458
3610
  if (!inst.installed) return "not installed";
3459
3611
  switch (inst.bridgeMode) {
@@ -3480,7 +3632,18 @@ function instanceStatusLine(inst, status) {
3480
3632
  const warns = inst.warnings.length > 0 ? ` [${inst.warnings.length} warning(s)]` : "";
3481
3633
  return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
3482
3634
  }
3483
- async function statusCommand(_args) {
3635
+ async function statusCommand(args) {
3636
+ if (args.includes("--help") || args.includes("-h")) {
3637
+ log(STATUS_HELP);
3638
+ return {
3639
+ ok: true,
3640
+ command: "status",
3641
+ code: "TAP_NO_OP",
3642
+ message: STATUS_HELP,
3643
+ warnings: [],
3644
+ data: {}
3645
+ };
3646
+ }
3484
3647
  const repoRoot = findRepoRoot();
3485
3648
  const state = loadState(repoRoot);
3486
3649
  if (!state) {
@@ -3709,7 +3872,32 @@ function cleanEmptyParents(obj, keyPath) {
3709
3872
  }
3710
3873
 
3711
3874
  // src/commands/remove.ts
3875
+ var REMOVE_HELP = `
3876
+ Usage:
3877
+ tap-comms remove <instance>
3878
+
3879
+ Description:
3880
+ Remove a registered instance, stop its bridge, and rollback config changes.
3881
+
3882
+ Arguments:
3883
+ <instance> Instance ID or runtime name (e.g. claude, codex-reviewer)
3884
+
3885
+ Examples:
3886
+ npx @hua-labs/tap remove claude
3887
+ npx @hua-labs/tap remove codex-reviewer
3888
+ `.trim();
3712
3889
  async function removeCommand(args) {
3890
+ if (args.includes("--help") || args.includes("-h")) {
3891
+ log(REMOVE_HELP);
3892
+ return {
3893
+ ok: true,
3894
+ command: "remove",
3895
+ code: "TAP_NO_OP",
3896
+ message: REMOVE_HELP,
3897
+ warnings: [],
3898
+ data: {}
3899
+ };
3900
+ }
3713
3901
  const identifier = args.find((a) => !a.startsWith("-"));
3714
3902
  if (!identifier) {
3715
3903
  return {
@@ -3745,8 +3933,8 @@ async function removeCommand(args) {
3745
3933
  };
3746
3934
  }
3747
3935
  const instanceId = resolved.instanceId;
3748
- const instance = state.instances[instanceId];
3749
- if (!instance?.installed) {
3936
+ const instance2 = state.instances[instanceId];
3937
+ if (!instance2?.installed) {
3750
3938
  return {
3751
3939
  ok: true,
3752
3940
  command: "remove",
@@ -3758,7 +3946,7 @@ async function removeCommand(args) {
3758
3946
  };
3759
3947
  }
3760
3948
  logHeader(`@hua-labs/tap remove ${instanceId}`);
3761
- if (instance.bridge) {
3949
+ if (instance2.bridge) {
3762
3950
  const ctx = createAdapterContext(state.commsDir, repoRoot);
3763
3951
  const stopped = await stopBridge({
3764
3952
  instanceId,
@@ -3771,7 +3959,7 @@ async function removeCommand(args) {
3771
3959
  log(`No running bridge for ${instanceId}`);
3772
3960
  }
3773
3961
  }
3774
- const result = await rollbackRuntime(instanceId, instance);
3962
+ const result = await rollbackRuntime(instanceId, instance2);
3775
3963
  if (result.success) {
3776
3964
  logSuccess(`Rolled back ${result.restoredCount} artifact(s)`);
3777
3965
  for (const f of result.restoredFiles) logSuccess(`Restored: ${f}`);
@@ -3783,7 +3971,7 @@ async function removeCommand(args) {
3783
3971
  ok: true,
3784
3972
  command: "remove",
3785
3973
  instanceId,
3786
- runtime: instance.runtime,
3974
+ runtime: instance2.runtime,
3787
3975
  code: "TAP_REMOVE_OK",
3788
3976
  message: `${instanceId} removed successfully`,
3789
3977
  warnings: [],
@@ -3798,7 +3986,7 @@ async function removeCommand(args) {
3798
3986
  ok: false,
3799
3987
  command: "remove",
3800
3988
  instanceId,
3801
- runtime: instance.runtime,
3989
+ runtime: instance2.runtime,
3802
3990
  code: "TAP_ROLLBACK_FAILED",
3803
3991
  message: "Rollback had errors. State preserved for retry.",
3804
3992
  warnings: result.errors,
@@ -3827,7 +4015,7 @@ Subcommands:
3827
4015
 
3828
4016
  Options:
3829
4017
  --agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
3830
- Saved to state \u2014 only needed on first start
4018
+ Overrides the stored name from 'tap add' when needed
3831
4019
  --all Start all registered app-server instances
3832
4020
  --busy-mode <steer|wait> How to handle active turns (default: steer)
3833
4021
  --poll-seconds <n> Inbox poll interval (default: 5)
@@ -3863,11 +4051,11 @@ function redactProtectedUrl(url) {
3863
4051
  try {
3864
4052
  const parsed = new URL(url);
3865
4053
  if (parsed.searchParams.has("tap_token")) {
3866
- parsed.searchParams.set("tap_token", "***");
4054
+ parsed.searchParams.delete("tap_token");
3867
4055
  }
3868
4056
  return parsed.toString().replace(/\/$/, "");
3869
4057
  } catch {
3870
- return url.replace(/tap_token=[^&]+/g, "tap_token=***");
4058
+ return url.replace(/[?&]tap_token=[^&]+/g, "");
3871
4059
  }
3872
4060
  }
3873
4061
  function loadCurrentBridgeState(stateDir, instanceId, fallback) {
@@ -3950,37 +4138,37 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3950
4138
  };
3951
4139
  }
3952
4140
  const instanceId = resolved.instanceId;
3953
- let instance = state.instances[instanceId];
3954
- if (!instance?.installed) {
4141
+ let instance2 = state.instances[instanceId];
4142
+ if (!instance2?.installed) {
3955
4143
  return {
3956
4144
  ok: false,
3957
4145
  command: "bridge",
3958
4146
  instanceId,
3959
- runtime: instance?.runtime,
4147
+ runtime: instance2?.runtime,
3960
4148
  code: "TAP_INSTANCE_NOT_FOUND",
3961
- message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance?.runtime ?? identifier}`,
4149
+ message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance2?.runtime ?? identifier}`,
3962
4150
  warnings: [],
3963
4151
  data: {}
3964
4152
  };
3965
4153
  }
3966
- const adapter = getAdapter(instance.runtime);
4154
+ const adapter = getAdapter(instance2.runtime);
3967
4155
  const mode = adapter.bridgeMode();
3968
4156
  if (mode !== "app-server") {
3969
4157
  return {
3970
4158
  ok: true,
3971
4159
  command: "bridge",
3972
4160
  instanceId,
3973
- runtime: instance.runtime,
4161
+ runtime: instance2.runtime,
3974
4162
  code: "TAP_NO_OP",
3975
4163
  message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
3976
4164
  warnings: [],
3977
4165
  data: { bridgeMode: mode }
3978
4166
  };
3979
4167
  }
3980
- const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
3981
- if (agentName && agentName !== instance.agentName) {
3982
- instance = { ...instance, agentName };
3983
- const updatedState = updateInstanceState(state, instanceId, instance);
4168
+ const resolvedAgentName = agentName ?? instance2.agentName ?? void 0;
4169
+ if (agentName && agentName !== instance2.agentName) {
4170
+ instance2 = { ...instance2, agentName };
4171
+ const updatedState = updateInstanceState(state, instanceId, instance2);
3984
4172
  saveState(repoRoot, updatedState);
3985
4173
  state = updatedState;
3986
4174
  }
@@ -3991,7 +4179,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3991
4179
  ok: false,
3992
4180
  command: "bridge",
3993
4181
  instanceId,
3994
- runtime: instance.runtime,
4182
+ runtime: instance2.runtime,
3995
4183
  code: "TAP_BRIDGE_SCRIPT_MISSING",
3996
4184
  message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
3997
4185
  warnings: [],
@@ -4000,8 +4188,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4000
4188
  }
4001
4189
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
4002
4190
  const runtimeCommand = resolvedConfig.runtimeCommand;
4003
- const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
4004
- let effectivePort = instance.port;
4191
+ const manageAppServer = instance2.runtime === "codex" && flags["no-server"] !== true;
4192
+ let effectivePort = instance2.port;
4005
4193
  if (effectivePort == null && manageAppServer) {
4006
4194
  effectivePort = await findNextAvailableAppServerPort(
4007
4195
  state,
@@ -4009,8 +4197,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4009
4197
  4501,
4010
4198
  instanceId
4011
4199
  );
4012
- instance = { ...instance, port: effectivePort };
4013
- const updatedState = updateInstanceState(state, instanceId, instance);
4200
+ instance2 = { ...instance2, port: effectivePort };
4201
+ const updatedState = updateInstanceState(state, instanceId, instance2);
4014
4202
  saveState(repoRoot, updatedState);
4015
4203
  state = updatedState;
4016
4204
  }
@@ -4026,19 +4214,19 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4026
4214
  if (effectivePort != null) log(`Port: ${effectivePort}`);
4027
4215
  if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
4028
4216
  const noAuth = flags["no-auth"] === true;
4029
- if (!manageAppServer && instance.runtime === "codex") {
4217
+ if (!manageAppServer && instance2.runtime === "codex") {
4030
4218
  log("Auto server: disabled (--no-server)");
4031
4219
  }
4032
4220
  if (noAuth && manageAppServer) {
4033
4221
  log("Auth gateway: disabled (--no-auth)");
4034
4222
  }
4035
- const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
4223
+ const willBeHeadless = flags["headless"] === true || instance2.headless?.enabled;
4036
4224
  if (willBeHeadless) {
4037
- const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance.headless?.role ?? "reviewer";
4225
+ const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance2.headless?.role ?? "reviewer";
4038
4226
  log(`Headless: ${role}`);
4039
4227
  }
4040
4228
  try {
4041
- if (!manageAppServer && instance.runtime === "codex") {
4229
+ if (!manageAppServer && instance2.runtime === "codex") {
4042
4230
  log("Checking app-server health...");
4043
4231
  const healthy = await checkAppServerHealth(appServerUrl);
4044
4232
  if (healthy) {
@@ -4049,7 +4237,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4049
4237
  ok: false,
4050
4238
  command: "bridge",
4051
4239
  instanceId,
4052
- runtime: instance.runtime,
4240
+ runtime: instance2.runtime,
4053
4241
  code: "TAP_BRIDGE_START_FAILED",
4054
4242
  message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
4055
4243
  warnings: [],
@@ -4063,7 +4251,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4063
4251
  ok: false,
4064
4252
  command: "bridge",
4065
4253
  instanceId,
4066
- runtime: instance.runtime,
4254
+ runtime: instance2.runtime,
4067
4255
  code: "TAP_INVALID_ARGUMENT",
4068
4256
  message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
4069
4257
  warnings: [],
@@ -4071,9 +4259,38 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4071
4259
  };
4072
4260
  }
4073
4261
  const busyMode = busyModeRaw;
4074
- const pollSeconds = typeof flags["poll-seconds"] === "string" ? parseInt(flags["poll-seconds"], 10) : void 0;
4075
- const reconnectSeconds = typeof flags["reconnect-seconds"] === "string" ? parseInt(flags["reconnect-seconds"], 10) : void 0;
4076
- const messageLookbackMinutes = typeof flags["message-lookback-minutes"] === "string" ? parseInt(flags["message-lookback-minutes"], 10) : void 0;
4262
+ const pollSecondsRaw = typeof flags["poll-seconds"] === "string" ? flags["poll-seconds"] : void 0;
4263
+ const reconnectSecondsRaw = typeof flags["reconnect-seconds"] === "string" ? flags["reconnect-seconds"] : void 0;
4264
+ const lookbackRaw = typeof flags["message-lookback-minutes"] === "string" ? flags["message-lookback-minutes"] : void 0;
4265
+ let pollSeconds;
4266
+ let reconnectSeconds;
4267
+ let messageLookbackMinutes;
4268
+ try {
4269
+ pollSeconds = parseIntFlag(pollSecondsRaw, "--poll-seconds", 1, 3600);
4270
+ reconnectSeconds = parseIntFlag(
4271
+ reconnectSecondsRaw,
4272
+ "--reconnect-seconds",
4273
+ 1,
4274
+ 3600
4275
+ );
4276
+ messageLookbackMinutes = parseIntFlag(
4277
+ lookbackRaw,
4278
+ "--message-lookback-minutes",
4279
+ 1,
4280
+ 10080
4281
+ );
4282
+ } catch (err) {
4283
+ return {
4284
+ ok: false,
4285
+ command: "bridge",
4286
+ instanceId,
4287
+ runtime: instance2.runtime,
4288
+ code: "TAP_INVALID_ARGUMENT",
4289
+ message: err instanceof Error ? err.message : String(err),
4290
+ warnings: [],
4291
+ data: {}
4292
+ };
4293
+ }
4077
4294
  const threadId = typeof flags["thread-id"] === "string" ? flags["thread-id"] : void 0;
4078
4295
  const ephemeral = flags["ephemeral"] === true;
4079
4296
  const processExistingMessages = flags["process-existing-messages"] === true;
@@ -4085,7 +4302,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4085
4302
  ok: false,
4086
4303
  command: "bridge",
4087
4304
  instanceId,
4088
- runtime: instance.runtime,
4305
+ runtime: instance2.runtime,
4089
4306
  code: "TAP_INVALID_ARGUMENT",
4090
4307
  message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
4091
4308
  warnings: [],
@@ -4097,10 +4314,10 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4097
4314
  role: roleArg ?? "reviewer",
4098
4315
  maxRounds: 5,
4099
4316
  qualitySeverityFloor: "high"
4100
- } : instance.headless;
4317
+ } : instance2.headless;
4101
4318
  const bridge = await startBridge({
4102
4319
  instanceId,
4103
- runtime: instance.runtime,
4320
+ runtime: instance2.runtime,
4104
4321
  stateDir: ctx.stateDir,
4105
4322
  commsDir: ctx.commsDir,
4106
4323
  bridgeScript,
@@ -4141,14 +4358,14 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4141
4358
  log(`TUI connect: ${bridge.appServer.url}`);
4142
4359
  }
4143
4360
  }
4144
- const updated = { ...instance, bridge, manageAppServer, noAuth };
4361
+ const updated = { ...instance2, bridge, manageAppServer, noAuth };
4145
4362
  const newState = updateInstanceState(state, instanceId, updated);
4146
4363
  saveState(repoRoot, newState);
4147
4364
  return {
4148
4365
  ok: true,
4149
4366
  command: "bridge",
4150
4367
  instanceId,
4151
- runtime: instance.runtime,
4368
+ runtime: instance2.runtime,
4152
4369
  code: "TAP_BRIDGE_START_OK",
4153
4370
  message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
4154
4371
  warnings: [],
@@ -4161,7 +4378,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4161
4378
  ok: false,
4162
4379
  command: "bridge",
4163
4380
  instanceId,
4164
- runtime: instance.runtime,
4381
+ runtime: instance2.runtime,
4165
4382
  code: "TAP_BRIDGE_START_FAILED",
4166
4383
  message: msg,
4167
4384
  warnings: [],
@@ -4263,11 +4480,11 @@ async function bridgeStopOne(identifier) {
4263
4480
  }
4264
4481
  const instanceId = resolved.instanceId;
4265
4482
  const ctx = createAdapterContext(state.commsDir, repoRoot);
4266
- const instance = state.instances[instanceId];
4483
+ const instance2 = state.instances[instanceId];
4267
4484
  const bridgeState = loadCurrentBridgeState(
4268
4485
  ctx.stateDir,
4269
4486
  instanceId,
4270
- instance?.bridge
4487
+ instance2?.bridge
4271
4488
  );
4272
4489
  const appServer = bridgeState?.appServer ?? null;
4273
4490
  logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
@@ -4315,8 +4532,8 @@ async function bridgeStopOne(identifier) {
4315
4532
  }
4316
4533
  }
4317
4534
  }
4318
- if (instance) {
4319
- const updated = { ...instance, bridge: null };
4535
+ if (instance2) {
4536
+ const updated = { ...instance2, bridge: null };
4320
4537
  const newState = updateInstanceState(state, instanceId, updated);
4321
4538
  saveState(repoRoot, newState);
4322
4539
  }
@@ -4388,9 +4605,9 @@ async function bridgeStopAll() {
4388
4605
  logSuccess(`Stopped bridge for ${instanceId}`);
4389
4606
  stopped.push(instanceId);
4390
4607
  }
4391
- const instance = state.instances[instanceId];
4392
- if (instance?.bridge) {
4393
- state.instances[instanceId] = { ...instance, bridge: null };
4608
+ const instance2 = state.instances[instanceId];
4609
+ if (instance2?.bridge) {
4610
+ state.instances[instanceId] = { ...instance2, bridge: null };
4394
4611
  stateChanged = true;
4395
4612
  }
4396
4613
  }
@@ -4686,8 +4903,22 @@ async function bridgeRestart(identifier, flags) {
4686
4903
  };
4687
4904
  }
4688
4905
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
4689
- const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
4690
- const drainTimeout = parseInt(drainStr, 10) || 30;
4906
+ const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : void 0;
4907
+ let drainTimeout;
4908
+ try {
4909
+ drainTimeout = parseIntFlag(drainStr, "--drain-timeout", 1, 300) ?? 30;
4910
+ } catch (err) {
4911
+ return {
4912
+ ok: false,
4913
+ command: "bridge",
4914
+ instanceId,
4915
+ runtime: instance.runtime,
4916
+ code: "TAP_INVALID_ARGUMENT",
4917
+ message: err instanceof Error ? err.message : String(err),
4918
+ warnings: [],
4919
+ data: {}
4920
+ };
4921
+ }
4691
4922
  logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
4692
4923
  log(`Drain timeout: ${drainTimeout}s`);
4693
4924
  try {
@@ -4834,7 +5065,7 @@ async function bridgeCommand(args) {
4834
5065
  // src/engine/dashboard.ts
4835
5066
  import * as fs15 from "fs";
4836
5067
  import * as path15 from "path";
4837
- import { execSync as execSync5 } from "child_process";
5068
+ import { execSync as execSync4 } from "child_process";
4838
5069
  function collectAgents(commsDir) {
4839
5070
  const heartbeatsPath = path15.join(commsDir, "heartbeats.json");
4840
5071
  if (!fs15.existsSync(heartbeatsPath)) return [];
@@ -4913,7 +5144,7 @@ function collectBridges(repoRoot) {
4913
5144
  }
4914
5145
  function collectPRs() {
4915
5146
  try {
4916
- const output = execSync5(
5147
+ const output = execSync4(
4917
5148
  "gh pr list --state all --limit 10 --json number,title,author,state,url",
4918
5149
  { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
4919
5150
  );
@@ -5087,7 +5318,34 @@ async function downCommand(args) {
5087
5318
  // src/commands/serve.ts
5088
5319
  import * as path16 from "path";
5089
5320
  import { spawn as spawn2 } from "child_process";
5321
+ var SERVE_HELP = `
5322
+ Usage:
5323
+ tap-comms serve [options]
5324
+
5325
+ Description:
5326
+ Start the tap-comms MCP server over stdio. This command takes over the
5327
+ process \u2014 it is intended to be launched by an MCP host (e.g. Claude Code).
5328
+
5329
+ Options:
5330
+ --comms-dir <path> Override comms directory (also reads TAP_COMMS_DIR env)
5331
+ --help, -h Show help
5332
+
5333
+ Examples:
5334
+ npx @hua-labs/tap serve
5335
+ npx @hua-labs/tap serve --comms-dir /shared/comms
5336
+ `.trim();
5090
5337
  async function serveCommand(args) {
5338
+ if (args.includes("--help") || args.includes("-h")) {
5339
+ log(SERVE_HELP);
5340
+ return {
5341
+ ok: true,
5342
+ command: "serve",
5343
+ code: "TAP_NO_OP",
5344
+ message: SERVE_HELP,
5345
+ warnings: [],
5346
+ data: {}
5347
+ };
5348
+ }
5091
5349
  const repoRoot = findRepoRoot();
5092
5350
  let commsDir;
5093
5351
  const commsDirIdx = args.indexOf("--comms-dir");
@@ -5162,7 +5420,7 @@ async function serveCommand(args) {
5162
5420
  // src/commands/init-worktree.ts
5163
5421
  import * as fs16 from "fs";
5164
5422
  import * as path17 from "path";
5165
- import { execSync as execSync6 } from "child_process";
5423
+ import { execSync as execSync5 } from "child_process";
5166
5424
  var INIT_WORKTREE_HELP = `
5167
5425
  Usage:
5168
5426
  tap-comms init-worktree [options]
@@ -5186,7 +5444,7 @@ function warn(warnings, message) {
5186
5444
  }
5187
5445
  function run(cmd, opts) {
5188
5446
  try {
5189
- return execSync6(cmd, {
5447
+ return execSync5(cmd, {
5190
5448
  cwd: opts?.cwd,
5191
5449
  encoding: "utf-8",
5192
5450
  stdio: ["pipe", "pipe", "pipe"],
@@ -5203,7 +5461,7 @@ function toAbsolute(p) {
5203
5461
  }
5204
5462
  function probeBun(candidate) {
5205
5463
  try {
5206
- const out = execSync6(`"${candidate}" --version`, {
5464
+ const out = execSync5(`"${candidate}" --version`, {
5207
5465
  encoding: "utf-8",
5208
5466
  stdio: ["pipe", "pipe", "pipe"],
5209
5467
  timeout: 5e3
@@ -5217,7 +5475,7 @@ function findBun() {
5217
5475
  const candidates = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
5218
5476
  for (const name of candidates) {
5219
5477
  try {
5220
- const out = execSync6(
5478
+ const out = execSync5(
5221
5479
  process.platform === "win32" ? `where ${name}` : `which ${name}`,
5222
5480
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
5223
5481
  ).trim();
@@ -5448,7 +5706,7 @@ async function initWorktreeCommand(args) {
5448
5706
  ok: true,
5449
5707
  command: "init-worktree",
5450
5708
  code: "TAP_NO_OP",
5451
- message: "init-worktree help",
5709
+ message: INIT_WORKTREE_HELP,
5452
5710
  warnings: [],
5453
5711
  data: {}
5454
5712
  };
@@ -5610,8 +5868,38 @@ function renderSnapshot(snapshot) {
5610
5868
  }
5611
5869
  }
5612
5870
  }
5871
+ var DASHBOARD_HELP = `
5872
+ Usage:
5873
+ tap-comms dashboard [options]
5874
+
5875
+ Description:
5876
+ Display a unified ops dashboard: agents, bridges, PRs, and warnings.
5877
+
5878
+ Options:
5879
+ --json Output snapshot as JSON
5880
+ --watch Refresh dashboard on an interval
5881
+ --interval <seconds> Refresh interval in seconds (default: 5, min: 2)
5882
+ --comms-dir <path> Override comms directory
5883
+ --help, -h Show help
5884
+
5885
+ Examples:
5886
+ npx @hua-labs/tap dashboard
5887
+ npx @hua-labs/tap dashboard --watch --interval 10
5888
+ npx @hua-labs/tap dashboard --json
5889
+ `.trim();
5613
5890
  async function dashboardCommand(args) {
5614
5891
  const { flags } = parseArgs(args);
5892
+ if (flags["help"] === true || flags["h"] === true) {
5893
+ log(DASHBOARD_HELP);
5894
+ return {
5895
+ ok: true,
5896
+ command: "dashboard",
5897
+ code: "TAP_NO_OP",
5898
+ message: DASHBOARD_HELP,
5899
+ warnings: [],
5900
+ data: {}
5901
+ };
5902
+ }
5615
5903
  const jsonMode = flags["json"] === true;
5616
5904
  const watchMode = flags["watch"] === true;
5617
5905
  const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : "5";
@@ -5674,7 +5962,7 @@ import {
5674
5962
  statSync as statSync2,
5675
5963
  unlinkSync as unlinkSync3
5676
5964
  } from "fs";
5677
- import { execSync as execSync7 } from "child_process";
5965
+ import { execSync as execSync6 } from "child_process";
5678
5966
  import { join as join17 } from "path";
5679
5967
  var PASS = "pass";
5680
5968
  var WARN = "warn";
@@ -5941,7 +6229,7 @@ function checkMcpServer(repoRoot) {
5941
6229
  let cmdAvailable = existsSync16(cmd);
5942
6230
  if (!cmdAvailable) {
5943
6231
  try {
5944
- execSync7(`"${cmd}" --version`, {
6232
+ execSync6(`"${cmd}" --version`, {
5945
6233
  stdio: "pipe",
5946
6234
  timeout: 5e3
5947
6235
  });
@@ -6118,7 +6406,35 @@ function renderCheck(check, fixMode) {
6118
6406
  const msg = check.message ? ` \u2014 ${check.message}${fixable}` : "";
6119
6407
  return ` ${icon} ${check.name}${msg}`;
6120
6408
  }
6409
+ var DOCTOR_HELP = `
6410
+ Usage:
6411
+ tap-comms doctor [options]
6412
+
6413
+ Description:
6414
+ Diagnose tap infrastructure health: comms directory, instances, bridges,
6415
+ message lifecycle, and MCP server configuration.
6416
+
6417
+ Options:
6418
+ --fix Auto-repair detected issues where possible
6419
+ --comms-dir <path> Override comms directory
6420
+ --help, -h Show help
6421
+
6422
+ Examples:
6423
+ npx @hua-labs/tap doctor
6424
+ npx @hua-labs/tap doctor --fix
6425
+ `.trim();
6121
6426
  async function doctorCommand(args) {
6427
+ if (args.includes("--help") || args.includes("-h")) {
6428
+ log(DOCTOR_HELP);
6429
+ return {
6430
+ ok: true,
6431
+ command: "doctor",
6432
+ code: "TAP_NO_OP",
6433
+ message: DOCTOR_HELP,
6434
+ warnings: [],
6435
+ data: {}
6436
+ };
6437
+ }
6122
6438
  const repoRoot = findRepoRoot();
6123
6439
  const overrides = {};
6124
6440
  let fixMode = false;
@@ -6231,7 +6547,7 @@ async function doctorCommand(args) {
6231
6547
  }
6232
6548
 
6233
6549
  // src/commands/comms.ts
6234
- import { execSync as execSync8 } from "child_process";
6550
+ import { execSync as execSync7, spawnSync as spawnSync4 } from "child_process";
6235
6551
  import * as fs17 from "fs";
6236
6552
  import * as path18 from "path";
6237
6553
  var COMMS_HELP = `
@@ -6263,7 +6579,7 @@ function commsPull(commsDir) {
6263
6579
  };
6264
6580
  }
6265
6581
  try {
6266
- const output = execSync8("git pull --rebase", {
6582
+ const output = execSync7("git pull --rebase", {
6267
6583
  cwd: commsDir,
6268
6584
  encoding: "utf-8",
6269
6585
  stdio: "pipe"
@@ -6305,8 +6621,8 @@ function commsPush(commsDir) {
6305
6621
  };
6306
6622
  }
6307
6623
  try {
6308
- execSync8("git add -A", { cwd: commsDir, stdio: "pipe" });
6309
- const status = execSync8("git status --porcelain", {
6624
+ execSync7("git add -A", { cwd: commsDir, stdio: "pipe" });
6625
+ const status = execSync7("git status --porcelain", {
6310
6626
  cwd: commsDir,
6311
6627
  encoding: "utf-8",
6312
6628
  stdio: "pipe"
@@ -6323,11 +6639,23 @@ function commsPush(commsDir) {
6323
6639
  };
6324
6640
  }
6325
6641
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6326
- execSync8(`git commit -m "chore(comms): sync ${timestamp}"`, {
6327
- cwd: commsDir,
6328
- stdio: "pipe"
6329
- });
6330
- execSync8("git push", { cwd: commsDir, stdio: "pipe" });
6642
+ const commitResult = spawnSync4(
6643
+ "git",
6644
+ ["commit", "-m", `chore(comms): sync ${timestamp}`],
6645
+ { cwd: commsDir, stdio: "pipe", encoding: "utf-8" }
6646
+ );
6647
+ if (commitResult.status !== 0) {
6648
+ const msg = commitResult.stderr || `git commit exited with code ${commitResult.status}`;
6649
+ return {
6650
+ ok: false,
6651
+ command: "comms",
6652
+ code: "TAP_COMMS_PUSH_FAILED",
6653
+ message: `Commit failed: ${msg}`,
6654
+ warnings: [],
6655
+ data: { commsDir }
6656
+ };
6657
+ }
6658
+ execSync7("git push", { cwd: commsDir, stdio: "pipe" });
6331
6659
  logSuccess("Comms push complete");
6332
6660
  return {
6333
6661
  ok: true,
@@ -6393,7 +6721,12 @@ function emitResult(result, jsonMode) {
6393
6721
  } else {
6394
6722
  logError(result.message);
6395
6723
  }
6724
+ const emittedWarnings = /* @__PURE__ */ new Set();
6396
6725
  for (const w of result.warnings) {
6726
+ if (emittedWarnings.has(w) || wasWarningLogged(w)) {
6727
+ continue;
6728
+ }
6729
+ emittedWarnings.add(w);
6397
6730
  logWarn(w);
6398
6731
  }
6399
6732
  }
@@ -6462,6 +6795,7 @@ function normalizeCommandName(command) {
6462
6795
  async function main() {
6463
6796
  const rawArgs = process.argv.slice(2);
6464
6797
  const { jsonMode, cleanArgs } = extractJsonFlag(rawArgs);
6798
+ resetLoggedWarnings();
6465
6799
  setJsonMode(jsonMode);
6466
6800
  const command = cleanArgs[0];
6467
6801
  if (!command || command === "--help" || command === "-h") {
@@ -6519,7 +6853,7 @@ async function main() {
6519
6853
  break;
6520
6854
  case "serve": {
6521
6855
  const serveResult = await serveCommand(commandArgs);
6522
- if (!serveResult.ok) {
6856
+ if (!serveResult.ok || serveResult.code === "TAP_NO_OP") {
6523
6857
  emitResult(serveResult, jsonMode);
6524
6858
  }
6525
6859
  process.exit(exitCode(serveResult));