@hua-labs/tap 0.2.3 → 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
  });
@@ -1068,7 +1128,7 @@ function findMcpJsonPath(ctx) {
1068
1128
  }
1069
1129
  function findClaudeCommand() {
1070
1130
  try {
1071
- execSync2("claude --version", { stdio: "pipe" });
1131
+ execSync("claude --version", { stdio: "pipe" });
1072
1132
  return "claude";
1073
1133
  } catch {
1074
1134
  return null;
@@ -1929,13 +1989,13 @@ import * as fs13 from "fs";
1929
1989
  import * as net from "net";
1930
1990
  import * as path13 from "path";
1931
1991
  import { randomBytes } from "crypto";
1932
- import { spawn, spawnSync as spawnSync2, execSync as execSync4 } from "child_process";
1992
+ import { spawn, spawnSync as spawnSync3, execSync as execSync3 } from "child_process";
1933
1993
  import { fileURLToPath as fileURLToPath4 } from "url";
1934
1994
 
1935
1995
  // src/runtime/resolve-node.ts
1936
1996
  import * as fs12 from "fs";
1937
1997
  import * as path12 from "path";
1938
- import { execSync as execSync3 } from "child_process";
1998
+ import { execSync as execSync2 } from "child_process";
1939
1999
  function readNodeVersion(repoRoot) {
1940
2000
  const nvFile = path12.join(repoRoot, ".node-version");
1941
2001
  if (!fs12.existsSync(nvFile)) return null;
@@ -1978,7 +2038,7 @@ function probeFnmNode(desiredVersion) {
1978
2038
  );
1979
2039
  if (!fs12.existsSync(candidate)) continue;
1980
2040
  try {
1981
- const v = execSync3(`"${candidate}" --version`, {
2041
+ const v = execSync2(`"${candidate}" --version`, {
1982
2042
  encoding: "utf-8",
1983
2043
  timeout: 5e3
1984
2044
  }).trim();
@@ -1992,7 +2052,7 @@ function probeFnmNode(desiredVersion) {
1992
2052
  }
1993
2053
  function detectNodeMajorVersion(command) {
1994
2054
  try {
1995
- const version2 = execSync3(`"${command}" --version`, {
2055
+ const version2 = execSync2(`"${command}" --version`, {
1996
2056
  encoding: "utf-8",
1997
2057
  timeout: 5e3
1998
2058
  }).trim();
@@ -2006,7 +2066,7 @@ function checkStripTypesSupport(command) {
2006
2066
  const major = detectNodeMajorVersion(command);
2007
2067
  if (major !== null && major >= 22) return true;
2008
2068
  try {
2009
- execSync3(`"${command}" --experimental-strip-types -e ""`, {
2069
+ execSync2(`"${command}" --experimental-strip-types -e ""`, {
2010
2070
  timeout: 5e3,
2011
2071
  stdio: "pipe"
2012
2072
  });
@@ -2097,7 +2157,7 @@ var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
2097
2157
  var APP_SERVER_START_TIMEOUT_MS = 2e4;
2098
2158
  var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
2099
2159
  var APP_SERVER_HEALTH_RETRY_MS = 250;
2100
- var APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
2160
+ var AUTH_SUBPROTOCOL_PREFIX = "tap-auth-";
2101
2161
  var APP_SERVER_AUTH_FILE_MODE = 384;
2102
2162
  function appServerLogFilePath(stateDir, instanceId) {
2103
2163
  return path13.join(stateDir, "logs", `app-server-${instanceId}.log`);
@@ -2158,8 +2218,11 @@ function resolvePowerShellCommand() {
2158
2218
  function resolveAuthGatewayScript(repoRoot) {
2159
2219
  const moduleDir = path13.dirname(fileURLToPath4(import.meta.url));
2160
2220
  const candidates = [
2161
- path13.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.mjs"),
2162
- 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
2163
2226
  path13.join(
2164
2227
  repoRoot,
2165
2228
  "packages",
@@ -2212,10 +2275,8 @@ async function allocateLoopbackPort(hostname) {
2212
2275
  });
2213
2276
  });
2214
2277
  }
2215
- function buildProtectedAppServerUrl(publicUrl, token) {
2216
- const url = new URL(publicUrl);
2217
- url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
2218
- return url.toString().replace(/\/(?=\?|$)/, "");
2278
+ function buildProtectedAppServerUrl(publicUrl, _token) {
2279
+ return publicUrl;
2219
2280
  }
2220
2281
  function readGatewayTokenFromPath(tokenPath) {
2221
2282
  return fs13.readFileSync(tokenPath, "utf8").trim();
@@ -2330,7 +2391,7 @@ async function createManagedAppServerAuth(options) {
2330
2391
  throw new Error("Failed to spawn app-server auth gateway");
2331
2392
  }
2332
2393
  return {
2333
- mode: "query-token",
2394
+ mode: "subprotocol",
2334
2395
  protectedUrl,
2335
2396
  upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
2336
2397
  tokenPath,
@@ -2444,7 +2505,7 @@ function findListeningProcessId(url, platform) {
2444
2505
  if (port == null || !Number.isFinite(port)) {
2445
2506
  return null;
2446
2507
  }
2447
- const result = spawnSync2(
2508
+ const result = spawnSync3(
2448
2509
  resolvePowerShellCommand(),
2449
2510
  [
2450
2511
  "-NoLogo",
@@ -2517,7 +2578,7 @@ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, e
2517
2578
  `Failed to find a free app-server port starting at ${basePort}`
2518
2579
  );
2519
2580
  }
2520
- async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
2581
+ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS, gatewayToken) {
2521
2582
  const WebSocket = getWebSocketCtor();
2522
2583
  if (!WebSocket) {
2523
2584
  return false;
@@ -2539,7 +2600,8 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
2539
2600
  };
2540
2601
  const timer = setTimeout(() => finish(false), timeoutMs);
2541
2602
  try {
2542
- socket = new WebSocket(url);
2603
+ const protocols = gatewayToken ? [`${AUTH_SUBPROTOCOL_PREFIX}${gatewayToken}`] : void 0;
2604
+ socket = new WebSocket(url, protocols);
2543
2605
  socket.addEventListener("open", () => finish(true), { once: true });
2544
2606
  socket.addEventListener("error", () => finish(false), { once: true });
2545
2607
  socket.addEventListener("close", () => finish(false), { once: true });
@@ -2548,10 +2610,14 @@ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_M
2548
2610
  }
2549
2611
  });
2550
2612
  }
2551
- async function waitForAppServerHealth(url, timeoutMs) {
2613
+ async function waitForAppServerHealth(url, timeoutMs, gatewayToken) {
2552
2614
  const deadline = Date.now() + timeoutMs;
2553
2615
  while (Date.now() < deadline) {
2554
- if (await checkAppServerHealth(url)) {
2616
+ if (await checkAppServerHealth(
2617
+ url,
2618
+ APP_SERVER_HEALTH_TIMEOUT_MS,
2619
+ gatewayToken
2620
+ )) {
2555
2621
  return true;
2556
2622
  }
2557
2623
  await delay(APP_SERVER_HEALTH_RETRY_MS);
@@ -2564,7 +2630,7 @@ async function terminateProcess(pid, platform) {
2564
2630
  }
2565
2631
  try {
2566
2632
  if (platform === "win32") {
2567
- execSync4(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
2633
+ execSync3(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
2568
2634
  } else {
2569
2635
  process.kill(pid, "SIGTERM");
2570
2636
  await delay(2e3);
@@ -2816,8 +2882,9 @@ Or start it manually:
2816
2882
  throw new Error("Tap auth gateway token is missing after startup.");
2817
2883
  }
2818
2884
  const gatewayHealthy = await waitForAppServerHealth(
2819
- buildProtectedAppServerUrl(effectiveUrl, gatewayToken),
2820
- APP_SERVER_GATEWAY_START_TIMEOUT_MS
2885
+ effectiveUrl,
2886
+ APP_SERVER_GATEWAY_START_TIMEOUT_MS,
2887
+ gatewayToken
2821
2888
  );
2822
2889
  if (!gatewayHealthy) {
2823
2890
  await terminateProcess(pid, options.platform);
@@ -3169,8 +3236,49 @@ function getBridgeStatus(stateDir, instanceId) {
3169
3236
  }
3170
3237
 
3171
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
+ }
3172
3269
  async function addCommand(args) {
3173
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
+ }
3174
3282
  const runtimeArg = positional[0];
3175
3283
  if (!runtimeArg) {
3176
3284
  return {
@@ -3196,8 +3304,10 @@ async function addCommand(args) {
3196
3304
  const instanceName = typeof flags["name"] === "string" ? flags["name"] : void 0;
3197
3305
  const instanceId = buildInstanceId(runtime, instanceName);
3198
3306
  const portStr = typeof flags["port"] === "string" ? flags["port"] : void 0;
3199
- const port = portStr ? parseInt(portStr, 10) : null;
3200
- 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
+ );
3201
3311
  const force = flags["force"] === true;
3202
3312
  const headlessFlag = flags["headless"] === true;
3203
3313
  const roleArg = typeof flags["role"] === "string" ? flags["role"] : void 0;
@@ -3232,20 +3342,21 @@ async function addCommand(args) {
3232
3342
  maxRounds: 5,
3233
3343
  qualitySeverityFloor: "high"
3234
3344
  } : null;
3235
- if (portStr && (port === null || isNaN(port))) {
3345
+ if (portStr && (port === null || isNaN(port) || port < 1 || port > 65535)) {
3236
3346
  return {
3237
3347
  ok: false,
3238
3348
  command: "add",
3239
3349
  runtime,
3240
3350
  instanceId,
3241
3351
  code: "TAP_INVALID_ARGUMENT",
3242
- message: `Invalid port: ${portStr}`,
3352
+ message: `Invalid port: ${portStr}. Must be between 1 and 65535.`,
3243
3353
  warnings: [],
3244
3354
  data: {}
3245
3355
  };
3246
3356
  }
3247
3357
  const repoRoot = findRepoRoot();
3248
3358
  const state = loadState(repoRoot);
3359
+ const adapter = getAdapter(runtime);
3249
3360
  if (!state) {
3250
3361
  return {
3251
3362
  ok: false,
@@ -3258,7 +3369,39 @@ async function addCommand(args) {
3258
3369
  data: {}
3259
3370
  };
3260
3371
  }
3261
- 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
+ }
3262
3405
  return {
3263
3406
  ok: true,
3264
3407
  command: "add",
@@ -3288,14 +3431,12 @@ async function addCommand(args) {
3288
3431
  logHeader(`@hua-labs/tap add ${instanceId}`);
3289
3432
  if (instanceName) log(`Instance name: ${instanceName}`);
3290
3433
  if (port !== null) log(`Port: ${port}`);
3291
- const existingAgentName = state.instances[instanceId]?.agentName ?? null;
3292
- const effectiveAgentName = agentNameFlag ?? existingAgentName ?? void 0;
3434
+ if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
3293
3435
  const ctx = {
3294
3436
  ...createAdapterContext(state.commsDir, repoRoot),
3295
3437
  instanceId,
3296
- agentName: effectiveAgentName
3438
+ agentName: resolvedAgentName ?? void 0
3297
3439
  };
3298
- const adapter = getAdapter(runtime);
3299
3440
  const warnings = [];
3300
3441
  log("Probing runtime...");
3301
3442
  const probe = await adapter.probe(ctx);
@@ -3375,7 +3516,6 @@ async function addCommand(args) {
3375
3516
  );
3376
3517
  }
3377
3518
  let bridge = null;
3378
- const mode = adapter.bridgeMode();
3379
3519
  if (mode === "app-server") {
3380
3520
  const bridgeScript = adapter.resolveBridgeScript?.(ctx);
3381
3521
  if (!bridgeScript) {
@@ -3383,36 +3523,34 @@ async function addCommand(args) {
3383
3523
  warnings.push("Bridge script not found. Run bridge manually.");
3384
3524
  } else {
3385
3525
  const { config: resolvedCfg } = resolveConfig({}, repoRoot);
3386
- {
3387
- log(`Starting bridge: ${bridgeScript}`);
3388
- try {
3389
- bridge = await startBridge({
3390
- instanceId,
3391
- runtime,
3392
- stateDir: ctx.stateDir,
3393
- commsDir: ctx.commsDir,
3394
- bridgeScript,
3395
- platform: ctx.platform,
3396
- agentName: agentNameFlag ?? void 0,
3397
- runtimeCommand: resolvedCfg.runtimeCommand,
3398
- appServerUrl: resolvedCfg.appServerUrl,
3399
- repoRoot,
3400
- port: port ?? void 0,
3401
- headless
3402
- });
3403
- logSuccess(`Bridge started (PID: ${bridge.pid})`);
3404
- } catch (err) {
3405
- const msg = err instanceof Error ? err.message : String(err);
3406
- logWarn(`Bridge not started: ${msg}`);
3407
- warnings.push(`Bridge not started: ${msg}`);
3408
- }
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}`);
3409
3547
  }
3410
3548
  }
3411
3549
  }
3412
3550
  const instanceState = {
3413
3551
  instanceId,
3414
3552
  runtime,
3415
- agentName: agentNameFlag ?? existingAgentName,
3553
+ agentName: resolvedAgentName,
3416
3554
  port,
3417
3555
  installed: true,
3418
3556
  configPath: probe.configPath ?? "",
@@ -3424,7 +3562,7 @@ async function addCommand(args) {
3424
3562
  lastVerifiedAt: verify.ok ? (/* @__PURE__ */ new Date()).toISOString() : null,
3425
3563
  bridge,
3426
3564
  headless,
3427
- warnings: [...result.warnings, ...verify.warnings]
3565
+ warnings: Array.from(/* @__PURE__ */ new Set([...result.warnings, ...verify.warnings]))
3428
3566
  };
3429
3567
  const newState = updateInstanceState(state, instanceId, instanceState);
3430
3568
  saveState(repoRoot, newState);
@@ -3432,6 +3570,13 @@ async function addCommand(args) {
3432
3570
  if (result.restartRequired) {
3433
3571
  logWarn(`Restart ${runtime} to pick up the new configuration.`);
3434
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
+ }
3435
3580
  logHeader("Done!");
3436
3581
  return {
3437
3582
  ok: true,
@@ -3451,6 +3596,16 @@ async function addCommand(args) {
3451
3596
  }
3452
3597
 
3453
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();
3454
3609
  function resolveStatus(inst, stateDir) {
3455
3610
  if (!inst.installed) return "not installed";
3456
3611
  switch (inst.bridgeMode) {
@@ -3477,7 +3632,18 @@ function instanceStatusLine(inst, status) {
3477
3632
  const warns = inst.warnings.length > 0 ? ` [${inst.warnings.length} warning(s)]` : "";
3478
3633
  return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
3479
3634
  }
3480
- 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
+ }
3481
3647
  const repoRoot = findRepoRoot();
3482
3648
  const state = loadState(repoRoot);
3483
3649
  if (!state) {
@@ -3706,7 +3872,32 @@ function cleanEmptyParents(obj, keyPath) {
3706
3872
  }
3707
3873
 
3708
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();
3709
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
+ }
3710
3901
  const identifier = args.find((a) => !a.startsWith("-"));
3711
3902
  if (!identifier) {
3712
3903
  return {
@@ -3742,8 +3933,8 @@ async function removeCommand(args) {
3742
3933
  };
3743
3934
  }
3744
3935
  const instanceId = resolved.instanceId;
3745
- const instance = state.instances[instanceId];
3746
- if (!instance?.installed) {
3936
+ const instance2 = state.instances[instanceId];
3937
+ if (!instance2?.installed) {
3747
3938
  return {
3748
3939
  ok: true,
3749
3940
  command: "remove",
@@ -3755,7 +3946,7 @@ async function removeCommand(args) {
3755
3946
  };
3756
3947
  }
3757
3948
  logHeader(`@hua-labs/tap remove ${instanceId}`);
3758
- if (instance.bridge) {
3949
+ if (instance2.bridge) {
3759
3950
  const ctx = createAdapterContext(state.commsDir, repoRoot);
3760
3951
  const stopped = await stopBridge({
3761
3952
  instanceId,
@@ -3768,7 +3959,7 @@ async function removeCommand(args) {
3768
3959
  log(`No running bridge for ${instanceId}`);
3769
3960
  }
3770
3961
  }
3771
- const result = await rollbackRuntime(instanceId, instance);
3962
+ const result = await rollbackRuntime(instanceId, instance2);
3772
3963
  if (result.success) {
3773
3964
  logSuccess(`Rolled back ${result.restoredCount} artifact(s)`);
3774
3965
  for (const f of result.restoredFiles) logSuccess(`Restored: ${f}`);
@@ -3780,7 +3971,7 @@ async function removeCommand(args) {
3780
3971
  ok: true,
3781
3972
  command: "remove",
3782
3973
  instanceId,
3783
- runtime: instance.runtime,
3974
+ runtime: instance2.runtime,
3784
3975
  code: "TAP_REMOVE_OK",
3785
3976
  message: `${instanceId} removed successfully`,
3786
3977
  warnings: [],
@@ -3795,7 +3986,7 @@ async function removeCommand(args) {
3795
3986
  ok: false,
3796
3987
  command: "remove",
3797
3988
  instanceId,
3798
- runtime: instance.runtime,
3989
+ runtime: instance2.runtime,
3799
3990
  code: "TAP_ROLLBACK_FAILED",
3800
3991
  message: "Rollback had errors. State preserved for retry.",
3801
3992
  warnings: result.errors,
@@ -3824,7 +4015,7 @@ Subcommands:
3824
4015
 
3825
4016
  Options:
3826
4017
  --agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
3827
- Saved to state \u2014 only needed on first start
4018
+ Overrides the stored name from 'tap add' when needed
3828
4019
  --all Start all registered app-server instances
3829
4020
  --busy-mode <steer|wait> How to handle active turns (default: steer)
3830
4021
  --poll-seconds <n> Inbox poll interval (default: 5)
@@ -3860,11 +4051,11 @@ function redactProtectedUrl(url) {
3860
4051
  try {
3861
4052
  const parsed = new URL(url);
3862
4053
  if (parsed.searchParams.has("tap_token")) {
3863
- parsed.searchParams.set("tap_token", "***");
4054
+ parsed.searchParams.delete("tap_token");
3864
4055
  }
3865
4056
  return parsed.toString().replace(/\/$/, "");
3866
4057
  } catch {
3867
- return url.replace(/tap_token=[^&]+/g, "tap_token=***");
4058
+ return url.replace(/[?&]tap_token=[^&]+/g, "");
3868
4059
  }
3869
4060
  }
3870
4061
  function loadCurrentBridgeState(stateDir, instanceId, fallback) {
@@ -3947,37 +4138,37 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3947
4138
  };
3948
4139
  }
3949
4140
  const instanceId = resolved.instanceId;
3950
- let instance = state.instances[instanceId];
3951
- if (!instance?.installed) {
4141
+ let instance2 = state.instances[instanceId];
4142
+ if (!instance2?.installed) {
3952
4143
  return {
3953
4144
  ok: false,
3954
4145
  command: "bridge",
3955
4146
  instanceId,
3956
- runtime: instance?.runtime,
4147
+ runtime: instance2?.runtime,
3957
4148
  code: "TAP_INSTANCE_NOT_FOUND",
3958
- 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}`,
3959
4150
  warnings: [],
3960
4151
  data: {}
3961
4152
  };
3962
4153
  }
3963
- const adapter = getAdapter(instance.runtime);
4154
+ const adapter = getAdapter(instance2.runtime);
3964
4155
  const mode = adapter.bridgeMode();
3965
4156
  if (mode !== "app-server") {
3966
4157
  return {
3967
4158
  ok: true,
3968
4159
  command: "bridge",
3969
4160
  instanceId,
3970
- runtime: instance.runtime,
4161
+ runtime: instance2.runtime,
3971
4162
  code: "TAP_NO_OP",
3972
4163
  message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
3973
4164
  warnings: [],
3974
4165
  data: { bridgeMode: mode }
3975
4166
  };
3976
4167
  }
3977
- const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
3978
- if (agentName && agentName !== instance.agentName) {
3979
- instance = { ...instance, agentName };
3980
- 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);
3981
4172
  saveState(repoRoot, updatedState);
3982
4173
  state = updatedState;
3983
4174
  }
@@ -3988,7 +4179,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3988
4179
  ok: false,
3989
4180
  command: "bridge",
3990
4181
  instanceId,
3991
- runtime: instance.runtime,
4182
+ runtime: instance2.runtime,
3992
4183
  code: "TAP_BRIDGE_SCRIPT_MISSING",
3993
4184
  message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
3994
4185
  warnings: [],
@@ -3997,8 +4188,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
3997
4188
  }
3998
4189
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
3999
4190
  const runtimeCommand = resolvedConfig.runtimeCommand;
4000
- const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
4001
- let effectivePort = instance.port;
4191
+ const manageAppServer = instance2.runtime === "codex" && flags["no-server"] !== true;
4192
+ let effectivePort = instance2.port;
4002
4193
  if (effectivePort == null && manageAppServer) {
4003
4194
  effectivePort = await findNextAvailableAppServerPort(
4004
4195
  state,
@@ -4006,8 +4197,8 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4006
4197
  4501,
4007
4198
  instanceId
4008
4199
  );
4009
- instance = { ...instance, port: effectivePort };
4010
- const updatedState = updateInstanceState(state, instanceId, instance);
4200
+ instance2 = { ...instance2, port: effectivePort };
4201
+ const updatedState = updateInstanceState(state, instanceId, instance2);
4011
4202
  saveState(repoRoot, updatedState);
4012
4203
  state = updatedState;
4013
4204
  }
@@ -4023,19 +4214,19 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4023
4214
  if (effectivePort != null) log(`Port: ${effectivePort}`);
4024
4215
  if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
4025
4216
  const noAuth = flags["no-auth"] === true;
4026
- if (!manageAppServer && instance.runtime === "codex") {
4217
+ if (!manageAppServer && instance2.runtime === "codex") {
4027
4218
  log("Auto server: disabled (--no-server)");
4028
4219
  }
4029
4220
  if (noAuth && manageAppServer) {
4030
4221
  log("Auth gateway: disabled (--no-auth)");
4031
4222
  }
4032
- const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
4223
+ const willBeHeadless = flags["headless"] === true || instance2.headless?.enabled;
4033
4224
  if (willBeHeadless) {
4034
- 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";
4035
4226
  log(`Headless: ${role}`);
4036
4227
  }
4037
4228
  try {
4038
- if (!manageAppServer && instance.runtime === "codex") {
4229
+ if (!manageAppServer && instance2.runtime === "codex") {
4039
4230
  log("Checking app-server health...");
4040
4231
  const healthy = await checkAppServerHealth(appServerUrl);
4041
4232
  if (healthy) {
@@ -4046,7 +4237,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4046
4237
  ok: false,
4047
4238
  command: "bridge",
4048
4239
  instanceId,
4049
- runtime: instance.runtime,
4240
+ runtime: instance2.runtime,
4050
4241
  code: "TAP_BRIDGE_START_FAILED",
4051
4242
  message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
4052
4243
  warnings: [],
@@ -4060,7 +4251,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4060
4251
  ok: false,
4061
4252
  command: "bridge",
4062
4253
  instanceId,
4063
- runtime: instance.runtime,
4254
+ runtime: instance2.runtime,
4064
4255
  code: "TAP_INVALID_ARGUMENT",
4065
4256
  message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
4066
4257
  warnings: [],
@@ -4068,9 +4259,38 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4068
4259
  };
4069
4260
  }
4070
4261
  const busyMode = busyModeRaw;
4071
- const pollSeconds = typeof flags["poll-seconds"] === "string" ? parseInt(flags["poll-seconds"], 10) : void 0;
4072
- const reconnectSeconds = typeof flags["reconnect-seconds"] === "string" ? parseInt(flags["reconnect-seconds"], 10) : void 0;
4073
- 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
+ }
4074
4294
  const threadId = typeof flags["thread-id"] === "string" ? flags["thread-id"] : void 0;
4075
4295
  const ephemeral = flags["ephemeral"] === true;
4076
4296
  const processExistingMessages = flags["process-existing-messages"] === true;
@@ -4082,7 +4302,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4082
4302
  ok: false,
4083
4303
  command: "bridge",
4084
4304
  instanceId,
4085
- runtime: instance.runtime,
4305
+ runtime: instance2.runtime,
4086
4306
  code: "TAP_INVALID_ARGUMENT",
4087
4307
  message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
4088
4308
  warnings: [],
@@ -4094,10 +4314,10 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4094
4314
  role: roleArg ?? "reviewer",
4095
4315
  maxRounds: 5,
4096
4316
  qualitySeverityFloor: "high"
4097
- } : instance.headless;
4317
+ } : instance2.headless;
4098
4318
  const bridge = await startBridge({
4099
4319
  instanceId,
4100
- runtime: instance.runtime,
4320
+ runtime: instance2.runtime,
4101
4321
  stateDir: ctx.stateDir,
4102
4322
  commsDir: ctx.commsDir,
4103
4323
  bridgeScript,
@@ -4138,14 +4358,14 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4138
4358
  log(`TUI connect: ${bridge.appServer.url}`);
4139
4359
  }
4140
4360
  }
4141
- const updated = { ...instance, bridge, manageAppServer, noAuth };
4361
+ const updated = { ...instance2, bridge, manageAppServer, noAuth };
4142
4362
  const newState = updateInstanceState(state, instanceId, updated);
4143
4363
  saveState(repoRoot, newState);
4144
4364
  return {
4145
4365
  ok: true,
4146
4366
  command: "bridge",
4147
4367
  instanceId,
4148
- runtime: instance.runtime,
4368
+ runtime: instance2.runtime,
4149
4369
  code: "TAP_BRIDGE_START_OK",
4150
4370
  message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
4151
4371
  warnings: [],
@@ -4158,7 +4378,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
4158
4378
  ok: false,
4159
4379
  command: "bridge",
4160
4380
  instanceId,
4161
- runtime: instance.runtime,
4381
+ runtime: instance2.runtime,
4162
4382
  code: "TAP_BRIDGE_START_FAILED",
4163
4383
  message: msg,
4164
4384
  warnings: [],
@@ -4260,11 +4480,11 @@ async function bridgeStopOne(identifier) {
4260
4480
  }
4261
4481
  const instanceId = resolved.instanceId;
4262
4482
  const ctx = createAdapterContext(state.commsDir, repoRoot);
4263
- const instance = state.instances[instanceId];
4483
+ const instance2 = state.instances[instanceId];
4264
4484
  const bridgeState = loadCurrentBridgeState(
4265
4485
  ctx.stateDir,
4266
4486
  instanceId,
4267
- instance?.bridge
4487
+ instance2?.bridge
4268
4488
  );
4269
4489
  const appServer = bridgeState?.appServer ?? null;
4270
4490
  logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
@@ -4312,8 +4532,8 @@ async function bridgeStopOne(identifier) {
4312
4532
  }
4313
4533
  }
4314
4534
  }
4315
- if (instance) {
4316
- const updated = { ...instance, bridge: null };
4535
+ if (instance2) {
4536
+ const updated = { ...instance2, bridge: null };
4317
4537
  const newState = updateInstanceState(state, instanceId, updated);
4318
4538
  saveState(repoRoot, newState);
4319
4539
  }
@@ -4385,9 +4605,9 @@ async function bridgeStopAll() {
4385
4605
  logSuccess(`Stopped bridge for ${instanceId}`);
4386
4606
  stopped.push(instanceId);
4387
4607
  }
4388
- const instance = state.instances[instanceId];
4389
- if (instance?.bridge) {
4390
- state.instances[instanceId] = { ...instance, bridge: null };
4608
+ const instance2 = state.instances[instanceId];
4609
+ if (instance2?.bridge) {
4610
+ state.instances[instanceId] = { ...instance2, bridge: null };
4391
4611
  stateChanged = true;
4392
4612
  }
4393
4613
  }
@@ -4683,8 +4903,22 @@ async function bridgeRestart(identifier, flags) {
4683
4903
  };
4684
4904
  }
4685
4905
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
4686
- const drainStr = typeof flags["drain-timeout"] === "string" ? flags["drain-timeout"] : "30";
4687
- 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
+ }
4688
4922
  logHeader(`@hua-labs/tap bridge restart ${instanceId}`);
4689
4923
  log(`Drain timeout: ${drainTimeout}s`);
4690
4924
  try {
@@ -4831,7 +5065,7 @@ async function bridgeCommand(args) {
4831
5065
  // src/engine/dashboard.ts
4832
5066
  import * as fs15 from "fs";
4833
5067
  import * as path15 from "path";
4834
- import { execSync as execSync5 } from "child_process";
5068
+ import { execSync as execSync4 } from "child_process";
4835
5069
  function collectAgents(commsDir) {
4836
5070
  const heartbeatsPath = path15.join(commsDir, "heartbeats.json");
4837
5071
  if (!fs15.existsSync(heartbeatsPath)) return [];
@@ -4910,7 +5144,7 @@ function collectBridges(repoRoot) {
4910
5144
  }
4911
5145
  function collectPRs() {
4912
5146
  try {
4913
- const output = execSync5(
5147
+ const output = execSync4(
4914
5148
  "gh pr list --state all --limit 10 --json number,title,author,state,url",
4915
5149
  { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
4916
5150
  );
@@ -5084,7 +5318,34 @@ async function downCommand(args) {
5084
5318
  // src/commands/serve.ts
5085
5319
  import * as path16 from "path";
5086
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();
5087
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
+ }
5088
5349
  const repoRoot = findRepoRoot();
5089
5350
  let commsDir;
5090
5351
  const commsDirIdx = args.indexOf("--comms-dir");
@@ -5159,7 +5420,7 @@ async function serveCommand(args) {
5159
5420
  // src/commands/init-worktree.ts
5160
5421
  import * as fs16 from "fs";
5161
5422
  import * as path17 from "path";
5162
- import { execSync as execSync6 } from "child_process";
5423
+ import { execSync as execSync5 } from "child_process";
5163
5424
  var INIT_WORKTREE_HELP = `
5164
5425
  Usage:
5165
5426
  tap-comms init-worktree [options]
@@ -5183,7 +5444,7 @@ function warn(warnings, message) {
5183
5444
  }
5184
5445
  function run(cmd, opts) {
5185
5446
  try {
5186
- return execSync6(cmd, {
5447
+ return execSync5(cmd, {
5187
5448
  cwd: opts?.cwd,
5188
5449
  encoding: "utf-8",
5189
5450
  stdio: ["pipe", "pipe", "pipe"],
@@ -5200,7 +5461,7 @@ function toAbsolute(p) {
5200
5461
  }
5201
5462
  function probeBun(candidate) {
5202
5463
  try {
5203
- const out = execSync6(`"${candidate}" --version`, {
5464
+ const out = execSync5(`"${candidate}" --version`, {
5204
5465
  encoding: "utf-8",
5205
5466
  stdio: ["pipe", "pipe", "pipe"],
5206
5467
  timeout: 5e3
@@ -5214,7 +5475,7 @@ function findBun() {
5214
5475
  const candidates = process.platform === "win32" ? ["bun.exe", "bun"] : ["bun"];
5215
5476
  for (const name of candidates) {
5216
5477
  try {
5217
- const out = execSync6(
5478
+ const out = execSync5(
5218
5479
  process.platform === "win32" ? `where ${name}` : `which ${name}`,
5219
5480
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5e3 }
5220
5481
  ).trim();
@@ -5445,7 +5706,7 @@ async function initWorktreeCommand(args) {
5445
5706
  ok: true,
5446
5707
  command: "init-worktree",
5447
5708
  code: "TAP_NO_OP",
5448
- message: "init-worktree help",
5709
+ message: INIT_WORKTREE_HELP,
5449
5710
  warnings: [],
5450
5711
  data: {}
5451
5712
  };
@@ -5607,8 +5868,38 @@ function renderSnapshot(snapshot) {
5607
5868
  }
5608
5869
  }
5609
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();
5610
5890
  async function dashboardCommand(args) {
5611
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
+ }
5612
5903
  const jsonMode = flags["json"] === true;
5613
5904
  const watchMode = flags["watch"] === true;
5614
5905
  const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : "5";
@@ -5671,7 +5962,7 @@ import {
5671
5962
  statSync as statSync2,
5672
5963
  unlinkSync as unlinkSync3
5673
5964
  } from "fs";
5674
- import { execSync as execSync7 } from "child_process";
5965
+ import { execSync as execSync6 } from "child_process";
5675
5966
  import { join as join17 } from "path";
5676
5967
  var PASS = "pass";
5677
5968
  var WARN = "warn";
@@ -5938,7 +6229,7 @@ function checkMcpServer(repoRoot) {
5938
6229
  let cmdAvailable = existsSync16(cmd);
5939
6230
  if (!cmdAvailable) {
5940
6231
  try {
5941
- execSync7(`"${cmd}" --version`, {
6232
+ execSync6(`"${cmd}" --version`, {
5942
6233
  stdio: "pipe",
5943
6234
  timeout: 5e3
5944
6235
  });
@@ -6115,7 +6406,35 @@ function renderCheck(check, fixMode) {
6115
6406
  const msg = check.message ? ` \u2014 ${check.message}${fixable}` : "";
6116
6407
  return ` ${icon} ${check.name}${msg}`;
6117
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();
6118
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
+ }
6119
6438
  const repoRoot = findRepoRoot();
6120
6439
  const overrides = {};
6121
6440
  let fixMode = false;
@@ -6228,7 +6547,7 @@ async function doctorCommand(args) {
6228
6547
  }
6229
6548
 
6230
6549
  // src/commands/comms.ts
6231
- import { execSync as execSync8 } from "child_process";
6550
+ import { execSync as execSync7, spawnSync as spawnSync4 } from "child_process";
6232
6551
  import * as fs17 from "fs";
6233
6552
  import * as path18 from "path";
6234
6553
  var COMMS_HELP = `
@@ -6260,7 +6579,7 @@ function commsPull(commsDir) {
6260
6579
  };
6261
6580
  }
6262
6581
  try {
6263
- const output = execSync8("git pull --rebase", {
6582
+ const output = execSync7("git pull --rebase", {
6264
6583
  cwd: commsDir,
6265
6584
  encoding: "utf-8",
6266
6585
  stdio: "pipe"
@@ -6302,8 +6621,8 @@ function commsPush(commsDir) {
6302
6621
  };
6303
6622
  }
6304
6623
  try {
6305
- execSync8("git add -A", { cwd: commsDir, stdio: "pipe" });
6306
- const status = execSync8("git status --porcelain", {
6624
+ execSync7("git add -A", { cwd: commsDir, stdio: "pipe" });
6625
+ const status = execSync7("git status --porcelain", {
6307
6626
  cwd: commsDir,
6308
6627
  encoding: "utf-8",
6309
6628
  stdio: "pipe"
@@ -6320,11 +6639,23 @@ function commsPush(commsDir) {
6320
6639
  };
6321
6640
  }
6322
6641
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6323
- execSync8(`git commit -m "chore(comms): sync ${timestamp}"`, {
6324
- cwd: commsDir,
6325
- stdio: "pipe"
6326
- });
6327
- 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" });
6328
6659
  logSuccess("Comms push complete");
6329
6660
  return {
6330
6661
  ok: true,
@@ -6390,7 +6721,12 @@ function emitResult(result, jsonMode) {
6390
6721
  } else {
6391
6722
  logError(result.message);
6392
6723
  }
6724
+ const emittedWarnings = /* @__PURE__ */ new Set();
6393
6725
  for (const w of result.warnings) {
6726
+ if (emittedWarnings.has(w) || wasWarningLogged(w)) {
6727
+ continue;
6728
+ }
6729
+ emittedWarnings.add(w);
6394
6730
  logWarn(w);
6395
6731
  }
6396
6732
  }
@@ -6459,6 +6795,7 @@ function normalizeCommandName(command) {
6459
6795
  async function main() {
6460
6796
  const rawArgs = process.argv.slice(2);
6461
6797
  const { jsonMode, cleanArgs } = extractJsonFlag(rawArgs);
6798
+ resetLoggedWarnings();
6462
6799
  setJsonMode(jsonMode);
6463
6800
  const command = cleanArgs[0];
6464
6801
  if (!command || command === "--help" || command === "-h") {
@@ -6516,7 +6853,7 @@ async function main() {
6516
6853
  break;
6517
6854
  case "serve": {
6518
6855
  const serveResult = await serveCommand(commandArgs);
6519
- if (!serveResult.ok) {
6856
+ if (!serveResult.ok || serveResult.code === "TAP_NO_OP") {
6520
6857
  emitResult(serveResult, jsonMode);
6521
6858
  }
6522
6859
  process.exit(exitCode(serveResult));