@hua-labs/tap 0.1.1 → 0.2.0

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,49 +1,218 @@
1
1
  // src/commands/init.ts
2
- import * as fs5 from "fs";
3
- import * as path5 from "path";
2
+ import * as fs6 from "fs";
3
+ import * as path6 from "path";
4
4
 
5
5
  // src/state.ts
6
- import * as fs2 from "fs";
7
- import * as path2 from "path";
6
+ import * as fs3 from "fs";
7
+ import * as path3 from "path";
8
8
  import * as crypto from "crypto";
9
9
 
10
10
  // src/config/resolve.ts
11
+ import * as fs2 from "fs";
12
+ import * as path2 from "path";
13
+
14
+ // src/utils.ts
11
15
  import * as fs from "fs";
12
16
  import * as path from "path";
13
- var SHARED_CONFIG_FILE = "tap-config.json";
14
- var LOCAL_CONFIG_FILE = "tap-config.local.json";
15
- var DEFAULT_RUNTIME_COMMAND = "node";
16
- var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
17
+ var VALID_RUNTIMES = ["claude", "codex", "gemini"];
18
+ function isValidRuntime(name) {
19
+ return VALID_RUNTIMES.includes(name);
20
+ }
21
+ function detectPlatform() {
22
+ return process.platform;
23
+ }
24
+ var _noGitWarned = false;
25
+ function _setNoGitWarned() {
26
+ _noGitWarned = true;
27
+ }
17
28
  function findRepoRoot(startDir = process.cwd()) {
18
29
  let dir = path.resolve(startDir);
19
30
  while (true) {
20
31
  if (fs.existsSync(path.join(dir, ".git"))) return dir;
21
- if (fs.existsSync(path.join(dir, "package.json"))) return dir;
32
+ if (fs.existsSync(path.join(dir, "package.json"))) {
33
+ if (!_noGitWarned) {
34
+ _setNoGitWarned();
35
+ logWarn(
36
+ "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."
37
+ );
38
+ }
39
+ return dir;
40
+ }
22
41
  const parent = path.dirname(dir);
23
42
  if (parent === dir) break;
24
43
  dir = parent;
25
44
  }
45
+ if (!_noGitWarned) {
46
+ _setNoGitWarned();
47
+ logWarn(
48
+ "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."
49
+ );
50
+ }
51
+ return process.cwd();
52
+ }
53
+ function resolveCommsDir(args, repoRoot) {
54
+ const idx = args.indexOf("--comms-dir");
55
+ if (idx !== -1 && args[idx + 1]) {
56
+ return path.resolve(args[idx + 1]);
57
+ }
58
+ const { config } = resolveConfig({}, repoRoot);
59
+ return config.commsDir;
60
+ }
61
+ function createAdapterContext(commsDir, repoRoot) {
62
+ const { config } = resolveConfig({}, repoRoot);
63
+ return {
64
+ commsDir: path.resolve(commsDir),
65
+ repoRoot: path.resolve(repoRoot),
66
+ stateDir: config.stateDir,
67
+ platform: detectPlatform()
68
+ };
69
+ }
70
+ function parseArgs(args) {
71
+ const positional = [];
72
+ const flags = {};
73
+ for (let i = 0; i < args.length; i++) {
74
+ const arg = args[i];
75
+ if (arg.startsWith("--")) {
76
+ const key = arg.slice(2);
77
+ const next = args[i + 1];
78
+ if (next && !next.startsWith("--")) {
79
+ flags[key] = next;
80
+ i++;
81
+ } else {
82
+ flags[key] = true;
83
+ }
84
+ } else if (arg.startsWith("-")) {
85
+ flags[arg.slice(1)] = true;
86
+ } else {
87
+ positional.push(arg);
88
+ }
89
+ }
90
+ return { positional, flags };
91
+ }
92
+ var _jsonMode = false;
93
+ function setJsonMode(enabled) {
94
+ _jsonMode = enabled;
95
+ }
96
+ function log(message) {
97
+ if (!_jsonMode) console.log(` ${message}`);
98
+ }
99
+ function logSuccess(message) {
100
+ if (!_jsonMode) console.log(` + ${message}`);
101
+ }
102
+ function logWarn(message) {
103
+ if (!_jsonMode) console.log(` ! ${message}`);
104
+ }
105
+ function logError(message) {
106
+ if (!_jsonMode) console.error(` x ${message}`);
107
+ }
108
+ function logHeader(message) {
109
+ if (!_jsonMode) console.log(`
110
+ ${message}
111
+ `);
112
+ }
113
+ function resolveInstanceId(identifier, state) {
114
+ if (state.instances[identifier]) {
115
+ return { ok: true, instanceId: identifier };
116
+ }
117
+ if (isValidRuntime(identifier)) {
118
+ const matches = Object.values(state.instances).filter(
119
+ (inst) => inst.runtime === identifier
120
+ );
121
+ if (matches.length === 1) {
122
+ return { ok: true, instanceId: matches[0].instanceId };
123
+ }
124
+ if (matches.length > 1) {
125
+ const ids = matches.map((m) => m.instanceId).join(", ");
126
+ return {
127
+ ok: false,
128
+ code: "TAP_INSTANCE_AMBIGUOUS",
129
+ message: `Multiple ${identifier} instances found: ${ids}. Specify one explicitly.`
130
+ };
131
+ }
132
+ }
133
+ return {
134
+ ok: false,
135
+ code: "TAP_INSTANCE_NOT_FOUND",
136
+ message: `Instance not found: ${identifier}`
137
+ };
138
+ }
139
+ function buildInstanceId(runtime, name) {
140
+ return name ? `${runtime}-${name}` : runtime;
141
+ }
142
+ function findPortConflict(state, port, excludeInstanceId) {
143
+ for (const [id, inst] of Object.entries(state.instances)) {
144
+ if (id !== excludeInstanceId && inst.port === port) return id;
145
+ }
146
+ return null;
147
+ }
148
+
149
+ // src/config/resolve.ts
150
+ var SHARED_CONFIG_FILE = "tap-config.json";
151
+ var LOCAL_CONFIG_FILE = "tap-config.local.json";
152
+ var LEGACY_CONFIG_FILE = ".tap-config";
153
+ var DEFAULT_RUNTIME_COMMAND = "node";
154
+ var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
155
+ function findRepoRoot2(startDir = process.cwd()) {
156
+ let dir = path2.resolve(startDir);
157
+ while (true) {
158
+ if (fs2.existsSync(path2.join(dir, ".git"))) return dir;
159
+ if (fs2.existsSync(path2.join(dir, "package.json"))) {
160
+ if (!_noGitWarned) {
161
+ _setNoGitWarned();
162
+ console.error(
163
+ "[tap] warning: No .git directory found. Resolved via package.json. Use --comms-dir to specify explicitly."
164
+ );
165
+ }
166
+ return dir;
167
+ }
168
+ const parent = path2.dirname(dir);
169
+ if (parent === dir) break;
170
+ dir = parent;
171
+ }
172
+ if (!_noGitWarned) {
173
+ _setNoGitWarned();
174
+ console.error(
175
+ "[tap] warning: No git repository found. Using cwd as root. Run 'git init' or use --comms-dir."
176
+ );
177
+ }
26
178
  return process.cwd();
27
179
  }
28
180
  function loadJsonFile(filePath) {
29
- if (!fs.existsSync(filePath)) return null;
181
+ if (!fs2.existsSync(filePath)) return null;
30
182
  try {
31
- const raw = fs.readFileSync(filePath, "utf-8");
183
+ const raw = fs2.readFileSync(filePath, "utf-8");
32
184
  return JSON.parse(raw);
33
185
  } catch {
34
186
  return null;
35
187
  }
36
188
  }
37
189
  function loadSharedConfig(repoRoot) {
38
- return loadJsonFile(path.join(repoRoot, SHARED_CONFIG_FILE));
190
+ return loadJsonFile(path2.join(repoRoot, SHARED_CONFIG_FILE));
39
191
  }
40
192
  function loadLocalConfig(repoRoot) {
41
- return loadJsonFile(path.join(repoRoot, LOCAL_CONFIG_FILE));
193
+ return loadJsonFile(path2.join(repoRoot, LOCAL_CONFIG_FILE));
194
+ }
195
+ function readLegacyShellValue(configText, key) {
196
+ const match = configText.match(new RegExp(`^${key}="?(.+?)"?$`, "m"));
197
+ return match?.[1]?.trim() || null;
198
+ }
199
+ function loadLegacyShellConfig(repoRoot) {
200
+ const filePath = path2.join(repoRoot, LEGACY_CONFIG_FILE);
201
+ if (!fs2.existsSync(filePath)) return null;
202
+ try {
203
+ const raw = fs2.readFileSync(filePath, "utf-8");
204
+ const commsDir = readLegacyShellValue(raw, "TAP_COMMS_DIR");
205
+ if (!commsDir) return null;
206
+ return { commsDir };
207
+ } catch {
208
+ return null;
209
+ }
42
210
  }
43
211
  function resolveConfig(overrides = {}, startDir) {
44
- const repoRoot = findRepoRoot(startDir);
212
+ const repoRoot = findRepoRoot2(startDir);
45
213
  const shared = loadSharedConfig(repoRoot) ?? {};
46
214
  const local = loadLocalConfig(repoRoot) ?? {};
215
+ const legacy = loadLegacyShellConfig(repoRoot) ?? {};
47
216
  const sources = {
48
217
  repoRoot: "auto",
49
218
  commsDir: "auto",
@@ -53,10 +222,10 @@ function resolveConfig(overrides = {}, startDir) {
53
222
  };
54
223
  let commsDir;
55
224
  if (overrides.commsDir) {
56
- commsDir = path.resolve(overrides.commsDir);
225
+ commsDir = resolvePath(repoRoot, overrides.commsDir);
57
226
  sources.commsDir = "cli-flag";
58
227
  } else if (process.env.TAP_COMMS_DIR) {
59
- commsDir = path.resolve(process.env.TAP_COMMS_DIR);
228
+ commsDir = resolvePath(repoRoot, process.env.TAP_COMMS_DIR);
60
229
  sources.commsDir = "env";
61
230
  } else if (local.commsDir) {
62
231
  commsDir = resolvePath(repoRoot, local.commsDir);
@@ -64,15 +233,18 @@ function resolveConfig(overrides = {}, startDir) {
64
233
  } else if (shared.commsDir) {
65
234
  commsDir = resolvePath(repoRoot, shared.commsDir);
66
235
  sources.commsDir = "shared-config";
236
+ } else if (legacy.commsDir) {
237
+ commsDir = resolvePath(repoRoot, legacy.commsDir);
238
+ sources.commsDir = "legacy-shell-config";
67
239
  } else {
68
- commsDir = path.join(path.dirname(repoRoot), "tap-comms");
240
+ commsDir = path2.join(repoRoot, "tap-comms");
69
241
  }
70
242
  let stateDir;
71
243
  if (overrides.stateDir) {
72
- stateDir = path.resolve(overrides.stateDir);
244
+ stateDir = resolvePath(repoRoot, overrides.stateDir);
73
245
  sources.stateDir = "cli-flag";
74
246
  } else if (process.env.TAP_STATE_DIR) {
75
- stateDir = path.resolve(process.env.TAP_STATE_DIR);
247
+ stateDir = resolvePath(repoRoot, process.env.TAP_STATE_DIR);
76
248
  sources.stateDir = "env";
77
249
  } else if (local.stateDir) {
78
250
  stateDir = resolvePath(repoRoot, local.stateDir);
@@ -81,7 +253,7 @@ function resolveConfig(overrides = {}, startDir) {
81
253
  stateDir = resolvePath(repoRoot, shared.stateDir);
82
254
  sources.stateDir = "shared-config";
83
255
  } else {
84
- stateDir = path.join(repoRoot, ".tap-comms");
256
+ stateDir = path2.join(repoRoot, ".tap-comms");
85
257
  }
86
258
  let runtimeCommand;
87
259
  if (overrides.runtimeCommand) {
@@ -121,7 +293,21 @@ function resolveConfig(overrides = {}, startDir) {
121
293
  };
122
294
  }
123
295
  function resolvePath(repoRoot, p) {
124
- return path.isAbsolute(p) ? p : path.resolve(repoRoot, p);
296
+ const normalized = normalizeTapPath(p);
297
+ return path2.isAbsolute(normalized) ? normalized : path2.resolve(repoRoot, normalized);
298
+ }
299
+ function normalizeTapPath(input) {
300
+ const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
301
+ if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
302
+ return trimmed;
303
+ }
304
+ if (process.platform === "win32") {
305
+ const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
306
+ if (match) {
307
+ return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
308
+ }
309
+ }
310
+ return trimmed;
125
311
  }
126
312
 
127
313
  // src/state.ts
@@ -132,10 +318,10 @@ function getStateDir(repoRoot) {
132
318
  return config.stateDir;
133
319
  }
134
320
  function getStatePath(repoRoot) {
135
- return path2.join(getStateDir(repoRoot), STATE_FILE);
321
+ return path3.join(getStateDir(repoRoot), STATE_FILE);
136
322
  }
137
323
  function stateExists(repoRoot) {
138
- return fs2.existsSync(getStatePath(repoRoot));
324
+ return fs3.existsSync(getStatePath(repoRoot));
139
325
  }
140
326
  function migrateStateV1toV2(v1) {
141
327
  const instances = {};
@@ -163,8 +349,8 @@ function migrateStateV1toV2(v1) {
163
349
  }
164
350
  function loadState(repoRoot) {
165
351
  const statePath = getStatePath(repoRoot);
166
- if (!fs2.existsSync(statePath)) return null;
167
- const raw = fs2.readFileSync(statePath, "utf-8");
352
+ if (!fs3.existsSync(statePath)) return null;
353
+ const raw = fs3.readFileSync(statePath, "utf-8");
168
354
  const parsed = JSON.parse(raw);
169
355
  if (parsed.schemaVersion === 1 || parsed.runtimes) {
170
356
  const migrated = migrateStateV1toV2(parsed);
@@ -175,11 +361,11 @@ function loadState(repoRoot) {
175
361
  }
176
362
  function saveState(repoRoot, state) {
177
363
  const stateDir = getStateDir(repoRoot);
178
- fs2.mkdirSync(stateDir, { recursive: true });
364
+ fs3.mkdirSync(stateDir, { recursive: true });
179
365
  const statePath = getStatePath(repoRoot);
180
366
  const tmp = `${statePath}.tmp.${process.pid}`;
181
- fs2.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
182
- fs2.renameSync(tmp, statePath);
367
+ fs3.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
368
+ fs3.renameSync(tmp, statePath);
183
369
  }
184
370
  function createInitialState(commsDir, repoRoot, packageVersion) {
185
371
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -187,8 +373,8 @@ function createInitialState(commsDir, repoRoot, packageVersion) {
187
373
  schemaVersion: SCHEMA_VERSION,
188
374
  createdAt: now,
189
375
  updatedAt: now,
190
- commsDir: path2.resolve(commsDir),
191
- repoRoot: path2.resolve(repoRoot),
376
+ commsDir: path3.resolve(commsDir),
377
+ repoRoot: path3.resolve(repoRoot),
192
378
  packageVersion,
193
379
  instances: {}
194
380
  };
@@ -217,186 +403,85 @@ function getInstalledInstances(state) {
217
403
  );
218
404
  }
219
405
  function ensureBackupDir(stateDir, instanceId) {
220
- const backupDir = path2.join(stateDir, "backups", instanceId);
221
- fs2.mkdirSync(backupDir, { recursive: true });
406
+ const backupDir = path3.join(stateDir, "backups", instanceId);
407
+ fs3.mkdirSync(backupDir, { recursive: true });
222
408
  return backupDir;
223
409
  }
224
410
  function backupFile(filePath, backupDir) {
225
- const basename3 = path2.basename(filePath);
411
+ const basename3 = path3.basename(filePath);
226
412
  const hash = fileHash(filePath);
227
- const backupPath = path2.join(backupDir, `${basename3}.${hash}.bak`);
228
- fs2.copyFileSync(filePath, backupPath);
413
+ const backupPath = path3.join(backupDir, `${basename3}.${hash}.bak`);
414
+ fs3.copyFileSync(filePath, backupPath);
229
415
  return backupPath;
230
416
  }
231
417
  function fileHash(filePath) {
232
- if (!fs2.existsSync(filePath)) return "";
233
- const content = fs2.readFileSync(filePath);
418
+ if (!fs3.existsSync(filePath)) return "";
419
+ const content = fs3.readFileSync(filePath);
234
420
  return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
235
421
  }
236
422
 
237
- // src/utils.ts
238
- import * as fs3 from "fs";
239
- import * as path3 from "path";
240
- var VALID_RUNTIMES = ["claude", "codex", "gemini"];
241
- function isValidRuntime(name) {
242
- return VALID_RUNTIMES.includes(name);
243
- }
244
- function detectPlatform() {
245
- return process.platform;
246
- }
247
- function findRepoRoot2(startDir = process.cwd()) {
248
- let dir = path3.resolve(startDir);
249
- while (true) {
250
- if (fs3.existsSync(path3.join(dir, ".git"))) return dir;
251
- if (fs3.existsSync(path3.join(dir, "package.json"))) return dir;
252
- const parent = path3.dirname(dir);
253
- if (parent === dir) break;
254
- dir = parent;
423
+ // src/version.ts
424
+ import * as fs4 from "fs";
425
+ import * as path4 from "path";
426
+ import { fileURLToPath } from "url";
427
+ var FALLBACK_VERSION = "0.0.0";
428
+ function resolvePackageVersion(metaUrl = import.meta.url) {
429
+ const moduleDir = path4.dirname(fileURLToPath(metaUrl));
430
+ const packageJsonPath = path4.join(moduleDir, "..", "package.json");
431
+ try {
432
+ const parsed = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
433
+ if (typeof parsed.version === "string" && parsed.version.trim()) {
434
+ return parsed.version;
435
+ }
436
+ } catch {
255
437
  }
256
- return process.cwd();
438
+ return FALLBACK_VERSION;
257
439
  }
258
- function resolveCommsDir(args, repoRoot) {
259
- const idx = args.indexOf("--comms-dir");
260
- if (idx !== -1 && args[idx + 1]) {
261
- return path3.resolve(args[idx + 1]);
262
- }
263
- const { config } = resolveConfig({}, repoRoot);
264
- return config.commsDir;
440
+ var version = resolvePackageVersion();
441
+
442
+ // src/permissions.ts
443
+ import * as fs5 from "fs";
444
+ import * as path5 from "path";
445
+ import * as os from "os";
446
+
447
+ // src/toml.ts
448
+ function splitLines(content) {
449
+ return content.replace(/\r\n/g, "\n").split("\n");
265
450
  }
266
- function createAdapterContext(commsDir, repoRoot) {
267
- const { config } = resolveConfig({}, repoRoot);
268
- return {
269
- commsDir: path3.resolve(commsDir),
270
- repoRoot: path3.resolve(repoRoot),
271
- stateDir: config.stateDir,
272
- platform: detectPlatform()
273
- };
451
+ function tableHeader(selector) {
452
+ return `[${selector}]`;
274
453
  }
275
- function parseArgs(args) {
276
- const positional = [];
277
- const flags = {};
278
- for (let i = 0; i < args.length; i++) {
279
- const arg = args[i];
280
- if (arg.startsWith("--")) {
281
- const key = arg.slice(2);
282
- const next = args[i + 1];
283
- if (next && !next.startsWith("--")) {
284
- flags[key] = next;
285
- i++;
286
- } else {
287
- flags[key] = true;
454
+ function findTableRange(lines, selector) {
455
+ const header = tableHeader(selector);
456
+ for (let i = 0; i < lines.length; i++) {
457
+ if (lines[i].trim() !== header) continue;
458
+ let end = lines.length;
459
+ for (let j = i + 1; j < lines.length; j++) {
460
+ const trimmed = lines[j].trim();
461
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
462
+ end = j;
463
+ break;
288
464
  }
289
- } else if (arg.startsWith("-")) {
290
- flags[arg.slice(1)] = true;
291
- } else {
292
- positional.push(arg);
293
465
  }
466
+ return { start: i, end };
294
467
  }
295
- return { positional, flags };
296
- }
297
- var _jsonMode = false;
298
- function setJsonMode(enabled) {
299
- _jsonMode = enabled;
468
+ return null;
300
469
  }
301
- function log(message) {
302
- if (!_jsonMode) console.log(` ${message}`);
470
+ function escapeBasicString(value) {
471
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
303
472
  }
304
- function logSuccess(message) {
305
- if (!_jsonMode) console.log(` + ${message}`);
473
+ function renderValue(value) {
474
+ if (Array.isArray(value)) {
475
+ return `[${value.map((item) => `"${escapeBasicString(item)}"`).join(", ")}]`;
476
+ }
477
+ return `"${escapeBasicString(value)}"`;
306
478
  }
307
- function logWarn(message) {
308
- if (!_jsonMode) console.log(` ! ${message}`);
309
- }
310
- function logError(message) {
311
- if (!_jsonMode) console.error(` x ${message}`);
312
- }
313
- function logHeader(message) {
314
- if (!_jsonMode) console.log(`
315
- ${message}
316
- `);
317
- }
318
- function resolveInstanceId(identifier, state) {
319
- if (state.instances[identifier]) {
320
- return { ok: true, instanceId: identifier };
321
- }
322
- if (isValidRuntime(identifier)) {
323
- const matches = Object.values(state.instances).filter(
324
- (inst) => inst.runtime === identifier
325
- );
326
- if (matches.length === 1) {
327
- return { ok: true, instanceId: matches[0].instanceId };
328
- }
329
- if (matches.length > 1) {
330
- const ids = matches.map((m) => m.instanceId).join(", ");
331
- return {
332
- ok: false,
333
- code: "TAP_INSTANCE_AMBIGUOUS",
334
- message: `Multiple ${identifier} instances found: ${ids}. Specify one explicitly.`
335
- };
336
- }
337
- }
338
- return {
339
- ok: false,
340
- code: "TAP_INSTANCE_NOT_FOUND",
341
- message: `Instance not found: ${identifier}`
342
- };
343
- }
344
- function buildInstanceId(runtime, name) {
345
- return name ? `${runtime}-${name}` : runtime;
346
- }
347
- function findPortConflict(state, port, excludeInstanceId) {
348
- for (const [id, inst] of Object.entries(state.instances)) {
349
- if (id !== excludeInstanceId && inst.port === port) return id;
350
- }
351
- return null;
352
- }
353
-
354
- // src/version.ts
355
- var version = "0.1.0";
356
-
357
- // src/permissions.ts
358
- import * as fs4 from "fs";
359
- import * as path4 from "path";
360
- import * as os from "os";
361
-
362
- // src/toml.ts
363
- function splitLines(content) {
364
- return content.replace(/\r\n/g, "\n").split("\n");
365
- }
366
- function tableHeader(selector) {
367
- return `[${selector}]`;
368
- }
369
- function findTableRange(lines, selector) {
370
- const header = tableHeader(selector);
371
- for (let i = 0; i < lines.length; i++) {
372
- if (lines[i].trim() !== header) continue;
373
- let end = lines.length;
374
- for (let j = i + 1; j < lines.length; j++) {
375
- const trimmed = lines[j].trim();
376
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
377
- end = j;
378
- break;
379
- }
380
- }
381
- return { start: i, end };
382
- }
383
- return null;
384
- }
385
- function escapeBasicString(value) {
386
- return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
387
- }
388
- function renderValue(value) {
389
- if (Array.isArray(value)) {
390
- return `[${value.map((item) => `"${escapeBasicString(item)}"`).join(", ")}]`;
391
- }
392
- return `"${escapeBasicString(value)}"`;
393
- }
394
- function extractTomlTable(content, selector) {
395
- const lines = splitLines(content);
396
- const range = findTableRange(lines, selector);
397
- if (!range) return null;
398
- return `${lines.slice(range.start, range.end).join("\n")}
399
- `;
479
+ function extractTomlTable(content, selector) {
480
+ const lines = splitLines(content);
481
+ const range = findTableRange(lines, selector);
482
+ if (!range) return null;
483
+ return `${lines.slice(range.start, range.end).join("\n")}
484
+ `;
400
485
  }
401
486
  function removeTomlTable(content, selector) {
402
487
  const lines = splitLines(content);
@@ -486,13 +571,13 @@ var CLAUDE_DENY_RULES = [
486
571
  ];
487
572
  function applyClaudePermissions(repoRoot, mode) {
488
573
  const warnings = [];
489
- const claudeDir = path4.join(repoRoot, ".claude");
490
- const settingsPath = path4.join(claudeDir, "settings.local.json");
491
- fs4.mkdirSync(claudeDir, { recursive: true });
574
+ const claudeDir = path5.join(repoRoot, ".claude");
575
+ const settingsPath = path5.join(claudeDir, "settings.local.json");
576
+ fs5.mkdirSync(claudeDir, { recursive: true });
492
577
  let settings = {};
493
- if (fs4.existsSync(settingsPath)) {
578
+ if (fs5.existsSync(settingsPath)) {
494
579
  try {
495
- settings = JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
580
+ settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
496
581
  } catch {
497
582
  warnings.push(
498
583
  ".claude/settings.local.json was invalid JSON. Starting fresh."
@@ -506,8 +591,8 @@ function applyClaudePermissions(repoRoot, mode) {
506
591
  const cleaned = existingDeny.filter((r) => !tapRuleSet.has(r));
507
592
  settings.deny = cleaned;
508
593
  const tmp2 = `${settingsPath}.tmp.${process.pid}`;
509
- fs4.writeFileSync(tmp2, JSON.stringify(settings, null, 2) + "\n", "utf-8");
510
- fs4.renameSync(tmp2, settingsPath);
594
+ fs5.writeFileSync(tmp2, JSON.stringify(settings, null, 2) + "\n", "utf-8");
595
+ fs5.renameSync(tmp2, settingsPath);
511
596
  logWarn("Claude: full mode \u2014 tap deny rules removed. Use with caution.");
512
597
  warnings.push("Full permission mode: tap deny rules removed.");
513
598
  return { applied: true, warnings };
@@ -515,18 +600,18 @@ function applyClaudePermissions(repoRoot, mode) {
515
600
  const newDeny = [.../* @__PURE__ */ new Set([...existingDeny, ...CLAUDE_DENY_RULES])];
516
601
  settings.deny = newDeny;
517
602
  const tmp = `${settingsPath}.tmp.${process.pid}`;
518
- fs4.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", "utf-8");
519
- fs4.renameSync(tmp, settingsPath);
603
+ fs5.writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", "utf-8");
604
+ fs5.renameSync(tmp, settingsPath);
520
605
  logSuccess(
521
606
  `Claude: ${CLAUDE_DENY_RULES.length} deny rules applied to .claude/settings.local.json`
522
607
  );
523
608
  return { applied: true, warnings };
524
609
  }
525
610
  function findCodexConfigPath() {
526
- return path4.join(os.homedir(), ".codex", "config.toml");
611
+ return path5.join(os.homedir(), ".codex", "config.toml");
527
612
  }
528
613
  function canonicalizeTrustPath(targetPath) {
529
- let resolved = path4.resolve(targetPath).replace(/\//g, "\\");
614
+ let resolved = path5.resolve(targetPath).replace(/\//g, "\\");
530
615
  const driveRoot = /^[A-Za-z]:\\$/;
531
616
  if (!driveRoot.test(resolved)) {
532
617
  resolved = resolved.replace(/\\+$/g, "");
@@ -536,10 +621,10 @@ function canonicalizeTrustPath(targetPath) {
536
621
  function applyCodexPermissions(repoRoot, commsDir, mode) {
537
622
  const warnings = [];
538
623
  const configPath = findCodexConfigPath();
539
- fs4.mkdirSync(path4.dirname(configPath), { recursive: true });
624
+ fs5.mkdirSync(path5.dirname(configPath), { recursive: true });
540
625
  let content = "";
541
- if (fs4.existsSync(configPath)) {
542
- content = fs4.readFileSync(configPath, "utf-8");
626
+ if (fs5.existsSync(configPath)) {
627
+ content = fs5.readFileSync(configPath, "utf-8");
543
628
  }
544
629
  const trustTargets = getCodexWritableRoots(repoRoot, commsDir);
545
630
  if (mode === "full") {
@@ -601,8 +686,8 @@ function applyCodexPermissions(repoRoot, commsDir, mode) {
601
686
  );
602
687
  }
603
688
  const tmp = `${configPath}.tmp.${process.pid}`;
604
- fs4.writeFileSync(tmp, content, "utf-8");
605
- fs4.renameSync(tmp, configPath);
689
+ fs5.writeFileSync(tmp, content, "utf-8");
690
+ fs5.renameSync(tmp, configPath);
606
691
  const modeLabel = mode === "full" ? "danger-full-access" : "workspace-write, network=full";
607
692
  logSuccess(
608
693
  `Codex: sandbox=${modeLabel}, ${trustTargets.length} path(s) trusted`
@@ -611,12 +696,12 @@ function applyCodexPermissions(repoRoot, commsDir, mode) {
611
696
  }
612
697
  function getCodexWritableRoots(repoRoot, commsDir) {
613
698
  const roots = [repoRoot, commsDir];
614
- const parent = path4.dirname(repoRoot);
699
+ const parent = path5.dirname(repoRoot);
615
700
  for (let i = 1; i <= 4; i++) {
616
- const wtPath = path4.join(parent, `hua-wt-${i}`);
617
- if (fs4.existsSync(wtPath)) roots.push(wtPath);
701
+ const wtPath = path5.join(parent, `hua-wt-${i}`);
702
+ if (fs5.existsSync(wtPath)) roots.push(wtPath);
618
703
  }
619
- return [...new Set(roots.map((r) => path4.resolve(r)))];
704
+ return [...new Set(roots.map((r) => path5.resolve(r)))];
620
705
  }
621
706
  function buildPermissionSummary(mode, repoRoot, commsDir) {
622
707
  const trustedPaths = getCodexWritableRoots(repoRoot, commsDir);
@@ -654,7 +739,7 @@ function parsePermissionMode(args) {
654
739
  return "safe";
655
740
  }
656
741
  async function initCommand(args) {
657
- const repoRoot = findRepoRoot2();
742
+ const repoRoot = findRepoRoot();
658
743
  const commsDir = resolveCommsDir(args, repoRoot);
659
744
  const permMode = parsePermissionMode(args);
660
745
  if (stateExists(repoRoot) && !args.includes("--force")) {
@@ -670,13 +755,13 @@ async function initCommand(args) {
670
755
  logHeader("@hua-labs/tap init");
671
756
  log(`Comms directory: ${commsDir}`);
672
757
  for (const dir of COMMS_DIRS) {
673
- const dirPath = path5.join(commsDir, dir);
674
- fs5.mkdirSync(dirPath, { recursive: true });
758
+ const dirPath = path6.join(commsDir, dir);
759
+ fs6.mkdirSync(dirPath, { recursive: true });
675
760
  logSuccess(`Created ${dir}/`);
676
761
  }
677
- const gitignorePath = path5.join(commsDir, ".gitignore");
678
- if (!fs5.existsSync(gitignorePath)) {
679
- fs5.writeFileSync(
762
+ const gitignorePath = path6.join(commsDir, ".gitignore");
763
+ if (!fs6.existsSync(gitignorePath)) {
764
+ fs6.writeFileSync(
680
765
  gitignorePath,
681
766
  ["tap.db", ".lock", "*.tmp.*", ".DS_Store"].join("\n") + "\n",
682
767
  "utf-8"
@@ -685,12 +770,12 @@ async function initCommand(args) {
685
770
  }
686
771
  const { config } = resolveConfig({}, repoRoot);
687
772
  const stateDir = config.stateDir;
688
- fs5.mkdirSync(path5.join(stateDir, "pids"), { recursive: true });
689
- fs5.mkdirSync(path5.join(stateDir, "logs"), { recursive: true });
690
- fs5.mkdirSync(path5.join(stateDir, "backups"), { recursive: true });
691
- const stateDirRel = path5.relative(repoRoot, stateDir);
773
+ fs6.mkdirSync(path6.join(stateDir, "pids"), { recursive: true });
774
+ fs6.mkdirSync(path6.join(stateDir, "logs"), { recursive: true });
775
+ fs6.mkdirSync(path6.join(stateDir, "backups"), { recursive: true });
776
+ const stateDirRel = path6.relative(repoRoot, stateDir);
692
777
  logSuccess(`Created ${stateDirRel}/ state directory`);
693
- const repoGitignore = path5.join(repoRoot, ".gitignore");
778
+ const repoGitignore = path6.join(repoRoot, ".gitignore");
694
779
  const gitignoreEntries = [
695
780
  { entry: stateDirRel.replace(/\\/g, "/") + "/", label: "tap-comms state" },
696
781
  {
@@ -698,11 +783,11 @@ async function initCommand(args) {
698
783
  label: "tap-comms local config (machine-specific)"
699
784
  }
700
785
  ];
701
- if (fs5.existsSync(repoGitignore)) {
702
- const content = fs5.readFileSync(repoGitignore, "utf-8");
786
+ if (fs6.existsSync(repoGitignore)) {
787
+ const content = fs6.readFileSync(repoGitignore, "utf-8");
703
788
  for (const { entry, label } of gitignoreEntries) {
704
789
  if (!content.includes(entry)) {
705
- fs5.appendFileSync(repoGitignore, `
790
+ fs6.appendFileSync(repoGitignore, `
706
791
  # ${label}
707
792
  ${entry}
708
793
  `);
@@ -742,64 +827,178 @@ ${entry}
742
827
  }
743
828
 
744
829
  // src/adapters/claude.ts
745
- import * as fs6 from "fs";
746
- import * as path6 from "path";
830
+ import * as fs8 from "fs";
831
+ import * as path8 from "path";
747
832
  import { execSync } from "child_process";
748
- var MCP_SERVER_KEY = "tap-comms";
749
- function findMcpJsonPath(ctx) {
750
- return path6.join(ctx.repoRoot, ".mcp.json");
833
+
834
+ // src/adapters/common.ts
835
+ import * as fs7 from "fs";
836
+ import * as os2 from "os";
837
+ import * as path7 from "path";
838
+ import { spawnSync } from "child_process";
839
+ import { fileURLToPath as fileURLToPath2 } from "url";
840
+ function probeCommand(candidates) {
841
+ for (const candidate of candidates) {
842
+ const result = spawnSync(candidate, ["--version"], {
843
+ encoding: "utf-8",
844
+ shell: process.platform === "win32"
845
+ });
846
+ if (result.status === 0) {
847
+ const version2 = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() || null;
848
+ return { command: candidate, version: version2 };
849
+ }
850
+ }
851
+ return { command: null, version: null };
751
852
  }
752
- function findClaudeCommand() {
853
+ function getHomeDir() {
854
+ return os2.homedir();
855
+ }
856
+ function toForwardSlashPath(filePath) {
857
+ return path7.resolve(filePath).replace(/\\/g, "/");
858
+ }
859
+ function canWriteOrCreate(filePath) {
753
860
  try {
754
- execSync("claude --version", { stdio: "pipe" });
755
- return "claude";
861
+ if (fs7.existsSync(filePath)) {
862
+ fs7.accessSync(filePath, fs7.constants.W_OK);
863
+ return true;
864
+ }
865
+ const parent = path7.dirname(filePath);
866
+ fs7.mkdirSync(parent, { recursive: true });
867
+ fs7.accessSync(parent, fs7.constants.W_OK);
868
+ return true;
756
869
  } catch {
757
- return null;
870
+ return false;
758
871
  }
759
872
  }
760
- function buildMcpServerEntry(ctx) {
761
- const localChannels = findLocalChannels(ctx);
762
- if (!localChannels) return null;
763
- return {
764
- type: "stdio",
765
- command: "npx",
766
- args: ["bun", localChannels],
767
- env: { TAP_COMMS_DIR: ctx.commsDir }
768
- };
769
- }
770
- function findLocalChannels(ctx) {
873
+ function findLocalTapCommsSource(ctx) {
771
874
  const candidates = [
772
- path6.join(
875
+ path7.join(
773
876
  ctx.repoRoot,
774
877
  "packages",
775
878
  "tap-plugin",
776
879
  "channels",
777
880
  "tap-comms.ts"
778
881
  ),
779
- path6.join(
882
+ path7.join(
780
883
  ctx.repoRoot,
781
884
  "node_modules",
782
885
  "@hua-labs",
886
+ "tap-plugin",
783
887
  "channels",
784
888
  "tap-comms.ts"
785
889
  )
786
890
  ];
787
- for (const p of candidates) {
788
- if (fs6.existsSync(p)) return p;
891
+ for (const candidate of candidates) {
892
+ if (fs7.existsSync(candidate)) return candidate;
893
+ }
894
+ return null;
895
+ }
896
+ function findBundledTapCommsSource(metaUrl = import.meta.url) {
897
+ const moduleDir = path7.dirname(fileURLToPath2(metaUrl));
898
+ const candidates = [
899
+ path7.join(moduleDir, "mcp-server.mjs"),
900
+ path7.join(moduleDir, "..", "mcp-server.mjs"),
901
+ path7.join(moduleDir, "..", "mcp-server.ts")
902
+ ];
903
+ for (const candidate of candidates) {
904
+ if (fs7.existsSync(candidate)) return candidate;
905
+ }
906
+ return null;
907
+ }
908
+ function findTapCommsServerEntry(ctx, metaUrl = import.meta.url) {
909
+ return findBundledTapCommsSource(metaUrl) ?? findLocalTapCommsSource(ctx);
910
+ }
911
+ function findPreferredBunCommand() {
912
+ const home = getHomeDir();
913
+ const candidates = process.platform === "win32" ? [path7.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path7.join(home, ".bun", "bin", "bun"), "bun"];
914
+ for (const candidate of candidates) {
915
+ if (path7.isAbsolute(candidate) && !fs7.existsSync(candidate)) continue;
916
+ const result = spawnSync(candidate, ["--version"], {
917
+ encoding: "utf-8",
918
+ shell: process.platform === "win32"
919
+ });
920
+ if (result.status === 0) {
921
+ return path7.isAbsolute(candidate) ? toForwardSlashPath(candidate) : candidate;
922
+ }
789
923
  }
790
924
  return null;
791
925
  }
926
+ function buildManagedMcpServerSpec(ctx, instanceId) {
927
+ const sourcePath = findTapCommsServerEntry(ctx);
928
+ const bunCommand = findPreferredBunCommand();
929
+ const warnings = [];
930
+ const issues = [];
931
+ const env = {
932
+ TAP_AGENT_NAME: "<set-per-session>",
933
+ TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
934
+ };
935
+ if (instanceId) {
936
+ env.TAP_AGENT_ID = instanceId;
937
+ }
938
+ if (!sourcePath) {
939
+ issues.push(
940
+ "tap-comms MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
941
+ );
942
+ return { command: null, args: [], env, sourcePath, warnings, issues };
943
+ }
944
+ const isBundled = sourcePath.endsWith(".mjs");
945
+ let command = bunCommand;
946
+ if (!command && isBundled) {
947
+ command = process.execPath;
948
+ warnings.push(
949
+ "bun not found; using node to run the compiled MCP server. Install bun for better performance."
950
+ );
951
+ }
952
+ if (!command) {
953
+ issues.push(
954
+ "bun is required to run the repo-local tap-comms MCP server (.ts source). Install bun: https://bun.sh"
955
+ );
956
+ return { command: null, args: [], env, sourcePath, warnings, issues };
957
+ }
958
+ return {
959
+ command: isBundled && command === process.execPath ? toForwardSlashPath(command) : command,
960
+ args: [toForwardSlashPath(sourcePath)],
961
+ env,
962
+ sourcePath,
963
+ warnings,
964
+ issues
965
+ };
966
+ }
967
+
968
+ // src/adapters/claude.ts
969
+ var MCP_SERVER_KEY = "tap-comms";
970
+ function findMcpJsonPath(ctx) {
971
+ return path8.join(ctx.repoRoot, ".mcp.json");
972
+ }
973
+ function findClaudeCommand() {
974
+ try {
975
+ execSync("claude --version", { stdio: "pipe" });
976
+ return "claude";
977
+ } catch {
978
+ return null;
979
+ }
980
+ }
981
+ function buildMcpServerEntry(ctx) {
982
+ const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
983
+ if (!managed.command) return null;
984
+ return {
985
+ type: "stdio",
986
+ command: managed.command,
987
+ args: managed.args,
988
+ env: managed.env
989
+ };
990
+ }
792
991
  var claudeAdapter = {
793
992
  runtime: "claude",
794
993
  async probe(ctx) {
795
994
  const warnings = [];
796
995
  const issues = [];
797
996
  const configPath = findMcpJsonPath(ctx);
798
- const configExists = fs6.existsSync(configPath);
997
+ const configExists = fs8.existsSync(configPath);
799
998
  const runtimeCommand = findClaudeCommand();
800
999
  const canWrite = configExists ? (() => {
801
1000
  try {
802
- fs6.accessSync(configPath, fs6.constants.W_OK);
1001
+ fs8.accessSync(configPath, fs8.constants.W_OK);
803
1002
  return true;
804
1003
  } catch {
805
1004
  return false;
@@ -810,13 +1009,10 @@ var claudeAdapter = {
810
1009
  "Claude CLI not found in PATH. Config will be created but may need manual setup."
811
1010
  );
812
1011
  }
813
- const localChannels = findLocalChannels(ctx);
814
- if (!localChannels) {
815
- issues.push(
816
- "tap-comms MCP server not found locally. Ensure packages/tap-plugin/channels/tap-comms.ts exists. Run from the monorepo root."
817
- );
818
- }
819
- if (!fs6.existsSync(ctx.commsDir)) {
1012
+ const managed = buildManagedMcpServerSpec(ctx);
1013
+ warnings.push(...managed.warnings);
1014
+ issues.push(...managed.issues);
1015
+ if (!fs8.existsSync(ctx.commsDir)) {
820
1016
  issues.push(
821
1017
  `Comms directory not found: ${ctx.commsDir}. Run "init" first.`
822
1018
  );
@@ -840,7 +1036,7 @@ var claudeAdapter = {
840
1036
  const operations = [];
841
1037
  const ownedArtifacts = [];
842
1038
  if (probe.configExists) {
843
- const raw = fs6.readFileSync(configPath, "utf-8");
1039
+ const raw = fs8.readFileSync(configPath, "utf-8");
844
1040
  try {
845
1041
  const config = JSON.parse(raw);
846
1042
  if (config.mcpServers?.[MCP_SERVER_KEY]) {
@@ -857,7 +1053,7 @@ var claudeAdapter = {
857
1053
  const serverEntry = buildMcpServerEntry(ctx);
858
1054
  if (!serverEntry) {
859
1055
  warnings.push(
860
- "tap-comms MCP server not found locally. Skipping .mcp.json patch. Run from monorepo root with packages/tap-plugin/channels/ available."
1056
+ "tap-comms MCP server entry not found. Skipping .mcp.json patch. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
861
1057
  );
862
1058
  return {
863
1059
  runtime: "claude",
@@ -899,9 +1095,9 @@ var claudeAdapter = {
899
1095
  try {
900
1096
  if (op.type === "set" || op.type === "merge") {
901
1097
  let config = {};
902
- if (fs6.existsSync(op.path)) {
1098
+ if (fs8.existsSync(op.path)) {
903
1099
  backupFile(op.path, plan.backupDir);
904
- const raw = fs6.readFileSync(op.path, "utf-8");
1100
+ const raw = fs8.readFileSync(op.path, "utf-8");
905
1101
  try {
906
1102
  config = JSON.parse(raw);
907
1103
  } catch {
@@ -914,12 +1110,12 @@ var claudeAdapter = {
914
1110
  setNestedKey(config, op.key, op.value);
915
1111
  }
916
1112
  const tmp = `${op.path}.tmp.${process.pid}`;
917
- fs6.writeFileSync(
1113
+ fs8.writeFileSync(
918
1114
  tmp,
919
1115
  JSON.stringify(config, null, 2) + "\n",
920
1116
  "utf-8"
921
1117
  );
922
- fs6.renameSync(tmp, op.path);
1118
+ fs8.renameSync(tmp, op.path);
923
1119
  changedFiles.push(op.path);
924
1120
  appliedOps++;
925
1121
  }
@@ -948,12 +1144,12 @@ var claudeAdapter = {
948
1144
  if (configPath) {
949
1145
  checks.push({
950
1146
  name: "Config file exists",
951
- passed: fs6.existsSync(configPath),
952
- message: fs6.existsSync(configPath) ? void 0 : `${configPath} not found`
1147
+ passed: fs8.existsSync(configPath),
1148
+ message: fs8.existsSync(configPath) ? void 0 : `${configPath} not found`
953
1149
  });
954
- if (fs6.existsSync(configPath)) {
1150
+ if (fs8.existsSync(configPath)) {
955
1151
  try {
956
- const raw = fs6.readFileSync(configPath, "utf-8");
1152
+ const raw = fs8.readFileSync(configPath, "utf-8");
957
1153
  const config = JSON.parse(raw);
958
1154
  checks.push({ name: "Config is valid JSON", passed: true });
959
1155
  const entry = config.mcpServers?.[MCP_SERVER_KEY];
@@ -963,7 +1159,7 @@ var claudeAdapter = {
963
1159
  message: entry ? void 0 : `mcpServers.${MCP_SERVER_KEY} not found`
964
1160
  });
965
1161
  if (entry) {
966
- const hasCommsDir = entry.env?.TAP_COMMS_DIR === ctx.commsDir;
1162
+ const hasCommsDir = normalizeTapCommsDir(entry.env?.TAP_COMMS_DIR) === normalizeTapCommsDir(ctx.commsDir);
967
1163
  checks.push({
968
1164
  name: "TAP_COMMS_DIR configured",
969
1165
  passed: hasCommsDir,
@@ -981,8 +1177,8 @@ var claudeAdapter = {
981
1177
  }
982
1178
  checks.push({
983
1179
  name: "Comms directory exists",
984
- passed: fs6.existsSync(ctx.commsDir),
985
- message: fs6.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
1180
+ passed: fs8.existsSync(ctx.commsDir),
1181
+ message: fs8.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
986
1182
  });
987
1183
  const cmd = findClaudeCommand();
988
1184
  checks.push({
@@ -1014,174 +1210,62 @@ function setNestedKey(obj, keyPath, value) {
1014
1210
  }
1015
1211
  current[keys[keys.length - 1]] = value;
1016
1212
  }
1213
+ function normalizeTapCommsDir(value) {
1214
+ return typeof value === "string" ? path8.resolve(value).replace(/\\/g, "/") : "";
1215
+ }
1017
1216
 
1018
1217
  // src/adapters/codex.ts
1019
- import * as fs9 from "fs";
1020
- import * as path9 from "path";
1021
- import { fileURLToPath } from "url";
1218
+ import * as fs10 from "fs";
1219
+ import * as path10 from "path";
1220
+ import { fileURLToPath as fileURLToPath3 } from "url";
1022
1221
 
1023
1222
  // src/artifact-backups.ts
1024
1223
  import * as crypto2 from "crypto";
1025
- import * as fs7 from "fs";
1026
- import * as path7 from "path";
1224
+ import * as fs9 from "fs";
1225
+ import * as path9 from "path";
1027
1226
  function selectorHash(selector) {
1028
1227
  return crypto2.createHash("sha256").update(selector).digest("hex").slice(0, 12);
1029
1228
  }
1030
1229
  function artifactBackupPath(backupDir, kind, selector) {
1031
1230
  const safeKind = kind.replace(/[^a-z-]/gi, "-");
1032
- return path7.join(backupDir, `${safeKind}-${selectorHash(selector)}.json`);
1231
+ return path9.join(backupDir, `${safeKind}-${selectorHash(selector)}.json`);
1033
1232
  }
1034
1233
  function writeArtifactBackup(backupPath, payload) {
1035
- fs7.mkdirSync(path7.dirname(backupPath), { recursive: true });
1234
+ fs9.mkdirSync(path9.dirname(backupPath), { recursive: true });
1036
1235
  const tmp = `${backupPath}.tmp.${process.pid}`;
1037
- fs7.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
1038
- fs7.renameSync(tmp, backupPath);
1236
+ fs9.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
1237
+ fs9.renameSync(tmp, backupPath);
1039
1238
  }
1040
1239
  function readArtifactBackup(backupPath) {
1041
- if (!fs7.existsSync(backupPath)) return null;
1240
+ if (!fs9.existsSync(backupPath)) return null;
1042
1241
  try {
1043
- const raw = fs7.readFileSync(backupPath, "utf-8");
1242
+ const raw = fs9.readFileSync(backupPath, "utf-8");
1044
1243
  return JSON.parse(raw);
1045
1244
  } catch {
1046
1245
  return null;
1047
1246
  }
1048
1247
  }
1049
1248
 
1050
- // src/adapters/common.ts
1051
- import * as fs8 from "fs";
1052
- import * as os2 from "os";
1053
- import * as path8 from "path";
1054
- import { spawnSync } from "child_process";
1055
- function probeCommand(candidates) {
1056
- for (const candidate of candidates) {
1057
- const result = spawnSync(candidate, ["--version"], {
1058
- encoding: "utf-8",
1059
- shell: process.platform === "win32"
1060
- });
1061
- if (result.status === 0) {
1062
- const version2 = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() || null;
1063
- return { command: candidate, version: version2 };
1064
- }
1065
- }
1066
- return { command: null, version: null };
1249
+ // src/adapters/codex.ts
1250
+ var MCP_SELECTOR = "mcp_servers.tap-comms";
1251
+ var ENV_SELECTOR = "mcp_servers.tap-comms.env";
1252
+ function findCodexConfigPath2() {
1253
+ return path10.join(getHomeDir(), ".codex", "config.toml");
1067
1254
  }
1068
- function getHomeDir() {
1069
- return os2.homedir();
1255
+ function canonicalizeTrustPath2(targetPath) {
1256
+ let resolved = path10.resolve(targetPath).replace(/\//g, "\\");
1257
+ const driveRoot = /^[A-Za-z]:\\$/;
1258
+ if (!driveRoot.test(resolved)) {
1259
+ resolved = resolved.replace(/\\+$/g, "");
1260
+ }
1261
+ return resolved.startsWith("\\\\?\\") ? resolved : `\\\\?\\${resolved}`;
1070
1262
  }
1071
- function toForwardSlashPath(filePath) {
1072
- return path8.resolve(filePath).replace(/\\/g, "/");
1263
+ function trustSelector(targetPath) {
1264
+ return `projects.'${canonicalizeTrustPath2(targetPath)}'`;
1073
1265
  }
1074
- function canWriteOrCreate(filePath) {
1075
- try {
1076
- if (fs8.existsSync(filePath)) {
1077
- fs8.accessSync(filePath, fs8.constants.W_OK);
1078
- return true;
1079
- }
1080
- const parent = path8.dirname(filePath);
1081
- fs8.mkdirSync(parent, { recursive: true });
1082
- fs8.accessSync(parent, fs8.constants.W_OK);
1083
- return true;
1084
- } catch {
1085
- return false;
1086
- }
1087
- }
1088
- function findLocalTapCommsSource(ctx) {
1089
- const candidates = [
1090
- path8.join(
1091
- ctx.repoRoot,
1092
- "packages",
1093
- "tap-plugin",
1094
- "channels",
1095
- "tap-comms.ts"
1096
- ),
1097
- path8.join(
1098
- ctx.repoRoot,
1099
- "node_modules",
1100
- "@hua-labs",
1101
- "tap-plugin",
1102
- "channels",
1103
- "tap-comms.ts"
1104
- )
1105
- ];
1106
- for (const candidate of candidates) {
1107
- if (fs8.existsSync(candidate)) return candidate;
1108
- }
1109
- return null;
1110
- }
1111
- function findPreferredBunCommand() {
1112
- const home = getHomeDir();
1113
- const candidates = process.platform === "win32" ? [path8.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path8.join(home, ".bun", "bin", "bun"), "bun"];
1114
- for (const candidate of candidates) {
1115
- if (path8.isAbsolute(candidate) && !fs8.existsSync(candidate)) continue;
1116
- const result = spawnSync(candidate, ["--version"], {
1117
- encoding: "utf-8",
1118
- shell: process.platform === "win32"
1119
- });
1120
- if (result.status === 0) {
1121
- return path8.isAbsolute(candidate) ? toForwardSlashPath(candidate) : candidate;
1122
- }
1123
- }
1124
- return null;
1125
- }
1126
- function buildManagedMcpServerSpec(ctx) {
1127
- const sourcePath = findLocalTapCommsSource(ctx);
1128
- const bunCommand = findPreferredBunCommand();
1129
- const warnings = [];
1130
- const issues = [];
1131
- if (sourcePath && bunCommand) {
1132
- return {
1133
- command: bunCommand,
1134
- args: [toForwardSlashPath(sourcePath)],
1135
- env: {
1136
- TAP_AGENT_NAME: "<set-per-session>",
1137
- TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
1138
- },
1139
- sourcePath,
1140
- warnings,
1141
- issues
1142
- };
1143
- }
1144
- if (!sourcePath) {
1145
- issues.push(
1146
- "tap-comms MCP server source not found. v1 requires a repo-local tap-plugin/channels installation."
1147
- );
1148
- }
1149
- if (!bunCommand) {
1150
- issues.push("bun is required to run the repo-local tap-comms MCP server.");
1151
- }
1152
- return {
1153
- command: null,
1154
- args: [],
1155
- env: {
1156
- TAP_AGENT_NAME: "<set-per-session>",
1157
- TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
1158
- },
1159
- sourcePath,
1160
- warnings,
1161
- issues
1162
- };
1163
- }
1164
-
1165
- // src/adapters/codex.ts
1166
- var MCP_SELECTOR = "mcp_servers.tap-comms";
1167
- var ENV_SELECTOR = "mcp_servers.tap-comms.env";
1168
- function findCodexConfigPath2() {
1169
- return path9.join(getHomeDir(), ".codex", "config.toml");
1170
- }
1171
- function canonicalizeTrustPath2(targetPath) {
1172
- let resolved = path9.resolve(targetPath).replace(/\//g, "\\");
1173
- const driveRoot = /^[A-Za-z]:\\$/;
1174
- if (!driveRoot.test(resolved)) {
1175
- resolved = resolved.replace(/\\+$/g, "");
1176
- }
1177
- return resolved.startsWith("\\\\?\\") ? resolved : `\\\\?\\${resolved}`;
1178
- }
1179
- function trustSelector(targetPath) {
1180
- return `projects.'${canonicalizeTrustPath2(targetPath)}'`;
1181
- }
1182
- function getTrustTargets(ctx) {
1183
- const targets = [ctx.repoRoot, process.cwd()];
1184
- return [...new Set(targets.map((value) => path9.resolve(value)))];
1266
+ function getTrustTargets(ctx) {
1267
+ const targets = [ctx.repoRoot, process.cwd()];
1268
+ return [...new Set(targets.map((value) => path10.resolve(value)))];
1185
1269
  }
1186
1270
  function buildManagedArtifacts(configPath, ctx) {
1187
1271
  const artifacts = [
@@ -1198,14 +1282,14 @@ function buildManagedArtifacts(configPath, ctx) {
1198
1282
  return artifacts;
1199
1283
  }
1200
1284
  function readConfigOrEmpty(configPath) {
1201
- if (!fs9.existsSync(configPath)) return "";
1202
- return fs9.readFileSync(configPath, "utf-8");
1285
+ if (!fs10.existsSync(configPath)) return "";
1286
+ return fs10.readFileSync(configPath, "utf-8");
1203
1287
  }
1204
1288
  function writeTomlFile(filePath, content) {
1205
- fs9.mkdirSync(path9.dirname(filePath), { recursive: true });
1289
+ fs10.mkdirSync(path10.dirname(filePath), { recursive: true });
1206
1290
  const tmp = `${filePath}.tmp.${process.pid}`;
1207
- fs9.writeFileSync(tmp, content, "utf-8");
1208
- fs9.renameSync(tmp, filePath);
1291
+ fs10.writeFileSync(tmp, content, "utf-8");
1292
+ fs10.renameSync(tmp, filePath);
1209
1293
  }
1210
1294
  function verifyManagedToml(content, ctx, configPath) {
1211
1295
  const checks = [];
@@ -1214,8 +1298,8 @@ function verifyManagedToml(content, ctx, configPath) {
1214
1298
  const envTable = extractTomlTable(content, ENV_SELECTOR);
1215
1299
  checks.push({
1216
1300
  name: "Codex config exists",
1217
- passed: fs9.existsSync(configPath),
1218
- message: fs9.existsSync(configPath) ? void 0 : `${configPath} not found`
1301
+ passed: fs10.existsSync(configPath),
1302
+ message: fs10.existsSync(configPath) ? void 0 : `${configPath} not found`
1219
1303
  });
1220
1304
  checks.push({
1221
1305
  name: "tap-comms MCP table present",
@@ -1255,7 +1339,7 @@ var codexAdapter = {
1255
1339
  const warnings = [];
1256
1340
  const issues = [];
1257
1341
  const configPath = findCodexConfigPath2();
1258
- const configExists = fs9.existsSync(configPath);
1342
+ const configExists = fs10.existsSync(configPath);
1259
1343
  const runtimeProbe = probeCommand(
1260
1344
  ctx.platform === "win32" ? ["codex", "codex.cmd"] : ["codex"]
1261
1345
  );
@@ -1264,7 +1348,7 @@ var codexAdapter = {
1264
1348
  "Codex CLI not found in PATH. Config can still be written, but runtime verification will be limited."
1265
1349
  );
1266
1350
  }
1267
- if (!fs9.existsSync(ctx.commsDir)) {
1351
+ if (!fs10.existsSync(ctx.commsDir)) {
1268
1352
  issues.push(
1269
1353
  `Comms directory not found: ${ctx.commsDir}. Run "init" first.`
1270
1354
  );
@@ -1325,7 +1409,7 @@ var codexAdapter = {
1325
1409
  const configPath = plan.operations[0]?.path ?? findCodexConfigPath2();
1326
1410
  const warnings = [];
1327
1411
  const changedFiles = [];
1328
- const managed = buildManagedMcpServerSpec(ctx);
1412
+ const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
1329
1413
  warnings.push(...managed.warnings);
1330
1414
  if (managed.issues.length > 0 || !managed.command) {
1331
1415
  return {
@@ -1340,7 +1424,7 @@ var codexAdapter = {
1340
1424
  };
1341
1425
  }
1342
1426
  const existingContent = readConfigOrEmpty(configPath);
1343
- if (fs9.existsSync(configPath) && existingContent) {
1427
+ if (fs10.existsSync(configPath) && existingContent) {
1344
1428
  backupFile(configPath, plan.backupDir);
1345
1429
  }
1346
1430
  const artifactsWithBackups = plan.ownedArtifacts.map((artifact) => {
@@ -1415,8 +1499,8 @@ var codexAdapter = {
1415
1499
  const checks = verifyManagedToml(content, ctx, configPath);
1416
1500
  checks.push({
1417
1501
  name: "Comms directory exists",
1418
- passed: fs9.existsSync(ctx.commsDir),
1419
- message: fs9.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
1502
+ passed: fs10.existsSync(ctx.commsDir),
1503
+ message: fs10.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
1420
1504
  });
1421
1505
  checks.push({
1422
1506
  name: "Codex CLI found",
@@ -1439,12 +1523,12 @@ var codexAdapter = {
1439
1523
  return "app-server";
1440
1524
  },
1441
1525
  resolveBridgeScript(ctx) {
1442
- const distDir = path9.dirname(fileURLToPath(import.meta.url));
1526
+ const distDir = path10.dirname(fileURLToPath3(import.meta.url));
1443
1527
  const candidates = [
1444
1528
  // 1. Relative to bundled CLI (npm install / npx)
1445
- path9.join(distDir, "bridges", "codex-bridge-runner.mjs"),
1529
+ path10.join(distDir, "bridges", "codex-bridge-runner.mjs"),
1446
1530
  // 2. Monorepo development — dist inside repo
1447
- path9.join(
1531
+ path10.join(
1448
1532
  ctx.repoRoot,
1449
1533
  "packages",
1450
1534
  "tap-comms",
@@ -1453,7 +1537,7 @@ var codexAdapter = {
1453
1537
  "codex-bridge-runner.mjs"
1454
1538
  ),
1455
1539
  // 3. Source file — dev mode with strip-types
1456
- path9.join(
1540
+ path10.join(
1457
1541
  ctx.repoRoot,
1458
1542
  "packages",
1459
1543
  "tap-comms",
@@ -1463,30 +1547,30 @@ var codexAdapter = {
1463
1547
  )
1464
1548
  ];
1465
1549
  for (const candidate of candidates) {
1466
- if (fs9.existsSync(candidate)) return candidate;
1550
+ if (fs10.existsSync(candidate)) return candidate;
1467
1551
  }
1468
1552
  return null;
1469
1553
  }
1470
1554
  };
1471
1555
 
1472
1556
  // src/adapters/gemini.ts
1473
- import * as fs10 from "fs";
1474
- import * as path10 from "path";
1557
+ import * as fs11 from "fs";
1558
+ import * as path11 from "path";
1475
1559
  var GEMINI_SELECTOR = "mcpServers.tap-comms";
1476
1560
  function candidateConfigPaths(ctx) {
1477
1561
  const home = getHomeDir();
1478
1562
  return [
1479
- path10.join(ctx.repoRoot, ".gemini", "settings.json"),
1480
- path10.join(home, ".gemini", "settings.json"),
1481
- path10.join(home, ".gemini", "antigravity", "mcp_config.json")
1563
+ path11.join(ctx.repoRoot, ".gemini", "settings.json"),
1564
+ path11.join(home, ".gemini", "settings.json"),
1565
+ path11.join(home, ".gemini", "antigravity", "mcp_config.json")
1482
1566
  ];
1483
1567
  }
1484
1568
  function chooseGeminiConfigPath(ctx) {
1485
1569
  const [workspaceConfig, homeConfig, antigravityConfig] = candidateConfigPaths(ctx);
1486
- if (fs10.existsSync(workspaceConfig)) return workspaceConfig;
1487
- if (fs10.existsSync(homeConfig)) return homeConfig;
1488
- if (fs10.existsSync(antigravityConfig)) {
1489
- const raw = fs10.readFileSync(antigravityConfig, "utf-8").trim();
1570
+ if (fs11.existsSync(workspaceConfig)) return workspaceConfig;
1571
+ if (fs11.existsSync(homeConfig)) return homeConfig;
1572
+ if (fs11.existsSync(antigravityConfig)) {
1573
+ const raw = fs11.readFileSync(antigravityConfig, "utf-8").trim();
1490
1574
  if (raw) {
1491
1575
  try {
1492
1576
  JSON.parse(raw);
@@ -1498,8 +1582,8 @@ function chooseGeminiConfigPath(ctx) {
1498
1582
  return workspaceConfig;
1499
1583
  }
1500
1584
  function readJsonFile(filePath) {
1501
- if (!fs10.existsSync(filePath)) return {};
1502
- const raw = fs10.readFileSync(filePath, "utf-8").trim();
1585
+ if (!fs11.existsSync(filePath)) return {};
1586
+ const raw = fs11.readFileSync(filePath, "utf-8").trim();
1503
1587
  if (!raw) return {};
1504
1588
  return JSON.parse(raw);
1505
1589
  }
@@ -1530,8 +1614,8 @@ function verifyGeminiConfig(config, configPath, ctx) {
1530
1614
  const entry = readNestedKey(config, GEMINI_SELECTOR);
1531
1615
  checks.push({
1532
1616
  name: "Gemini config exists",
1533
- passed: fs10.existsSync(configPath),
1534
- message: fs10.existsSync(configPath) ? void 0 : `${configPath} not found`
1617
+ passed: fs11.existsSync(configPath),
1618
+ message: fs11.existsSync(configPath) ? void 0 : `${configPath} not found`
1535
1619
  });
1536
1620
  checks.push({
1537
1621
  name: "tap-comms entry present",
@@ -1540,8 +1624,8 @@ function verifyGeminiConfig(config, configPath, ctx) {
1540
1624
  });
1541
1625
  checks.push({
1542
1626
  name: "Comms directory exists",
1543
- passed: fs10.existsSync(ctx.commsDir),
1544
- message: fs10.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
1627
+ passed: fs11.existsSync(ctx.commsDir),
1628
+ message: fs11.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
1545
1629
  });
1546
1630
  if (entry?.env && typeof entry.env === "object") {
1547
1631
  checks.push({
@@ -1558,7 +1642,7 @@ var geminiAdapter = {
1558
1642
  const warnings = [];
1559
1643
  const issues = [];
1560
1644
  const configPath = chooseGeminiConfigPath(ctx);
1561
- const configExists = fs10.existsSync(configPath);
1645
+ const configExists = fs11.existsSync(configPath);
1562
1646
  const runtimeProbe = probeCommand(
1563
1647
  ctx.platform === "win32" ? ["gemini", "gemini.cmd"] : ["gemini"]
1564
1648
  );
@@ -1567,10 +1651,12 @@ var geminiAdapter = {
1567
1651
  "Gemini CLI not found in PATH. Config can still be written, but runtime verification will be limited."
1568
1652
  );
1569
1653
  }
1570
- if (!fs10.existsSync(ctx.commsDir)) {
1571
- issues.push(`Comms directory not found: ${ctx.commsDir}. Run "init" first.`);
1654
+ if (!fs11.existsSync(ctx.commsDir)) {
1655
+ issues.push(
1656
+ `Comms directory not found: ${ctx.commsDir}. Run "init" first.`
1657
+ );
1572
1658
  }
1573
- const managed = buildManagedMcpServerSpec(ctx);
1659
+ const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
1574
1660
  warnings.push(...managed.warnings);
1575
1661
  issues.push(...managed.issues);
1576
1662
  return {
@@ -1599,7 +1685,9 @@ var geminiAdapter = {
1599
1685
  conflicts.push(`Existing ${GEMINI_SELECTOR} entry will be updated.`);
1600
1686
  }
1601
1687
  } catch {
1602
- warnings.push(`${configPath} exists but is not valid JSON. It will be replaced.`);
1688
+ warnings.push(
1689
+ `${configPath} exists but is not valid JSON. It will be replaced.`
1690
+ );
1603
1691
  }
1604
1692
  }
1605
1693
  operations.push({
@@ -1621,7 +1709,7 @@ var geminiAdapter = {
1621
1709
  const configPath = plan.operations[0]?.path ?? chooseGeminiConfigPath(ctx);
1622
1710
  const warnings = [];
1623
1711
  const changedFiles = [];
1624
- const managed = buildManagedMcpServerSpec(ctx);
1712
+ const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
1625
1713
  warnings.push(...managed.warnings);
1626
1714
  if (managed.issues.length > 0 || !managed.command) {
1627
1715
  return {
@@ -1637,20 +1725,26 @@ var geminiAdapter = {
1637
1725
  }
1638
1726
  let config = {};
1639
1727
  let previousValue = void 0;
1640
- if (fs10.existsSync(configPath)) {
1641
- if (fs10.readFileSync(configPath, "utf-8").trim()) {
1728
+ if (fs11.existsSync(configPath)) {
1729
+ if (fs11.readFileSync(configPath, "utf-8").trim()) {
1642
1730
  backupFile(configPath, plan.backupDir);
1643
1731
  }
1644
1732
  try {
1645
1733
  config = readJsonFile(configPath);
1646
1734
  } catch {
1647
- warnings.push(`${configPath} was invalid JSON. Created backup and starting fresh.`);
1735
+ warnings.push(
1736
+ `${configPath} was invalid JSON. Created backup and starting fresh.`
1737
+ );
1648
1738
  config = {};
1649
1739
  }
1650
1740
  previousValue = readNestedKey(config, GEMINI_SELECTOR);
1651
1741
  }
1652
1742
  const artifact = plan.ownedArtifacts[0];
1653
- const backupPath = artifactBackupPath(plan.backupDir, artifact.kind, artifact.selector);
1743
+ const backupPath = artifactBackupPath(
1744
+ plan.backupDir,
1745
+ artifact.kind,
1746
+ artifact.selector
1747
+ );
1654
1748
  writeArtifactBackup(backupPath, {
1655
1749
  kind: "json-path",
1656
1750
  selector: artifact.selector,
@@ -1662,10 +1756,10 @@ var geminiAdapter = {
1662
1756
  args: managed.args,
1663
1757
  env: managed.env
1664
1758
  });
1665
- fs10.mkdirSync(path10.dirname(configPath), { recursive: true });
1759
+ fs11.mkdirSync(path11.dirname(configPath), { recursive: true });
1666
1760
  const tmp = `${configPath}.tmp.${process.pid}`;
1667
- fs10.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
1668
- fs10.renameSync(tmp, configPath);
1761
+ fs11.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
1762
+ fs11.renameSync(tmp, configPath);
1669
1763
  changedFiles.push(configPath);
1670
1764
  return {
1671
1765
  success: true,
@@ -1736,19 +1830,22 @@ function getAdapter(runtime) {
1736
1830
  }
1737
1831
 
1738
1832
  // src/engine/bridge.ts
1739
- import * as fs12 from "fs";
1740
- import * as path12 from "path";
1741
- import { spawn, execSync as execSync3 } from "child_process";
1833
+ import * as fs13 from "fs";
1834
+ import * as net from "net";
1835
+ import * as path13 from "path";
1836
+ import { randomBytes } from "crypto";
1837
+ import { spawn, spawnSync as spawnSync2, execSync as execSync3 } from "child_process";
1838
+ import { fileURLToPath as fileURLToPath4 } from "url";
1742
1839
 
1743
1840
  // src/runtime/resolve-node.ts
1744
- import * as fs11 from "fs";
1745
- import * as path11 from "path";
1841
+ import * as fs12 from "fs";
1842
+ import * as path12 from "path";
1746
1843
  import { execSync as execSync2 } from "child_process";
1747
1844
  function readNodeVersion(repoRoot) {
1748
- const nvFile = path11.join(repoRoot, ".node-version");
1749
- if (!fs11.existsSync(nvFile)) return null;
1845
+ const nvFile = path12.join(repoRoot, ".node-version");
1846
+ if (!fs12.existsSync(nvFile)) return null;
1750
1847
  try {
1751
- const raw = fs11.readFileSync(nvFile, "utf-8").trim();
1848
+ const raw = fs12.readFileSync(nvFile, "utf-8").trim();
1752
1849
  return raw.length > 0 ? raw.replace(/^v/, "") : null;
1753
1850
  } catch {
1754
1851
  return null;
@@ -1758,16 +1855,16 @@ function fnmCandidateDirs() {
1758
1855
  if (process.platform === "win32") {
1759
1856
  return [
1760
1857
  process.env.FNM_DIR,
1761
- process.env.APPDATA ? path11.join(process.env.APPDATA, "fnm") : null,
1762
- process.env.LOCALAPPDATA ? path11.join(process.env.LOCALAPPDATA, "fnm") : null,
1763
- process.env.USERPROFILE ? path11.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
1858
+ process.env.APPDATA ? path12.join(process.env.APPDATA, "fnm") : null,
1859
+ process.env.LOCALAPPDATA ? path12.join(process.env.LOCALAPPDATA, "fnm") : null,
1860
+ process.env.USERPROFILE ? path12.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
1764
1861
  ].filter(Boolean);
1765
1862
  }
1766
1863
  return [
1767
1864
  process.env.FNM_DIR,
1768
- process.env.HOME ? path11.join(process.env.HOME, ".local", "share", "fnm") : null,
1769
- process.env.HOME ? path11.join(process.env.HOME, ".fnm") : null,
1770
- process.env.XDG_DATA_HOME ? path11.join(process.env.XDG_DATA_HOME, "fnm") : null
1865
+ process.env.HOME ? path12.join(process.env.HOME, ".local", "share", "fnm") : null,
1866
+ process.env.HOME ? path12.join(process.env.HOME, ".fnm") : null,
1867
+ process.env.XDG_DATA_HOME ? path12.join(process.env.XDG_DATA_HOME, "fnm") : null
1771
1868
  ].filter(Boolean);
1772
1869
  }
1773
1870
  function nodeExecutableName() {
@@ -1777,14 +1874,14 @@ function probeFnmNode(desiredVersion) {
1777
1874
  const dirs = fnmCandidateDirs();
1778
1875
  const exe = nodeExecutableName();
1779
1876
  for (const baseDir of dirs) {
1780
- const candidate = path11.join(
1877
+ const candidate = path12.join(
1781
1878
  baseDir,
1782
1879
  "node-versions",
1783
1880
  `v${desiredVersion}`,
1784
1881
  "installation",
1785
1882
  exe
1786
1883
  );
1787
- if (!fs11.existsSync(candidate)) continue;
1884
+ if (!fs12.existsSync(candidate)) continue;
1788
1885
  try {
1789
1886
  const v = execSync2(`"${candidate}" --version`, {
1790
1887
  encoding: "utf-8",
@@ -1810,107 +1907,881 @@ function detectNodeMajorVersion(command) {
1810
1907
  return null;
1811
1908
  }
1812
1909
  }
1813
- function checkStripTypesSupport(command) {
1814
- const major = detectNodeMajorVersion(command);
1815
- if (major !== null && major >= 22) return true;
1910
+ function checkStripTypesSupport(command) {
1911
+ const major = detectNodeMajorVersion(command);
1912
+ if (major !== null && major >= 22) return true;
1913
+ try {
1914
+ execSync2(`"${command}" --experimental-strip-types -e ""`, {
1915
+ timeout: 5e3,
1916
+ stdio: "pipe"
1917
+ });
1918
+ return true;
1919
+ } catch {
1920
+ return false;
1921
+ }
1922
+ }
1923
+ function findTsxFallback(repoRoot) {
1924
+ const candidates = [
1925
+ path12.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
1926
+ path12.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
1927
+ path12.join(repoRoot, "node_modules", ".bin", "tsx")
1928
+ ];
1929
+ for (const c of candidates) {
1930
+ if (fs12.existsSync(c)) return c;
1931
+ }
1932
+ return null;
1933
+ }
1934
+ function getFnmBinDir(repoRoot) {
1935
+ const desiredVersion = readNodeVersion(repoRoot);
1936
+ if (!desiredVersion) return null;
1937
+ const nodePath = probeFnmNode(desiredVersion);
1938
+ if (!nodePath) return null;
1939
+ return path12.dirname(nodePath);
1940
+ }
1941
+ function resolveNodeRuntime(configCommand, repoRoot) {
1942
+ if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
1943
+ return {
1944
+ command: configCommand,
1945
+ supportsStripTypes: false,
1946
+ source: "bun",
1947
+ majorVersion: null
1948
+ };
1949
+ }
1950
+ const desiredVersion = readNodeVersion(repoRoot);
1951
+ if (desiredVersion) {
1952
+ const fnmNode = probeFnmNode(desiredVersion);
1953
+ if (fnmNode) {
1954
+ const major2 = detectNodeMajorVersion(fnmNode);
1955
+ return {
1956
+ command: fnmNode,
1957
+ supportsStripTypes: checkStripTypesSupport(fnmNode),
1958
+ source: "fnm",
1959
+ majorVersion: major2
1960
+ };
1961
+ }
1962
+ }
1963
+ const major = detectNodeMajorVersion(configCommand);
1964
+ if (major !== null) {
1965
+ return {
1966
+ command: configCommand,
1967
+ supportsStripTypes: checkStripTypesSupport(configCommand),
1968
+ source: major === detectNodeMajorVersion("node") ? "path" : "config",
1969
+ majorVersion: major
1970
+ };
1971
+ }
1972
+ const tsx = findTsxFallback(repoRoot);
1973
+ if (tsx) {
1974
+ return {
1975
+ command: tsx,
1976
+ supportsStripTypes: false,
1977
+ source: "tsx-fallback",
1978
+ majorVersion: null
1979
+ };
1980
+ }
1981
+ return {
1982
+ command: configCommand,
1983
+ supportsStripTypes: false,
1984
+ source: "path",
1985
+ majorVersion: null
1986
+ };
1987
+ }
1988
+ function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
1989
+ const fnmBin = getFnmBinDir(repoRoot);
1990
+ if (!fnmBin) return { ...baseEnv };
1991
+ const pathKey = process.platform === "win32" ? "Path" : "PATH";
1992
+ const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
1993
+ return {
1994
+ ...baseEnv,
1995
+ [pathKey]: `${fnmBin}${path12.delimiter}${currentPath}`
1996
+ };
1997
+ }
1998
+
1999
+ // src/engine/bridge.ts
2000
+ var DEFAULT_APP_SERVER_URL2 = "ws://127.0.0.1:4501";
2001
+ var APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
2002
+ var APP_SERVER_START_TIMEOUT_MS = 2e4;
2003
+ var APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
2004
+ var APP_SERVER_HEALTH_RETRY_MS = 250;
2005
+ var APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
2006
+ var APP_SERVER_AUTH_FILE_MODE = 384;
2007
+ function appServerLogFilePath(stateDir, instanceId) {
2008
+ return path13.join(stateDir, "logs", `app-server-${instanceId}.log`);
2009
+ }
2010
+ function appServerGatewayLogFilePath(stateDir, instanceId) {
2011
+ return path13.join(stateDir, "logs", `app-server-gateway-${instanceId}.log`);
2012
+ }
2013
+ function appServerGatewayTokenFilePath(stateDir, instanceId) {
2014
+ return path13.join(
2015
+ stateDir,
2016
+ "secrets",
2017
+ `app-server-gateway-${instanceId}.token`
2018
+ );
2019
+ }
2020
+ function stderrLogFilePath(logPath) {
2021
+ return `${logPath}.stderr`;
2022
+ }
2023
+ function writeProtectedTextFile(filePath, content) {
2024
+ fs13.mkdirSync(path13.dirname(filePath), { recursive: true });
2025
+ const tmp = `${filePath}.tmp.${process.pid}`;
2026
+ fs13.writeFileSync(tmp, content, {
2027
+ encoding: "utf-8",
2028
+ mode: APP_SERVER_AUTH_FILE_MODE
2029
+ });
2030
+ fs13.chmodSync(tmp, APP_SERVER_AUTH_FILE_MODE);
2031
+ fs13.renameSync(tmp, filePath);
2032
+ fs13.chmodSync(filePath, APP_SERVER_AUTH_FILE_MODE);
2033
+ }
2034
+ function removeFileIfExists(filePath) {
2035
+ if (!filePath || !fs13.existsSync(filePath)) {
2036
+ return;
2037
+ }
2038
+ try {
2039
+ fs13.unlinkSync(filePath);
2040
+ } catch {
2041
+ }
2042
+ }
2043
+ function getWebSocketCtor() {
2044
+ const candidate = globalThis.WebSocket;
2045
+ return typeof candidate === "function" ? candidate : null;
2046
+ }
2047
+ function delay(ms) {
2048
+ return new Promise((resolve11) => setTimeout(resolve11, ms));
2049
+ }
2050
+ function isLoopbackHost(hostname) {
2051
+ return hostname === "127.0.0.1" || hostname === "localhost";
2052
+ }
2053
+ function resolveCodexCommand(platform) {
2054
+ const candidates = platform === "win32" ? ["codex.cmd", "codex.exe", "codex", "codex.ps1"] : ["codex"];
2055
+ return probeCommand(candidates).command;
2056
+ }
2057
+ function formatCodexAppServerCommand(command, url) {
2058
+ return `${command} app-server --listen ${url}`;
2059
+ }
2060
+ function resolvePowerShellCommand() {
2061
+ return probeCommand(["pwsh", "powershell", "powershell.exe"]).command ?? "powershell";
2062
+ }
2063
+ function resolveAuthGatewayScript(repoRoot) {
2064
+ const moduleDir = path13.dirname(fileURLToPath4(import.meta.url));
2065
+ const candidates = [
2066
+ path13.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.mjs"),
2067
+ path13.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.ts"),
2068
+ path13.join(
2069
+ repoRoot,
2070
+ "packages",
2071
+ "tap-comms",
2072
+ "dist",
2073
+ "bridges",
2074
+ "codex-app-server-auth-gateway.mjs"
2075
+ ),
2076
+ path13.join(
2077
+ repoRoot,
2078
+ "packages",
2079
+ "tap-comms",
2080
+ "src",
2081
+ "bridges",
2082
+ "codex-app-server-auth-gateway.ts"
2083
+ )
2084
+ ];
2085
+ for (const candidate of candidates) {
2086
+ if (fs13.existsSync(candidate)) {
2087
+ return candidate;
2088
+ }
2089
+ }
2090
+ return null;
2091
+ }
2092
+ function getBridgeRuntimeStateDir(repoRoot, instanceId) {
2093
+ return path13.join(repoRoot, ".tmp", `codex-app-server-bridge-${instanceId}`);
2094
+ }
2095
+ async function allocateLoopbackPort(hostname) {
2096
+ const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
2097
+ return await new Promise((resolve11, reject) => {
2098
+ const server = net.createServer();
2099
+ server.unref();
2100
+ server.once("error", reject);
2101
+ server.listen(0, bindHost, () => {
2102
+ const address = server.address();
2103
+ if (!address || typeof address === "string") {
2104
+ server.close(() => {
2105
+ reject(new Error("Failed to allocate a loopback port"));
2106
+ });
2107
+ return;
2108
+ }
2109
+ const port = address.port;
2110
+ server.close((error) => {
2111
+ if (error) {
2112
+ reject(error);
2113
+ return;
2114
+ }
2115
+ resolve11(port);
2116
+ });
2117
+ });
2118
+ });
2119
+ }
2120
+ function buildProtectedAppServerUrl(publicUrl, token) {
2121
+ const url = new URL(publicUrl);
2122
+ url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
2123
+ return url.toString().replace(/\/(?=\?|$)/, "");
2124
+ }
2125
+ function readGatewayTokenFromPath(tokenPath) {
2126
+ return fs13.readFileSync(tokenPath, "utf8").trim();
2127
+ }
2128
+ function readGatewayToken(auth) {
2129
+ if (!auth) {
2130
+ return null;
2131
+ }
2132
+ const legacyToken = auth.token;
2133
+ if (legacyToken?.trim()) {
2134
+ return legacyToken.trim();
2135
+ }
2136
+ if (!auth.tokenPath || !fs13.existsSync(auth.tokenPath)) {
2137
+ return null;
2138
+ }
2139
+ const fileToken = readGatewayTokenFromPath(auth.tokenPath);
2140
+ return fileToken || null;
2141
+ }
2142
+ function materializeGatewayTokenFile(stateDir, instanceId, publicUrl, auth) {
2143
+ if (auth.tokenPath && fs13.existsSync(auth.tokenPath)) {
2144
+ return auth;
2145
+ }
2146
+ const token = readGatewayToken(auth);
2147
+ if (!token) {
2148
+ throw new Error(`Missing auth gateway token for ${instanceId}`);
2149
+ }
2150
+ const tokenPath = appServerGatewayTokenFilePath(stateDir, instanceId);
2151
+ writeProtectedTextFile(tokenPath, `${token}
2152
+ `);
2153
+ return {
2154
+ ...auth,
2155
+ protectedUrl: buildProtectedAppServerUrl(publicUrl, "***"),
2156
+ tokenPath
2157
+ };
2158
+ }
2159
+ async function createManagedAppServerAuth(options) {
2160
+ const publicUrl = new URL(options.publicUrl);
2161
+ const upstreamUrl = new URL(options.publicUrl);
2162
+ upstreamUrl.port = String(await allocateLoopbackPort(publicUrl.hostname));
2163
+ upstreamUrl.search = "";
2164
+ upstreamUrl.hash = "";
2165
+ const gatewayScript = resolveAuthGatewayScript(options.repoRoot);
2166
+ if (!gatewayScript) {
2167
+ throw new Error("Auth gateway script not found");
2168
+ }
2169
+ const token = randomBytes(24).toString("base64url");
2170
+ const tokenPath = appServerGatewayTokenFilePath(
2171
+ options.stateDir,
2172
+ options.instanceId
2173
+ );
2174
+ writeProtectedTextFile(tokenPath, `${token}
2175
+ `);
2176
+ const protectedUrl = buildProtectedAppServerUrl(options.publicUrl, "***");
2177
+ const gatewayLogPath = appServerGatewayLogFilePath(
2178
+ options.stateDir,
2179
+ options.instanceId
2180
+ );
2181
+ fs13.mkdirSync(path13.dirname(gatewayLogPath), { recursive: true });
2182
+ rotateLog(gatewayLogPath);
2183
+ const runtime = resolveNodeRuntime(process.execPath, options.repoRoot);
2184
+ const gatewayArgs = [];
2185
+ if (gatewayScript.endsWith(".ts")) {
2186
+ if (!runtime.supportsStripTypes) {
2187
+ throw new Error(
2188
+ "Current Node runtime cannot start the auth gateway from TypeScript source. Rebuild @hua-labs/tap or use Node 22.6+."
2189
+ );
2190
+ }
2191
+ gatewayArgs.push("--experimental-strip-types");
2192
+ }
2193
+ gatewayArgs.push(gatewayScript);
2194
+ const gatewayEnv = {
2195
+ ...buildRuntimeEnv(options.repoRoot),
2196
+ TAP_GATEWAY_LISTEN_URL: options.publicUrl,
2197
+ TAP_GATEWAY_UPSTREAM_URL: upstreamUrl.toString().replace(/\/$/, ""),
2198
+ TAP_GATEWAY_TOKEN_FILE: tokenPath
2199
+ };
2200
+ let gatewayPid;
2201
+ {
2202
+ let logFd = null;
2203
+ try {
2204
+ if (options.platform === "win32") {
2205
+ gatewayPid = startWindowsDetachedProcess(
2206
+ runtime.command,
2207
+ gatewayArgs,
2208
+ options.repoRoot,
2209
+ gatewayLogPath,
2210
+ gatewayEnv
2211
+ );
2212
+ } else {
2213
+ logFd = fs13.openSync(gatewayLogPath, "a");
2214
+ const child = spawn(runtime.command, gatewayArgs, {
2215
+ cwd: options.repoRoot,
2216
+ detached: true,
2217
+ stdio: ["ignore", logFd, logFd],
2218
+ env: gatewayEnv,
2219
+ windowsHide: true
2220
+ });
2221
+ child.unref();
2222
+ gatewayPid = child.pid ?? null;
2223
+ }
2224
+ } catch (error) {
2225
+ removeFileIfExists(tokenPath);
2226
+ throw error;
2227
+ } finally {
2228
+ if (logFd != null) {
2229
+ fs13.closeSync(logFd);
2230
+ }
2231
+ }
2232
+ }
2233
+ if (gatewayPid == null) {
2234
+ removeFileIfExists(tokenPath);
2235
+ throw new Error("Failed to spawn app-server auth gateway");
2236
+ }
2237
+ return {
2238
+ mode: "query-token",
2239
+ protectedUrl,
2240
+ upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
2241
+ tokenPath,
2242
+ gatewayPid,
2243
+ gatewayLogPath
2244
+ };
2245
+ }
2246
+ function canReuseManagedAppServer(appServer) {
2247
+ if (!appServer?.managed) {
2248
+ return false;
2249
+ }
2250
+ if (appServer.pid != null && !isProcessAlive(appServer.pid)) {
2251
+ return false;
2252
+ }
2253
+ const auth = appServer.auth;
2254
+ if (auth) {
2255
+ if (!auth.protectedUrl) {
2256
+ return false;
2257
+ }
2258
+ if (!readGatewayToken(auth)) {
2259
+ return false;
2260
+ }
2261
+ if (auth.gatewayPid != null && !isProcessAlive(auth.gatewayPid)) {
2262
+ return false;
2263
+ }
2264
+ }
2265
+ return true;
2266
+ }
2267
+ function markAppServerHealthy(appServer) {
2268
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
2269
+ return {
2270
+ ...appServer,
2271
+ healthy: true,
2272
+ lastCheckedAt: checkedAt,
2273
+ lastHealthyAt: checkedAt
2274
+ };
2275
+ }
2276
+ function findReusableManagedAppServer(stateDir, publicUrl) {
2277
+ const pidDir = path13.join(stateDir, "pids");
2278
+ if (!fs13.existsSync(pidDir)) {
2279
+ return null;
2280
+ }
2281
+ for (const name of fs13.readdirSync(pidDir)) {
2282
+ if (!name.startsWith("bridge-") || !name.endsWith(".json")) {
2283
+ continue;
2284
+ }
2285
+ try {
2286
+ const raw = fs13.readFileSync(path13.join(pidDir, name), "utf-8");
2287
+ const parsed = JSON.parse(raw);
2288
+ if (parsed.appServer?.url !== publicUrl) {
2289
+ continue;
2290
+ }
2291
+ if (canReuseManagedAppServer(parsed.appServer)) {
2292
+ return markAppServerHealthy(parsed.appServer);
2293
+ }
2294
+ } catch {
2295
+ }
2296
+ }
2297
+ return null;
2298
+ }
2299
+ function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
2300
+ const ext = path13.extname(command).toLowerCase();
2301
+ const stderrLogPath = stderrLogFilePath(logPath);
2302
+ const stdoutFd = fs13.openSync(logPath, "a");
2303
+ const stderrFd = fs13.openSync(stderrLogPath, "a");
2304
+ try {
2305
+ const child = ext === ".ps1" ? spawn(
2306
+ resolvePowerShellCommand(),
2307
+ ["-NoLogo", "-NoProfile", "-File", command, ...args],
2308
+ {
2309
+ cwd: repoRoot,
2310
+ detached: true,
2311
+ stdio: ["ignore", stdoutFd, stderrFd],
2312
+ env,
2313
+ windowsHide: true
2314
+ }
2315
+ ) : spawn(command, args, {
2316
+ cwd: repoRoot,
2317
+ detached: true,
2318
+ stdio: ["ignore", stdoutFd, stderrFd],
2319
+ env,
2320
+ windowsHide: true,
2321
+ shell: ext === ".cmd" || ext === ".bat"
2322
+ });
2323
+ child.unref();
2324
+ return child.pid ?? null;
2325
+ } finally {
2326
+ fs13.closeSync(stdoutFd);
2327
+ fs13.closeSync(stderrFd);
2328
+ }
2329
+ }
2330
+ function startWindowsCodexAppServer(command, url, repoRoot, logPath) {
2331
+ return startWindowsDetachedProcess(
2332
+ command,
2333
+ ["app-server", "--listen", url],
2334
+ repoRoot,
2335
+ logPath
2336
+ );
2337
+ }
2338
+ function findListeningProcessId(url, platform) {
2339
+ if (platform !== "win32") {
2340
+ return null;
2341
+ }
2342
+ let port;
2343
+ try {
2344
+ const parsed = new URL(url);
2345
+ port = parsed.port ? Number.parseInt(parsed.port, 10) : null;
2346
+ } catch {
2347
+ return null;
2348
+ }
2349
+ if (port == null || !Number.isFinite(port)) {
2350
+ return null;
2351
+ }
2352
+ const result = spawnSync2(
2353
+ resolvePowerShellCommand(),
2354
+ [
2355
+ "-NoLogo",
2356
+ "-NoProfile",
2357
+ "-Command",
2358
+ [
2359
+ `$port = ${port}`,
2360
+ "$processId = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty OwningProcess",
2361
+ "if ($processId) { $processId }"
2362
+ ].join("; ")
2363
+ ],
2364
+ {
2365
+ encoding: "utf-8",
2366
+ windowsHide: true
2367
+ }
2368
+ );
2369
+ if (result.status !== 0) {
2370
+ return null;
2371
+ }
2372
+ const parsedPid = Number.parseInt((result.stdout ?? "").trim(), 10);
2373
+ return Number.isFinite(parsedPid) ? parsedPid : null;
2374
+ }
2375
+ function resolveAppServerUrl(baseUrl, port) {
2376
+ const resolvedBase = (baseUrl ?? DEFAULT_APP_SERVER_URL2).replace(/\/$/, "");
2377
+ if (port == null) {
2378
+ return resolvedBase;
2379
+ }
2380
+ try {
2381
+ const parsed = new URL(resolvedBase);
2382
+ parsed.port = String(port);
2383
+ return parsed.toString().replace(/\/$/, "");
2384
+ } catch {
2385
+ return resolvedBase;
2386
+ }
2387
+ }
2388
+ async function isTcpPortAvailable(hostname, port) {
2389
+ const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
2390
+ return await new Promise((resolve11) => {
2391
+ const server = net.createServer();
2392
+ server.unref();
2393
+ server.once("error", () => resolve11(false));
2394
+ server.listen(port, bindHost, () => {
2395
+ server.close((error) => resolve11(!error));
2396
+ });
2397
+ });
2398
+ }
2399
+ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, excludeInstanceId) {
2400
+ let hostname = "127.0.0.1";
2401
+ try {
2402
+ hostname = new URL(baseUrl ?? DEFAULT_APP_SERVER_URL2).hostname;
2403
+ } catch {
2404
+ }
2405
+ const maxAttempts = 1e3;
2406
+ let port = basePort;
2407
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1, port += 1) {
2408
+ const claimedInState = Object.entries(state.instances).some(
2409
+ ([id, inst]) => id !== excludeInstanceId && inst.port === port
2410
+ );
2411
+ if (claimedInState) {
2412
+ continue;
2413
+ }
2414
+ if (!isLoopbackHost(hostname)) {
2415
+ return port;
2416
+ }
2417
+ if (await isTcpPortAvailable(hostname, port)) {
2418
+ return port;
2419
+ }
2420
+ }
2421
+ throw new Error(
2422
+ `Failed to find a free app-server port starting at ${basePort}`
2423
+ );
2424
+ }
2425
+ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
2426
+ const WebSocket = getWebSocketCtor();
2427
+ if (!WebSocket) {
2428
+ return false;
2429
+ }
2430
+ return new Promise((resolve11) => {
2431
+ let settled = false;
2432
+ let socket = null;
2433
+ const finish = (healthy) => {
2434
+ if (settled) {
2435
+ return;
2436
+ }
2437
+ settled = true;
2438
+ clearTimeout(timer);
2439
+ try {
2440
+ socket?.close();
2441
+ } catch {
2442
+ }
2443
+ resolve11(healthy);
2444
+ };
2445
+ const timer = setTimeout(() => finish(false), timeoutMs);
2446
+ try {
2447
+ socket = new WebSocket(url);
2448
+ socket.addEventListener("open", () => finish(true), { once: true });
2449
+ socket.addEventListener("error", () => finish(false), { once: true });
2450
+ socket.addEventListener("close", () => finish(false), { once: true });
2451
+ } catch {
2452
+ finish(false);
2453
+ }
2454
+ });
2455
+ }
2456
+ async function waitForAppServerHealth(url, timeoutMs) {
2457
+ const deadline = Date.now() + timeoutMs;
2458
+ while (Date.now() < deadline) {
2459
+ if (await checkAppServerHealth(url)) {
2460
+ return true;
2461
+ }
2462
+ await delay(APP_SERVER_HEALTH_RETRY_MS);
2463
+ }
2464
+ return false;
2465
+ }
2466
+ async function terminateProcess(pid, platform) {
2467
+ if (!isProcessAlive(pid)) {
2468
+ return false;
2469
+ }
2470
+ try {
2471
+ if (platform === "win32") {
2472
+ execSync3(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
2473
+ } else {
2474
+ process.kill(pid, "SIGTERM");
2475
+ await delay(2e3);
2476
+ if (isProcessAlive(pid)) {
2477
+ process.kill(pid, "SIGKILL");
2478
+ }
2479
+ }
2480
+ } catch {
2481
+ }
2482
+ return !isProcessAlive(pid);
2483
+ }
2484
+ async function stopManagedAppServer(appServer, platform) {
2485
+ if (!appServer.managed) {
2486
+ return false;
2487
+ }
2488
+ let stopped = false;
2489
+ if (appServer.auth?.gatewayPid != null) {
2490
+ stopped = await terminateProcess(appServer.auth.gatewayPid, platform) || stopped;
2491
+ }
2492
+ if (appServer.pid != null) {
2493
+ stopped = await terminateProcess(appServer.pid, platform) || stopped;
2494
+ }
2495
+ removeFileIfExists(appServer.auth?.tokenPath);
2496
+ return stopped;
2497
+ }
2498
+ async function ensureCodexAppServer(options) {
2499
+ const effectiveUrl = resolveAppServerUrl(options.appServerUrl);
2500
+ const fallbackManualCommand = formatCodexAppServerCommand(
2501
+ "codex",
2502
+ effectiveUrl
2503
+ );
2504
+ if (options.existingAppServer?.url === effectiveUrl && canReuseManagedAppServer(options.existingAppServer)) {
2505
+ return markAppServerHealthy(options.existingAppServer);
2506
+ }
2507
+ const sharedManaged = findReusableManagedAppServer(
2508
+ options.stateDir,
2509
+ effectiveUrl
2510
+ );
2511
+ if (sharedManaged) {
2512
+ return sharedManaged;
2513
+ }
2514
+ let parsedUrl;
1816
2515
  try {
1817
- execSync2(`"${command}" --experimental-strip-types -e ""`, {
1818
- timeout: 5e3,
1819
- stdio: "pipe"
1820
- });
1821
- return true;
2516
+ parsedUrl = new URL(effectiveUrl);
1822
2517
  } catch {
1823
- return false;
2518
+ throw new Error(
2519
+ `Invalid app-server URL: ${effectiveUrl}
2520
+ Start it manually:
2521
+ ${fallbackManualCommand}`
2522
+ );
1824
2523
  }
1825
- }
1826
- function findTsxFallback(repoRoot) {
1827
- const candidates = [
1828
- path11.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
1829
- path11.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
1830
- path11.join(repoRoot, "node_modules", ".bin", "tsx")
1831
- ];
1832
- for (const c of candidates) {
1833
- if (fs11.existsSync(c)) return c;
2524
+ if (!isLoopbackHost(parsedUrl.hostname)) {
2525
+ throw new Error(
2526
+ `Auto-start only supports loopback app-server URLs. Current URL: ${effectiveUrl}
2527
+ Start it manually:
2528
+ ${fallbackManualCommand}`
2529
+ );
1834
2530
  }
1835
- return null;
1836
- }
1837
- function getFnmBinDir(repoRoot) {
1838
- const desiredVersion = readNodeVersion(repoRoot);
1839
- if (!desiredVersion) return null;
1840
- const nodePath = probeFnmNode(desiredVersion);
1841
- if (!nodePath) return null;
1842
- return path11.dirname(nodePath);
1843
- }
1844
- function resolveNodeRuntime(configCommand, repoRoot) {
1845
- if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
2531
+ if (await checkAppServerHealth(effectiveUrl)) {
2532
+ const hint = options.noAuth ? "Stop it first or use --no-server for an unmanaged external app-server." : "A listener is already running, so tap cannot insert the auth gateway there.\nStop it first or use --no-server for an unmanaged external app-server.";
2533
+ throw new Error(`${effectiveUrl}: ${hint}`);
2534
+ }
2535
+ const resolvedCommand = resolveCodexCommand(options.platform);
2536
+ if (!resolvedCommand) {
2537
+ throw new Error(
2538
+ `Codex CLI not found in PATH.
2539
+ Start the app-server manually:
2540
+ ${fallbackManualCommand}`
2541
+ );
2542
+ }
2543
+ const logPath = appServerLogFilePath(options.stateDir, options.instanceId);
2544
+ fs13.mkdirSync(path13.dirname(logPath), { recursive: true });
2545
+ rotateLog(logPath);
2546
+ if (options.noAuth) {
2547
+ const manualCommand2 = formatCodexAppServerCommand("codex", effectiveUrl);
2548
+ let pid2;
2549
+ if (options.platform === "win32") {
2550
+ try {
2551
+ pid2 = startWindowsCodexAppServer(
2552
+ resolvedCommand,
2553
+ effectiveUrl,
2554
+ options.repoRoot,
2555
+ logPath
2556
+ );
2557
+ } catch (err) {
2558
+ throw new Error(
2559
+ `Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
2560
+ Start it manually:
2561
+ ${manualCommand2}`,
2562
+ { cause: err }
2563
+ );
2564
+ }
2565
+ } else {
2566
+ const logFd = fs13.openSync(logPath, "a");
2567
+ try {
2568
+ const child = spawn(
2569
+ resolvedCommand,
2570
+ ["app-server", "--listen", effectiveUrl],
2571
+ {
2572
+ cwd: options.repoRoot,
2573
+ detached: true,
2574
+ stdio: ["ignore", logFd, logFd],
2575
+ env: process.env,
2576
+ windowsHide: true
2577
+ }
2578
+ );
2579
+ child.unref();
2580
+ pid2 = child.pid ?? null;
2581
+ } catch (err) {
2582
+ throw new Error(
2583
+ `Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
2584
+ Start it manually:
2585
+ ${manualCommand2}`,
2586
+ { cause: err }
2587
+ );
2588
+ } finally {
2589
+ fs13.closeSync(logFd);
2590
+ }
2591
+ }
2592
+ if (pid2 == null) {
2593
+ throw new Error(
2594
+ `Failed to spawn Codex app-server.
2595
+ Start it manually:
2596
+ ${manualCommand2}`
2597
+ );
2598
+ }
2599
+ const healthy2 = await waitForAppServerHealth(
2600
+ effectiveUrl,
2601
+ APP_SERVER_START_TIMEOUT_MS
2602
+ );
2603
+ if (!healthy2) {
2604
+ await terminateProcess(pid2, options.platform);
2605
+ throw new Error(
2606
+ `Codex app-server did not become healthy at ${effectiveUrl}.
2607
+ Check ${logPath}
2608
+ Or start it manually:
2609
+ ${manualCommand2}`
2610
+ );
2611
+ }
2612
+ pid2 = findListeningProcessId(effectiveUrl, options.platform) ?? pid2;
2613
+ const healthyAt2 = (/* @__PURE__ */ new Date()).toISOString();
1846
2614
  return {
1847
- command: configCommand,
1848
- supportsStripTypes: false,
1849
- source: "bun",
1850
- majorVersion: null
2615
+ url: effectiveUrl,
2616
+ pid: pid2,
2617
+ managed: true,
2618
+ healthy: true,
2619
+ lastCheckedAt: healthyAt2,
2620
+ lastHealthyAt: healthyAt2,
2621
+ logPath,
2622
+ manualCommand: manualCommand2,
2623
+ auth: null
1851
2624
  };
1852
2625
  }
1853
- const desiredVersion = readNodeVersion(repoRoot);
1854
- if (desiredVersion) {
1855
- const fnmNode = probeFnmNode(desiredVersion);
1856
- if (fnmNode) {
1857
- const major2 = detectNodeMajorVersion(fnmNode);
1858
- return {
1859
- command: fnmNode,
1860
- supportsStripTypes: checkStripTypesSupport(fnmNode),
1861
- source: "fnm",
1862
- majorVersion: major2
1863
- };
2626
+ const auth = await createManagedAppServerAuth({
2627
+ instanceId: options.instanceId,
2628
+ stateDir: options.stateDir,
2629
+ repoRoot: options.repoRoot,
2630
+ platform: options.platform,
2631
+ publicUrl: effectiveUrl
2632
+ });
2633
+ const manualCommand = formatCodexAppServerCommand("codex", auth.upstreamUrl);
2634
+ let pid;
2635
+ if (options.platform === "win32") {
2636
+ try {
2637
+ pid = startWindowsCodexAppServer(
2638
+ resolvedCommand,
2639
+ auth.upstreamUrl,
2640
+ options.repoRoot,
2641
+ logPath
2642
+ );
2643
+ } catch (err) {
2644
+ if (auth.gatewayPid != null) {
2645
+ await terminateProcess(auth.gatewayPid, options.platform);
2646
+ }
2647
+ removeFileIfExists(auth.tokenPath);
2648
+ throw new Error(
2649
+ `Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
2650
+ Start it manually:
2651
+ ${manualCommand}`,
2652
+ { cause: err }
2653
+ );
2654
+ }
2655
+ } else {
2656
+ const logFd = fs13.openSync(logPath, "a");
2657
+ try {
2658
+ const child = spawn(
2659
+ resolvedCommand,
2660
+ ["app-server", "--listen", auth.upstreamUrl],
2661
+ {
2662
+ cwd: options.repoRoot,
2663
+ detached: true,
2664
+ stdio: ["ignore", logFd, logFd],
2665
+ env: process.env,
2666
+ windowsHide: true
2667
+ }
2668
+ );
2669
+ child.unref();
2670
+ pid = child.pid ?? null;
2671
+ } catch (err) {
2672
+ if (auth.gatewayPid != null) {
2673
+ await terminateProcess(auth.gatewayPid, options.platform);
2674
+ }
2675
+ removeFileIfExists(auth.tokenPath);
2676
+ throw new Error(
2677
+ `Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
2678
+ Start it manually:
2679
+ ${manualCommand}`,
2680
+ { cause: err }
2681
+ );
2682
+ } finally {
2683
+ fs13.closeSync(logFd);
1864
2684
  }
1865
2685
  }
1866
- const major = detectNodeMajorVersion(configCommand);
1867
- if (major !== null) {
1868
- return {
1869
- command: configCommand,
1870
- supportsStripTypes: checkStripTypesSupport(configCommand),
1871
- source: major === detectNodeMajorVersion("node") ? "path" : "config",
1872
- majorVersion: major
1873
- };
2686
+ if (pid == null) {
2687
+ if (auth.gatewayPid != null) {
2688
+ await terminateProcess(auth.gatewayPid, options.platform);
2689
+ }
2690
+ removeFileIfExists(auth.tokenPath);
2691
+ throw new Error(
2692
+ `Failed to spawn Codex app-server.
2693
+ Start it manually:
2694
+ ${manualCommand}`
2695
+ );
1874
2696
  }
1875
- const tsx = findTsxFallback(repoRoot);
1876
- if (tsx) {
1877
- return {
1878
- command: tsx,
1879
- supportsStripTypes: false,
1880
- source: "tsx-fallback",
1881
- majorVersion: null
1882
- };
2697
+ const healthy = await waitForAppServerHealth(
2698
+ auth.upstreamUrl,
2699
+ APP_SERVER_START_TIMEOUT_MS
2700
+ );
2701
+ if (!healthy) {
2702
+ await terminateProcess(pid, options.platform);
2703
+ if (auth.gatewayPid != null) {
2704
+ await terminateProcess(auth.gatewayPid, options.platform);
2705
+ }
2706
+ removeFileIfExists(auth.tokenPath);
2707
+ throw new Error(
2708
+ `Codex app-server did not become healthy at ${auth.upstreamUrl}.
2709
+ Check ${logPath}
2710
+ Or start it manually:
2711
+ ${manualCommand}`
2712
+ );
1883
2713
  }
2714
+ const gatewayToken = readGatewayToken(auth);
2715
+ if (!gatewayToken) {
2716
+ await terminateProcess(pid, options.platform);
2717
+ if (auth.gatewayPid != null) {
2718
+ await terminateProcess(auth.gatewayPid, options.platform);
2719
+ }
2720
+ removeFileIfExists(auth.tokenPath);
2721
+ throw new Error("Tap auth gateway token is missing after startup.");
2722
+ }
2723
+ const gatewayHealthy = await waitForAppServerHealth(
2724
+ buildProtectedAppServerUrl(effectiveUrl, gatewayToken),
2725
+ APP_SERVER_GATEWAY_START_TIMEOUT_MS
2726
+ );
2727
+ if (!gatewayHealthy) {
2728
+ await terminateProcess(pid, options.platform);
2729
+ if (auth.gatewayPid != null) {
2730
+ await terminateProcess(auth.gatewayPid, options.platform);
2731
+ }
2732
+ removeFileIfExists(auth.tokenPath);
2733
+ throw new Error(
2734
+ `Tap auth gateway did not become healthy at ${effectiveUrl}.
2735
+ Check ${auth.gatewayLogPath ?? "the gateway log"} and ${logPath}.`
2736
+ );
2737
+ }
2738
+ const healthyAt = (/* @__PURE__ */ new Date()).toISOString();
2739
+ pid = findListeningProcessId(auth.upstreamUrl, options.platform) ?? pid;
1884
2740
  return {
1885
- command: configCommand,
1886
- supportsStripTypes: false,
1887
- source: "path",
1888
- majorVersion: null
1889
- };
1890
- }
1891
- function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
1892
- const fnmBin = getFnmBinDir(repoRoot);
1893
- if (!fnmBin) return { ...baseEnv };
1894
- const pathKey = process.platform === "win32" ? "Path" : "PATH";
1895
- const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
1896
- return {
1897
- ...baseEnv,
1898
- [pathKey]: `${fnmBin}${path11.delimiter}${currentPath}`
2741
+ url: effectiveUrl,
2742
+ pid,
2743
+ managed: true,
2744
+ healthy: true,
2745
+ lastCheckedAt: healthyAt,
2746
+ lastHealthyAt: healthyAt,
2747
+ logPath,
2748
+ manualCommand,
2749
+ auth
1899
2750
  };
1900
2751
  }
1901
-
1902
- // src/engine/bridge.ts
1903
2752
  function pidFilePath(stateDir, instanceId) {
1904
- return path12.join(stateDir, "pids", `bridge-${instanceId}.json`);
2753
+ return path13.join(stateDir, "pids", `bridge-${instanceId}.json`);
1905
2754
  }
1906
2755
  function logFilePath(stateDir, instanceId) {
1907
- return path12.join(stateDir, "logs", `bridge-${instanceId}.log`);
2756
+ return path13.join(stateDir, "logs", `bridge-${instanceId}.log`);
2757
+ }
2758
+ function runtimeHeartbeatFilePath(runtimeStateDir) {
2759
+ return path13.join(runtimeStateDir, "heartbeat.json");
2760
+ }
2761
+ function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
2762
+ if (!runtimeStateDir) {
2763
+ return null;
2764
+ }
2765
+ const heartbeatPath = runtimeHeartbeatFilePath(runtimeStateDir);
2766
+ if (!fs13.existsSync(heartbeatPath)) {
2767
+ return null;
2768
+ }
2769
+ try {
2770
+ const raw = fs13.readFileSync(heartbeatPath, "utf-8");
2771
+ const parsed = JSON.parse(raw);
2772
+ return typeof parsed.updatedAt === "string" ? parsed.updatedAt : null;
2773
+ } catch {
2774
+ return null;
2775
+ }
2776
+ }
2777
+ function resolveHeartbeatTimestamp(state) {
2778
+ return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
1908
2779
  }
1909
2780
  function loadBridgeState(stateDir, instanceId) {
1910
2781
  const pidPath = pidFilePath(stateDir, instanceId);
1911
- if (!fs12.existsSync(pidPath)) return null;
2782
+ if (!fs13.existsSync(pidPath)) return null;
1912
2783
  try {
1913
- const raw = fs12.readFileSync(pidPath, "utf-8");
2784
+ const raw = fs13.readFileSync(pidPath, "utf-8");
1914
2785
  return JSON.parse(raw);
1915
2786
  } catch {
1916
2787
  return null;
@@ -1918,15 +2789,16 @@ function loadBridgeState(stateDir, instanceId) {
1918
2789
  }
1919
2790
  function saveBridgeState(stateDir, instanceId, state) {
1920
2791
  const pidPath = pidFilePath(stateDir, instanceId);
1921
- fs12.mkdirSync(path12.dirname(pidPath), { recursive: true });
1922
- const tmp = `${pidPath}.tmp.${process.pid}`;
1923
- fs12.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
1924
- fs12.renameSync(tmp, pidPath);
2792
+ const serializable = JSON.parse(JSON.stringify(state));
2793
+ if (serializable.appServer?.auth) {
2794
+ delete serializable.appServer.auth.token;
2795
+ }
2796
+ writeProtectedTextFile(pidPath, JSON.stringify(serializable, null, 2));
1925
2797
  }
1926
2798
  function clearBridgeState(stateDir, instanceId) {
1927
2799
  const pidPath = pidFilePath(stateDir, instanceId);
1928
- if (fs12.existsSync(pidPath)) {
1929
- fs12.unlinkSync(pidPath);
2800
+ if (fs13.existsSync(pidPath)) {
2801
+ fs13.unlinkSync(pidPath);
1930
2802
  }
1931
2803
  }
1932
2804
  function isProcessAlive(pid) {
@@ -1964,31 +2836,61 @@ async function startBridge(options) {
1964
2836
  `Bridge for ${instanceId} is already running (PID: ${existing.pid})`
1965
2837
  );
1966
2838
  }
2839
+ const previousBridgeState = loadBridgeState(stateDir, instanceId);
2840
+ const previousAppServer = previousBridgeState?.appServer ?? null;
1967
2841
  clearBridgeState(stateDir, instanceId);
1968
2842
  const logPath = logFilePath(stateDir, instanceId);
1969
- fs12.mkdirSync(path12.dirname(logPath), { recursive: true });
2843
+ fs13.mkdirSync(path13.dirname(logPath), { recursive: true });
1970
2844
  rotateLog(logPath);
1971
- const logFd = fs12.openSync(logPath, "a");
1972
- const repoRoot = options.repoRoot ?? path12.resolve(stateDir, "..");
2845
+ let logFd = null;
2846
+ const repoRoot = options.repoRoot ?? path13.resolve(stateDir, "..");
2847
+ const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
1973
2848
  const resolved = resolveNodeRuntime(
1974
2849
  options.runtimeCommand ?? "node",
1975
2850
  repoRoot
1976
2851
  );
1977
2852
  const command = resolved.command;
1978
2853
  const runtimeEnv = buildRuntimeEnv(repoRoot);
1979
- const child = spawn(command, [bridgeScript], {
1980
- detached: true,
1981
- stdio: ["ignore", logFd, logFd],
1982
- env: {
2854
+ const effectiveAppServerUrl = resolveAppServerUrl(options.appServerUrl, port);
2855
+ let appServer = null;
2856
+ let bridgeAppServerUrl = effectiveAppServerUrl;
2857
+ if (runtime === "codex" && options.manageAppServer) {
2858
+ appServer = await ensureCodexAppServer({
2859
+ instanceId,
2860
+ stateDir,
2861
+ repoRoot,
2862
+ platform: options.platform,
2863
+ appServerUrl: effectiveAppServerUrl,
2864
+ existingAppServer: previousAppServer,
2865
+ noAuth: options.noAuth
2866
+ });
2867
+ if (appServer.auth) {
2868
+ appServer = {
2869
+ ...appServer,
2870
+ auth: materializeGatewayTokenFile(
2871
+ stateDir,
2872
+ instanceId,
2873
+ effectiveAppServerUrl,
2874
+ appServer.auth
2875
+ )
2876
+ };
2877
+ }
2878
+ bridgeAppServerUrl = effectiveAppServerUrl;
2879
+ }
2880
+ try {
2881
+ const bridgeEnv = {
1983
2882
  ...runtimeEnv,
1984
2883
  TAP_COMMS_DIR: commsDir,
2884
+ TAP_STATE_DIR: runtimeStateDir,
1985
2885
  TAP_BRIDGE_RUNTIME: runtime,
1986
2886
  TAP_BRIDGE_INSTANCE_ID: instanceId,
2887
+ TAP_AGENT_ID: instanceId,
1987
2888
  TAP_AGENT_NAME: resolvedAgent,
1988
2889
  CODEX_TAP_AGENT_NAME: resolvedAgent,
1989
2890
  TAP_RESOLVED_NODE: resolved.command,
1990
2891
  TAP_STRIP_TYPES: resolved.supportsStripTypes ? "1" : "0",
1991
- ...options.appServerUrl ? { CODEX_APP_SERVER_URL: options.appServerUrl } : {},
2892
+ ...bridgeAppServerUrl ? { CODEX_APP_SERVER_URL: bridgeAppServerUrl } : {},
2893
+ ...appServer?.auth?.tokenPath ? { TAP_GATEWAY_TOKEN_FILE: appServer.auth.tokenPath } : {},
1992
2894
  ...port != null ? { TAP_BRIDGE_PORT: String(port) } : {},
1993
2895
  ...options.headless?.enabled ? {
1994
2896
  TAP_HEADLESS: "true",
@@ -1996,7 +2898,6 @@ async function startBridge(options) {
1996
2898
  TAP_MAX_REVIEW_ROUNDS: String(options.headless.maxRounds),
1997
2899
  TAP_QUALITY_FLOOR: options.headless.qualitySeverityFloor
1998
2900
  } : {},
1999
- // Bridge script operational flags
2000
2901
  ...options.busyMode ? { TAP_BUSY_MODE: options.busyMode } : {},
2001
2902
  ...options.pollSeconds != null ? { TAP_POLL_SECONDS: String(options.pollSeconds) } : {},
2002
2903
  ...options.reconnectSeconds != null ? { TAP_RECONNECT_SECONDS: String(options.reconnectSeconds) } : {},
@@ -2008,20 +2909,55 @@ async function startBridge(options) {
2008
2909
  ...options.threadId ? { TAP_THREAD_ID: options.threadId } : {},
2009
2910
  ...options.ephemeral ? { TAP_EPHEMERAL: "true" } : {},
2010
2911
  ...options.processExistingMessages ? { TAP_PROCESS_EXISTING: "true" } : {}
2912
+ };
2913
+ let bridgePid = null;
2914
+ if (options.platform === "win32") {
2915
+ bridgePid = startWindowsDetachedProcess(
2916
+ command,
2917
+ [bridgeScript],
2918
+ repoRoot,
2919
+ logPath,
2920
+ bridgeEnv
2921
+ );
2922
+ } else {
2923
+ logFd = fs13.openSync(logPath, "a");
2924
+ const child = spawn(command, [bridgeScript], {
2925
+ detached: true,
2926
+ stdio: ["ignore", logFd, logFd],
2927
+ env: bridgeEnv,
2928
+ windowsHide: true
2929
+ });
2930
+ child.unref();
2931
+ bridgePid = child.pid ?? null;
2011
2932
  }
2012
- });
2013
- child.unref();
2014
- fs12.closeSync(logFd);
2015
- if (!child.pid) {
2016
- throw new Error(`Failed to spawn bridge process for ${instanceId}`);
2017
- }
2018
- const state = {
2019
- pid: child.pid,
2020
- statePath: pidFilePath(stateDir, instanceId),
2021
- lastHeartbeat: (/* @__PURE__ */ new Date()).toISOString()
2022
- };
2023
- saveBridgeState(stateDir, instanceId, state);
2024
- return state;
2933
+ if (logFd != null) {
2934
+ fs13.closeSync(logFd);
2935
+ logFd = null;
2936
+ }
2937
+ if (!bridgePid) {
2938
+ throw new Error(`Failed to spawn bridge process for ${instanceId}`);
2939
+ }
2940
+ const state = {
2941
+ pid: bridgePid,
2942
+ statePath: pidFilePath(stateDir, instanceId),
2943
+ lastHeartbeat: (/* @__PURE__ */ new Date()).toISOString(),
2944
+ appServer,
2945
+ runtimeStateDir
2946
+ };
2947
+ saveBridgeState(stateDir, instanceId, state);
2948
+ return state;
2949
+ } catch (err) {
2950
+ if (logFd != null) {
2951
+ try {
2952
+ fs13.closeSync(logFd);
2953
+ } catch {
2954
+ }
2955
+ }
2956
+ if (appServer?.managed) {
2957
+ await stopManagedAppServer(appServer, options.platform);
2958
+ }
2959
+ throw err;
2960
+ }
2025
2961
  }
2026
2962
  async function stopBridge(options) {
2027
2963
  const { instanceId, stateDir, platform } = options;
@@ -2034,37 +2970,33 @@ async function stopBridge(options) {
2034
2970
  return false;
2035
2971
  }
2036
2972
  try {
2037
- if (platform === "win32") {
2038
- execSync3(`taskkill /PID ${state.pid} /F /T`, { stdio: "pipe" });
2039
- } else {
2040
- process.kill(state.pid, "SIGTERM");
2041
- await new Promise((resolve10) => setTimeout(resolve10, 2e3));
2042
- if (isProcessAlive(state.pid)) {
2043
- process.kill(state.pid, "SIGKILL");
2044
- }
2045
- }
2973
+ await terminateProcess(state.pid, platform);
2046
2974
  } catch {
2047
2975
  }
2048
2976
  clearBridgeState(stateDir, instanceId);
2049
2977
  return true;
2050
2978
  }
2051
2979
  function rotateLog(logPath) {
2052
- if (!fs12.existsSync(logPath)) return;
2980
+ if (!fs13.existsSync(logPath)) return;
2053
2981
  try {
2054
- const stats = fs12.statSync(logPath);
2982
+ const stats = fs13.statSync(logPath);
2055
2983
  if (stats.size === 0) return;
2056
2984
  const prevPath = `${logPath}.prev`;
2057
- fs12.renameSync(logPath, prevPath);
2985
+ fs13.renameSync(logPath, prevPath);
2058
2986
  } catch {
2059
2987
  }
2060
2988
  }
2061
2989
  function getHeartbeatAge(stateDir, instanceId) {
2062
2990
  const state = loadBridgeState(stateDir, instanceId);
2063
- if (!state?.lastHeartbeat) return null;
2064
- const heartbeatTime = new Date(state.lastHeartbeat).getTime();
2991
+ const heartbeat = resolveHeartbeatTimestamp(state);
2992
+ if (!heartbeat) return null;
2993
+ const heartbeatTime = new Date(heartbeat).getTime();
2065
2994
  if (isNaN(heartbeatTime)) return null;
2066
2995
  return Math.floor((Date.now() - heartbeatTime) / 1e3);
2067
2996
  }
2997
+ function getBridgeHeartbeatTimestamp(stateDir, instanceId) {
2998
+ return resolveHeartbeatTimestamp(loadBridgeState(stateDir, instanceId));
2999
+ }
2068
3000
  function getBridgeStatus(stateDir, instanceId) {
2069
3001
  const state = loadBridgeState(stateDir, instanceId);
2070
3002
  if (!state) return "stopped";
@@ -2084,7 +3016,7 @@ async function addCommand(args) {
2084
3016
  ok: false,
2085
3017
  command: "add",
2086
3018
  code: "TAP_INVALID_ARGUMENT",
2087
- message: "Missing runtime argument. Usage: npx @hua-labs/tap add <claude|codex|gemini> [--name <name>] [--port <port>] [--headless] [--role <role>]",
3019
+ message: "Missing runtime argument. Usage: npx @hua-labs/tap add <claude|codex|gemini> [--name <name>] [--port <port>] [--agent-name <name>] [--headless] [--role <role>]",
2088
3020
  warnings: [],
2089
3021
  data: {}
2090
3022
  };
@@ -2104,6 +3036,7 @@ async function addCommand(args) {
2104
3036
  const instanceId = buildInstanceId(runtime, instanceName);
2105
3037
  const portStr = typeof flags["port"] === "string" ? flags["port"] : void 0;
2106
3038
  const port = portStr ? parseInt(portStr, 10) : null;
3039
+ const agentNameFlag = typeof flags["agent-name"] === "string" ? flags["agent-name"] : null;
2107
3040
  const force = flags["force"] === true;
2108
3041
  const headlessFlag = flags["headless"] === true;
2109
3042
  const roleArg = typeof flags["role"] === "string" ? flags["role"] : void 0;
@@ -2150,7 +3083,7 @@ async function addCommand(args) {
2150
3083
  data: {}
2151
3084
  };
2152
3085
  }
2153
- const repoRoot = findRepoRoot2();
3086
+ const repoRoot = findRepoRoot();
2154
3087
  const state = loadState(repoRoot);
2155
3088
  if (!state) {
2156
3089
  return {
@@ -2194,7 +3127,7 @@ async function addCommand(args) {
2194
3127
  logHeader(`@hua-labs/tap add ${instanceId}`);
2195
3128
  if (instanceName) log(`Instance name: ${instanceName}`);
2196
3129
  if (port !== null) log(`Port: ${port}`);
2197
- const ctx = createAdapterContext(state.commsDir, repoRoot);
3130
+ const ctx = { ...createAdapterContext(state.commsDir, repoRoot), instanceId };
2198
3131
  const adapter = getAdapter(runtime);
2199
3132
  const warnings = [];
2200
3133
  log("Probing runtime...");
@@ -2226,13 +3159,15 @@ async function addCommand(args) {
2226
3159
  log(`Artifacts: ${plan.ownedArtifacts.length}`);
2227
3160
  for (const w of plan.warnings) logWarn(w);
2228
3161
  if (plan.operations.length === 0) {
3162
+ const failureMessage = probe.issues[0] ?? plan.warnings[0] ?? probe.warnings[0] ?? "No operations to apply. Runtime not configured.";
3163
+ const failureCode = /MCP server/i.test(failureMessage) ? "TAP_LOCAL_SERVER_MISSING" : "TAP_PATCH_FAILED";
2229
3164
  return {
2230
- ok: true,
3165
+ ok: false,
2231
3166
  command: "add",
2232
3167
  runtime,
2233
3168
  instanceId,
2234
- code: "TAP_NO_OP",
2235
- message: "No operations to apply. Runtime not configured.",
3169
+ code: failureCode,
3170
+ message: failureMessage,
2236
3171
  warnings,
2237
3172
  data: { planOps: 0 }
2238
3173
  };
@@ -2280,10 +3215,10 @@ async function addCommand(args) {
2280
3215
  logWarn("Bridge script not found. Bridge not started.");
2281
3216
  warnings.push("Bridge script not found. Run bridge manually.");
2282
3217
  } else {
2283
- const agentNameEnv = process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;
2284
- if (!agentNameEnv) {
3218
+ const resolvedAgentName = agentNameFlag || process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;
3219
+ if (!resolvedAgentName) {
2285
3220
  logWarn(
2286
- "No agent name set (TAP_AGENT_NAME). Bridge not started. Use: npx @hua-labs/tap bridge start <instance> --agent-name <name>"
3221
+ "No agent name set. Bridge not started. Use: npx @hua-labs/tap bridge start <instance> --agent-name <name>"
2287
3222
  );
2288
3223
  warnings.push("Bridge not auto-started: no agent name available.");
2289
3224
  } else {
@@ -2297,7 +3232,7 @@ async function addCommand(args) {
2297
3232
  commsDir: ctx.commsDir,
2298
3233
  bridgeScript,
2299
3234
  platform: ctx.platform,
2300
- agentName: agentNameEnv,
3235
+ agentName: resolvedAgentName,
2301
3236
  runtimeCommand: resolvedCfg.runtimeCommand,
2302
3237
  appServerUrl: resolvedCfg.appServerUrl,
2303
3238
  repoRoot,
@@ -2313,10 +3248,11 @@ async function addCommand(args) {
2313
3248
  }
2314
3249
  }
2315
3250
  }
3251
+ const existingAgentName = state.instances[instanceId]?.agentName ?? null;
2316
3252
  const instanceState = {
2317
3253
  instanceId,
2318
3254
  runtime,
2319
- agentName: null,
3255
+ agentName: agentNameFlag ?? existingAgentName,
2320
3256
  port,
2321
3257
  installed: true,
2322
3258
  configPath: probe.configPath ?? "",
@@ -2382,7 +3318,7 @@ function instanceStatusLine(inst, status) {
2382
3318
  return `${inst.instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${status.padEnd(14)} ${mode.padEnd(14)}${bridgeInfo}${portStr}${restart}${warns}`;
2383
3319
  }
2384
3320
  async function statusCommand(_args) {
2385
- const repoRoot = findRepoRoot2();
3321
+ const repoRoot = findRepoRoot();
2386
3322
  const state = loadState(repoRoot);
2387
3323
  if (!state) {
2388
3324
  return {
@@ -2395,7 +3331,7 @@ async function statusCommand(_args) {
2395
3331
  };
2396
3332
  }
2397
3333
  logHeader("@hua-labs/tap status");
2398
- log(`Version: ${state.packageVersion}`);
3334
+ log(`Version: ${version}`);
2399
3335
  log(`Comms dir: ${state.commsDir}`);
2400
3336
  log(`Repo root: ${state.repoRoot}`);
2401
3337
  log(`Schema: v${state.schemaVersion}`);
@@ -2452,7 +3388,7 @@ async function statusCommand(_args) {
2452
3388
  message: `${installed.length} instance(s) installed`,
2453
3389
  warnings: [],
2454
3390
  data: {
2455
- version: state.packageVersion,
3391
+ version,
2456
3392
  commsDir: state.commsDir,
2457
3393
  repoRoot: state.repoRoot,
2458
3394
  instances
@@ -2461,7 +3397,7 @@ async function statusCommand(_args) {
2461
3397
  }
2462
3398
 
2463
3399
  // src/engine/rollback.ts
2464
- import * as fs13 from "fs";
3400
+ import * as fs14 from "fs";
2465
3401
  async function rollbackRuntime(_instanceId, runtimeState) {
2466
3402
  const errors = [];
2467
3403
  const restoredFiles = [];
@@ -2490,7 +3426,7 @@ async function rollbackRuntime(_instanceId, runtimeState) {
2490
3426
  };
2491
3427
  }
2492
3428
  function rollbackArtifact(artifact) {
2493
- if (!fs13.existsSync(artifact.path)) {
3429
+ if (!fs14.existsSync(artifact.path)) {
2494
3430
  return { restored: false, error: `File not found: ${artifact.path}` };
2495
3431
  }
2496
3432
  switch (artifact.kind) {
@@ -2508,7 +3444,7 @@ function rollbackArtifact(artifact) {
2508
3444
  }
2509
3445
  }
2510
3446
  function rollbackJsonPath(artifact) {
2511
- const raw = fs13.readFileSync(artifact.path, "utf-8");
3447
+ const raw = fs14.readFileSync(artifact.path, "utf-8");
2512
3448
  let config;
2513
3449
  try {
2514
3450
  config = JSON.parse(raw);
@@ -2534,18 +3470,18 @@ function rollbackJsonPath(artifact) {
2534
3470
  cleanEmptyParents(config, artifact.selector);
2535
3471
  }
2536
3472
  const tmp = `${artifact.path}.tmp.${process.pid}`;
2537
- fs13.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
2538
- fs13.renameSync(tmp, artifact.path);
3473
+ fs14.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
3474
+ fs14.renameSync(tmp, artifact.path);
2539
3475
  return { restored: true };
2540
3476
  }
2541
3477
  function rollbackTomlTable(artifact) {
2542
- const content = fs13.readFileSync(artifact.path, "utf-8");
3478
+ const content = fs14.readFileSync(artifact.path, "utf-8");
2543
3479
  const backup = artifact.backupPath ? readArtifactBackup(artifact.backupPath) : null;
2544
3480
  if (backup?.kind === "toml-table" && backup.selector === artifact.selector) {
2545
3481
  const nextContent = backup.existed ? replaceTomlTable(content, artifact.selector, backup.content ?? "") : removeTomlTable(content, artifact.selector);
2546
3482
  const tmp2 = `${artifact.path}.tmp.${process.pid}`;
2547
- fs13.writeFileSync(tmp2, nextContent, "utf-8");
2548
- fs13.renameSync(tmp2, artifact.path);
3483
+ fs14.writeFileSync(tmp2, nextContent, "utf-8");
3484
+ fs14.renameSync(tmp2, artifact.path);
2549
3485
  return { restored: true };
2550
3486
  }
2551
3487
  if (!extractTomlTable(content, artifact.selector)) {
@@ -2555,13 +3491,13 @@ function rollbackTomlTable(artifact) {
2555
3491
  };
2556
3492
  }
2557
3493
  const tmp = `${artifact.path}.tmp.${process.pid}`;
2558
- fs13.writeFileSync(tmp, removeTomlTable(content, artifact.selector), "utf-8");
2559
- fs13.renameSync(tmp, artifact.path);
3494
+ fs14.writeFileSync(tmp, removeTomlTable(content, artifact.selector), "utf-8");
3495
+ fs14.renameSync(tmp, artifact.path);
2560
3496
  return { restored: true };
2561
3497
  }
2562
3498
  function rollbackFile(artifact) {
2563
- if (fs13.existsSync(artifact.path)) {
2564
- fs13.unlinkSync(artifact.path);
3499
+ if (fs14.existsSync(artifact.path)) {
3500
+ fs14.unlinkSync(artifact.path);
2565
3501
  return { restored: true };
2566
3502
  }
2567
3503
  return { restored: false, error: `File not found: ${artifact.path}` };
@@ -2622,7 +3558,7 @@ async function removeCommand(args) {
2622
3558
  data: {}
2623
3559
  };
2624
3560
  }
2625
- const repoRoot = findRepoRoot2();
3561
+ const repoRoot = findRepoRoot();
2626
3562
  const state = loadState(repoRoot);
2627
3563
  if (!state) {
2628
3564
  return {
@@ -2708,7 +3644,7 @@ async function removeCommand(args) {
2708
3644
  }
2709
3645
 
2710
3646
  // src/commands/bridge.ts
2711
- import * as path13 from "path";
3647
+ import * as path14 from "path";
2712
3648
  function formatAge(seconds) {
2713
3649
  if (seconds < 60) return `${seconds}s ago`;
2714
3650
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
@@ -2720,6 +3656,7 @@ Usage:
2720
3656
 
2721
3657
  Subcommands:
2722
3658
  start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
3659
+ start --all Start all registered app-server instances
2723
3660
  stop <instance> Stop bridge for an instance
2724
3661
  stop Stop all running bridges
2725
3662
  status Show bridge status for all instances
@@ -2727,6 +3664,8 @@ Subcommands:
2727
3664
 
2728
3665
  Options:
2729
3666
  --agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
3667
+ Saved to state \u2014 only needed on first start
3668
+ --all Start all registered app-server instances
2730
3669
  --busy-mode <steer|wait> How to handle active turns (default: steer)
2731
3670
  --poll-seconds <n> Inbox poll interval (default: 5)
2732
3671
  --reconnect-seconds <n> Reconnect delay after disconnect (default: 5)
@@ -2734,17 +3673,98 @@ Options:
2734
3673
  --thread-id <id> Resume specific thread
2735
3674
  --ephemeral Use ephemeral thread (no persistence)
2736
3675
  --process-existing-messages Process all existing inbox messages
3676
+ --no-server Skip app-server auto-start and connect only
3677
+ --no-auth Skip auth gateway (app-server listens directly, localhost only)
3678
+
3679
+ Port Assignment:
3680
+ Ports are auto-assigned from 4501 on first bridge start if not set via --port
3681
+ during 'tap add'. Auto-assigned ports are saved to state for future starts.
2737
3682
 
2738
3683
  Examples:
2739
3684
  npx @hua-labs/tap bridge start codex --agent-name myAgent
3685
+ npx @hua-labs/tap bridge start --all
3686
+ npx @hua-labs/tap bridge start codex --agent-name myAgent --no-server
2740
3687
  npx @hua-labs/tap bridge start codex-reviewer --agent-name reviewer --busy-mode steer
2741
3688
  npx @hua-labs/tap bridge stop codex
2742
3689
  npx @hua-labs/tap bridge stop
2743
3690
  npx @hua-labs/tap bridge status
2744
3691
  `.trim();
3692
+ function formatAppServerState(appServer) {
3693
+ const ownership = appServer.managed ? "managed" : "external";
3694
+ const pid = appServer.pid != null ? ` pid:${appServer.pid}` : "";
3695
+ const health = appServer.healthy ? "healthy" : "unhealthy";
3696
+ const auth = appServer.auth != null ? `, auth gateway:${appServer.auth.gatewayPid ?? "-"} -> ${appServer.auth.upstreamUrl}` : "";
3697
+ return `${health}, ${ownership}${pid}, ${appServer.url}${auth}`;
3698
+ }
3699
+ function redactProtectedUrl(url) {
3700
+ try {
3701
+ const parsed = new URL(url);
3702
+ if (parsed.searchParams.has("tap_token")) {
3703
+ parsed.searchParams.set("tap_token", "***");
3704
+ }
3705
+ return parsed.toString().replace(/\/$/, "");
3706
+ } catch {
3707
+ return url.replace(/tap_token=[^&]+/g, "tap_token=***");
3708
+ }
3709
+ }
3710
+ function loadCurrentBridgeState(stateDir, instanceId, fallback) {
3711
+ return loadBridgeState(stateDir, instanceId) ?? fallback ?? null;
3712
+ }
3713
+ function getSharedAppServerUsers(state, stateDir, currentInstanceId, appServerUrl) {
3714
+ const shared = [];
3715
+ for (const [id, inst] of Object.entries(state.instances)) {
3716
+ if (id === currentInstanceId || !inst?.installed) {
3717
+ continue;
3718
+ }
3719
+ const instanceId = id;
3720
+ if (getBridgeStatus(stateDir, instanceId) !== "running") {
3721
+ continue;
3722
+ }
3723
+ const bridgeState = loadCurrentBridgeState(
3724
+ stateDir,
3725
+ instanceId,
3726
+ inst.bridge
3727
+ );
3728
+ if (bridgeState?.appServer?.url === appServerUrl) {
3729
+ shared.push(instanceId);
3730
+ }
3731
+ }
3732
+ return shared;
3733
+ }
3734
+ function transferManagedAppServerOwnership(state, stateDir, recipientId, appServer) {
3735
+ const recipient = state.instances[recipientId];
3736
+ if (!recipient) {
3737
+ return false;
3738
+ }
3739
+ const bridgeState = loadCurrentBridgeState(
3740
+ stateDir,
3741
+ recipientId,
3742
+ recipient.bridge
3743
+ );
3744
+ if (!bridgeState) {
3745
+ return false;
3746
+ }
3747
+ const transferredAppServer = {
3748
+ ...appServer,
3749
+ managed: true,
3750
+ healthy: true,
3751
+ lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
3752
+ lastHealthyAt: appServer.lastHealthyAt ?? (/* @__PURE__ */ new Date()).toISOString()
3753
+ };
3754
+ const updatedBridge = {
3755
+ ...bridgeState,
3756
+ appServer: transferredAppServer
3757
+ };
3758
+ saveBridgeState(stateDir, recipientId, updatedBridge);
3759
+ state.instances[recipientId] = {
3760
+ ...recipient,
3761
+ bridge: updatedBridge
3762
+ };
3763
+ return true;
3764
+ }
2745
3765
  async function bridgeStart(identifier, agentName, flags = {}) {
2746
- const repoRoot = findRepoRoot2();
2747
- const state = loadState(repoRoot);
3766
+ const repoRoot = findRepoRoot();
3767
+ let state = loadState(repoRoot);
2748
3768
  if (!state) {
2749
3769
  return {
2750
3770
  ok: false,
@@ -2767,7 +3787,7 @@ async function bridgeStart(identifier, agentName, flags = {}) {
2767
3787
  };
2768
3788
  }
2769
3789
  const instanceId = resolved.instanceId;
2770
- const instance = state.instances[instanceId];
3790
+ let instance = state.instances[instanceId];
2771
3791
  if (!instance?.installed) {
2772
3792
  return {
2773
3793
  ok: false,
@@ -2794,6 +3814,13 @@ async function bridgeStart(identifier, agentName, flags = {}) {
2794
3814
  data: { bridgeMode: mode }
2795
3815
  };
2796
3816
  }
3817
+ const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
3818
+ if (agentName && agentName !== instance.agentName) {
3819
+ instance = { ...instance, agentName };
3820
+ const updatedState = updateInstanceState(state, instanceId, instance);
3821
+ saveState(repoRoot, updatedState);
3822
+ state = updatedState;
3823
+ }
2797
3824
  const ctx = createAdapterContext(state.commsDir, repoRoot);
2798
3825
  const bridgeScript = adapter.resolveBridgeScript?.(ctx);
2799
3826
  if (!bridgeScript) {
@@ -2810,19 +3837,63 @@ async function bridgeStart(identifier, agentName, flags = {}) {
2810
3837
  }
2811
3838
  const { config: resolvedConfig } = resolveConfig({}, repoRoot);
2812
3839
  const runtimeCommand = resolvedConfig.runtimeCommand;
2813
- const appServerUrl = resolvedConfig.appServerUrl;
3840
+ const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
3841
+ let effectivePort = instance.port;
3842
+ if (effectivePort == null && manageAppServer) {
3843
+ effectivePort = await findNextAvailableAppServerPort(
3844
+ state,
3845
+ resolvedConfig.appServerUrl,
3846
+ 4501,
3847
+ instanceId
3848
+ );
3849
+ instance = { ...instance, port: effectivePort };
3850
+ const updatedState = updateInstanceState(state, instanceId, instance);
3851
+ saveState(repoRoot, updatedState);
3852
+ state = updatedState;
3853
+ }
3854
+ const appServerUrl = resolveAppServerUrl(
3855
+ resolvedConfig.appServerUrl,
3856
+ effectivePort ?? void 0
3857
+ );
2814
3858
  logHeader(`@hua-labs/tap bridge start ${instanceId}`);
2815
3859
  log(`Bridge script: ${bridgeScript}`);
2816
3860
  log(`Bridge mode: ${mode}`);
2817
3861
  log(`Runtime cmd: ${runtimeCommand}`);
2818
3862
  log(`App server: ${appServerUrl}`);
2819
- if (instance.port) log(`Port: ${instance.port}`);
3863
+ if (effectivePort != null) log(`Port: ${effectivePort}`);
3864
+ if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
3865
+ const noAuth = flags["no-auth"] === true;
3866
+ if (!manageAppServer && instance.runtime === "codex") {
3867
+ log("Auto server: disabled (--no-server)");
3868
+ }
3869
+ if (noAuth && manageAppServer) {
3870
+ log("Auth gateway: disabled (--no-auth)");
3871
+ }
2820
3872
  const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
2821
3873
  if (willBeHeadless) {
2822
3874
  const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance.headless?.role ?? "reviewer";
2823
3875
  log(`Headless: ${role}`);
2824
3876
  }
2825
3877
  try {
3878
+ if (!manageAppServer && instance.runtime === "codex") {
3879
+ log("Checking app-server health...");
3880
+ const healthy = await checkAppServerHealth(appServerUrl);
3881
+ if (healthy) {
3882
+ logSuccess("App server reachable");
3883
+ } else {
3884
+ logError(`App server not reachable at ${appServerUrl}`);
3885
+ return {
3886
+ ok: false,
3887
+ command: "bridge",
3888
+ instanceId,
3889
+ runtime: instance.runtime,
3890
+ code: "TAP_BRIDGE_START_FAILED",
3891
+ message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
3892
+ warnings: [],
3893
+ data: {}
3894
+ };
3895
+ }
3896
+ }
2826
3897
  const busyModeRaw = flags["busy-mode"];
2827
3898
  if (busyModeRaw !== void 0 && busyModeRaw !== "steer" && busyModeRaw !== "wait") {
2828
3899
  return {
@@ -2871,11 +3942,13 @@ async function bridgeStart(identifier, agentName, flags = {}) {
2871
3942
  commsDir: ctx.commsDir,
2872
3943
  bridgeScript,
2873
3944
  platform: ctx.platform,
2874
- agentName,
3945
+ agentName: resolvedAgentName,
2875
3946
  runtimeCommand,
2876
3947
  appServerUrl,
2877
3948
  repoRoot,
2878
- port: instance.port ?? void 0,
3949
+ port: effectivePort ?? void 0,
3950
+ manageAppServer,
3951
+ noAuth,
2879
3952
  headless,
2880
3953
  busyMode,
2881
3954
  pollSeconds,
@@ -2886,7 +3959,25 @@ async function bridgeStart(identifier, agentName, flags = {}) {
2886
3959
  processExistingMessages
2887
3960
  });
2888
3961
  logSuccess(`Bridge started (PID: ${bridge.pid})`);
2889
- log(`Log: ${path13.join(ctx.stateDir, "logs", `bridge-${instanceId}.log`)}`);
3962
+ log(`Log: ${path14.join(ctx.stateDir, "logs", `bridge-${instanceId}.log`)}`);
3963
+ if (bridge.appServer) {
3964
+ log(`App server: ${formatAppServerState(bridge.appServer)}`);
3965
+ if (bridge.appServer.logPath) {
3966
+ log(`Server log: ${bridge.appServer.logPath}`);
3967
+ }
3968
+ if (bridge.appServer.auth) {
3969
+ log(
3970
+ `Protected: ${redactProtectedUrl(bridge.appServer.auth.protectedUrl)}`
3971
+ );
3972
+ if (bridge.appServer.auth.gatewayLogPath) {
3973
+ log(`Gateway log: ${bridge.appServer.auth.gatewayLogPath}`);
3974
+ }
3975
+ log(`TUI connect: ${bridge.appServer.auth.upstreamUrl}`);
3976
+ }
3977
+ if (bridge.appServer.managed && !bridge.appServer.auth) {
3978
+ log(`TUI connect: ${bridge.appServer.url}`);
3979
+ }
3980
+ }
2890
3981
  const updated = { ...instance, bridge };
2891
3982
  const newState = updateInstanceState(state, instanceId, updated);
2892
3983
  saveState(repoRoot, newState);
@@ -2898,25 +3989,93 @@ async function bridgeStart(identifier, agentName, flags = {}) {
2898
3989
  code: "TAP_BRIDGE_START_OK",
2899
3990
  message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
2900
3991
  warnings: [],
2901
- data: { pid: bridge.pid }
3992
+ data: { pid: bridge.pid, appServer: bridge.appServer ?? null }
2902
3993
  };
2903
3994
  } catch (err) {
2904
3995
  const msg = err instanceof Error ? err.message : String(err);
2905
3996
  logError(msg);
2906
3997
  return {
2907
- ok: false,
3998
+ ok: false,
3999
+ command: "bridge",
4000
+ instanceId,
4001
+ runtime: instance.runtime,
4002
+ code: "TAP_BRIDGE_START_FAILED",
4003
+ message: msg,
4004
+ warnings: [],
4005
+ data: {}
4006
+ };
4007
+ }
4008
+ }
4009
+ async function bridgeStartAll(flags = {}) {
4010
+ const repoRoot = findRepoRoot();
4011
+ const state = loadState(repoRoot);
4012
+ if (!state) {
4013
+ return {
4014
+ ok: false,
4015
+ command: "bridge",
4016
+ code: "TAP_NOT_INITIALIZED",
4017
+ message: "Not initialized. Run: npx @hua-labs/tap init",
4018
+ warnings: [],
4019
+ data: {}
4020
+ };
4021
+ }
4022
+ const instanceIds = Object.keys(state.instances);
4023
+ const appServerInstances = instanceIds.filter((id) => {
4024
+ const inst = state.instances[id];
4025
+ if (!inst?.installed) return false;
4026
+ const adapter = getAdapter(inst.runtime);
4027
+ return adapter.bridgeMode() === "app-server";
4028
+ });
4029
+ if (appServerInstances.length === 0) {
4030
+ return {
4031
+ ok: true,
2908
4032
  command: "bridge",
2909
- instanceId,
2910
- runtime: instance.runtime,
2911
- code: "TAP_BRIDGE_START_FAILED",
2912
- message: msg,
4033
+ code: "TAP_NO_OP",
4034
+ message: "No app-server instances found to start.",
2913
4035
  warnings: [],
2914
4036
  data: {}
2915
4037
  };
2916
4038
  }
4039
+ logHeader("@hua-labs/tap bridge start --all");
4040
+ log(
4041
+ `Found ${appServerInstances.length} app-server instance(s): ${appServerInstances.join(", ")}`
4042
+ );
4043
+ log("");
4044
+ const started = [];
4045
+ const failed = [];
4046
+ const warnings = [];
4047
+ for (const instanceId of appServerInstances) {
4048
+ const inst = state.instances[instanceId];
4049
+ const storedName = inst?.agentName ?? void 0;
4050
+ if (!storedName) {
4051
+ const msg = `${instanceId}: skipped \u2014 no stored agent-name. Set it first: tap bridge start ${instanceId} --agent-name <name>`;
4052
+ log(msg);
4053
+ warnings.push(msg);
4054
+ continue;
4055
+ }
4056
+ log(`Starting ${instanceId} (agent: ${storedName})...`);
4057
+ const result = await bridgeStart(instanceId, storedName, flags);
4058
+ if (result.ok) {
4059
+ started.push(instanceId);
4060
+ logSuccess(`${instanceId} started`);
4061
+ } else {
4062
+ failed.push(instanceId);
4063
+ logError(`${instanceId}: ${result.message}`);
4064
+ }
4065
+ log("");
4066
+ }
4067
+ const message = started.length > 0 ? `Started ${started.length}/${appServerInstances.length} bridge(s): ${started.join(", ")}` + (failed.length > 0 ? `. Failed: ${failed.join(", ")}` : "") : `No bridges started. Failed: ${failed.join(", ")}`;
4068
+ return {
4069
+ ok: failed.length === 0 && started.length > 0,
4070
+ command: "bridge",
4071
+ code: started.length > 0 ? "TAP_BRIDGE_START_OK" : "TAP_BRIDGE_START_FAILED",
4072
+ message,
4073
+ warnings,
4074
+ data: { started, failed }
4075
+ };
2917
4076
  }
2918
4077
  async function bridgeStopOne(identifier) {
2919
- const repoRoot = findRepoRoot2();
4078
+ const repoRoot = findRepoRoot();
2920
4079
  const state = loadState(repoRoot);
2921
4080
  if (!state) {
2922
4081
  return {
@@ -2941,20 +4100,64 @@ async function bridgeStopOne(identifier) {
2941
4100
  }
2942
4101
  const instanceId = resolved.instanceId;
2943
4102
  const ctx = createAdapterContext(state.commsDir, repoRoot);
4103
+ const instance = state.instances[instanceId];
4104
+ const bridgeState = loadCurrentBridgeState(
4105
+ ctx.stateDir,
4106
+ instanceId,
4107
+ instance?.bridge
4108
+ );
4109
+ const appServer = bridgeState?.appServer ?? null;
2944
4110
  logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
2945
4111
  const stopped = await stopBridge({
2946
4112
  instanceId,
2947
4113
  stateDir: ctx.stateDir,
2948
4114
  platform: ctx.platform
2949
4115
  });
4116
+ let appServerStopped = false;
4117
+ let appServerTransferredTo = null;
2950
4118
  if (stopped) {
2951
4119
  logSuccess(`Bridge for ${instanceId} stopped`);
2952
- const instance2 = state.instances[instanceId];
2953
- if (instance2) {
2954
- const updated = { ...instance2, bridge: null };
2955
- const newState = updateInstanceState(state, instanceId, updated);
2956
- saveState(repoRoot, newState);
4120
+ } else {
4121
+ log(`No running bridge for ${instanceId}`);
4122
+ }
4123
+ if (appServer?.managed) {
4124
+ const sharedUsers = getSharedAppServerUsers(
4125
+ state,
4126
+ ctx.stateDir,
4127
+ instanceId,
4128
+ appServer.url
4129
+ );
4130
+ if (sharedUsers.length > 0) {
4131
+ const recipient = sharedUsers[0];
4132
+ if (transferManagedAppServerOwnership(
4133
+ state,
4134
+ ctx.stateDir,
4135
+ recipient,
4136
+ appServer
4137
+ )) {
4138
+ appServerTransferredTo = recipient;
4139
+ log(`Managed app-server ownership moved to ${recipient}`);
4140
+ } else {
4141
+ log(
4142
+ `Managed app-server left running at ${appServer.url} because ownership transfer failed`
4143
+ );
4144
+ }
4145
+ } else {
4146
+ appServerStopped = await stopManagedAppServer(appServer, ctx.platform);
4147
+ if (appServerStopped) {
4148
+ const gatewayNote = appServer.auth?.gatewayPid != null ? `, gateway PID: ${appServer.auth.gatewayPid}` : "";
4149
+ logSuccess(
4150
+ `Managed app-server stopped (PID: ${appServer.pid ?? "-"}${gatewayNote})`
4151
+ );
4152
+ }
2957
4153
  }
4154
+ }
4155
+ if (instance) {
4156
+ const updated = { ...instance, bridge: null };
4157
+ const newState = updateInstanceState(state, instanceId, updated);
4158
+ saveState(repoRoot, newState);
4159
+ }
4160
+ if (stopped) {
2958
4161
  return {
2959
4162
  ok: true,
2960
4163
  command: "bridge",
@@ -2962,16 +4165,12 @@ async function bridgeStopOne(identifier) {
2962
4165
  code: "TAP_BRIDGE_STOP_OK",
2963
4166
  message: `Bridge for ${instanceId} stopped`,
2964
4167
  warnings: [],
2965
- data: {}
4168
+ data: {
4169
+ appServerStopped,
4170
+ appServerTransferredTo
4171
+ }
2966
4172
  };
2967
4173
  }
2968
- log(`No running bridge for ${instanceId}`);
2969
- const instance = state.instances[instanceId];
2970
- if (instance?.bridge) {
2971
- const updated = { ...instance, bridge: null };
2972
- const newState = updateInstanceState(state, instanceId, updated);
2973
- saveState(repoRoot, newState);
2974
- }
2975
4174
  return {
2976
4175
  ok: true,
2977
4176
  command: "bridge",
@@ -2979,11 +4178,14 @@ async function bridgeStopOne(identifier) {
2979
4178
  code: "TAP_BRIDGE_NOT_RUNNING",
2980
4179
  message: `No running bridge for ${instanceId}`,
2981
4180
  warnings: [],
2982
- data: {}
4181
+ data: {
4182
+ appServerStopped,
4183
+ appServerTransferredTo
4184
+ }
2983
4185
  };
2984
4186
  }
2985
4187
  async function bridgeStopAll() {
2986
- const repoRoot = findRepoRoot2();
4188
+ const repoRoot = findRepoRoot();
2987
4189
  const state = loadState(repoRoot);
2988
4190
  if (!state) {
2989
4191
  return {
@@ -2998,9 +4200,22 @@ async function bridgeStopAll() {
2998
4200
  const ctx = createAdapterContext(state.commsDir, repoRoot);
2999
4201
  const instanceIds = Object.keys(state.instances);
3000
4202
  const stopped = [];
4203
+ const managedAppServers = /* @__PURE__ */ new Map();
3001
4204
  logHeader("@hua-labs/tap bridge stop (all)");
3002
4205
  let stateChanged = false;
3003
4206
  for (const instanceId of instanceIds) {
4207
+ const bridgeState = loadCurrentBridgeState(
4208
+ ctx.stateDir,
4209
+ instanceId,
4210
+ state.instances[instanceId]?.bridge
4211
+ );
4212
+ const appServer = bridgeState?.appServer;
4213
+ if (appServer?.managed && appServer.pid != null) {
4214
+ managedAppServers.set(
4215
+ `${appServer.url}:${appServer.pid}:${appServer.auth?.gatewayPid ?? "-"}`,
4216
+ appServer
4217
+ );
4218
+ }
3004
4219
  const didStop = await stopBridge({
3005
4220
  instanceId,
3006
4221
  stateDir: ctx.stateDir,
@@ -3016,6 +4231,16 @@ async function bridgeStopAll() {
3016
4231
  stateChanged = true;
3017
4232
  }
3018
4233
  }
4234
+ const stoppedAppServers = [];
4235
+ for (const appServer of managedAppServers.values()) {
4236
+ if (await stopManagedAppServer(appServer, ctx.platform)) {
4237
+ stoppedAppServers.push(appServer.pid);
4238
+ const gatewayNote = appServer.auth?.gatewayPid != null ? `, gateway PID ${appServer.auth.gatewayPid}` : "";
4239
+ logSuccess(
4240
+ `Stopped app-server PID ${appServer.pid} (${appServer.url}${gatewayNote})`
4241
+ );
4242
+ }
4243
+ }
3019
4244
  if (stateChanged) {
3020
4245
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3021
4246
  saveState(repoRoot, state);
@@ -3028,11 +4253,11 @@ async function bridgeStopAll() {
3028
4253
  code: stopped.length > 0 ? "TAP_BRIDGE_STOP_OK" : "TAP_BRIDGE_NOT_RUNNING",
3029
4254
  message,
3030
4255
  warnings: [],
3031
- data: { stopped }
4256
+ data: { stopped, stoppedAppServers }
3032
4257
  };
3033
4258
  }
3034
4259
  function bridgeStatusAll() {
3035
- const repoRoot = findRepoRoot2();
4260
+ const repoRoot = findRepoRoot();
3036
4261
  const state = loadState(repoRoot);
3037
4262
  if (!state) {
3038
4263
  return {
@@ -3067,7 +4292,8 @@ function bridgeStatusAll() {
3067
4292
  runtime: inst.runtime,
3068
4293
  pid: null,
3069
4294
  port: inst.port,
3070
- lastHeartbeat: null
4295
+ lastHeartbeat: null,
4296
+ appServer: null
3071
4297
  };
3072
4298
  continue;
3073
4299
  }
@@ -3075,7 +4301,7 @@ function bridgeStatusAll() {
3075
4301
  const bridgeState = loadBridgeState(stateDir, instanceId);
3076
4302
  const age = getHeartbeatAge(stateDir, instanceId);
3077
4303
  const pid = bridgeState?.pid ?? null;
3078
- const heartbeat = bridgeState?.lastHeartbeat ?? null;
4304
+ const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
3079
4305
  const pidStr = pid ? String(pid) : "-";
3080
4306
  const portStr = inst.port ? String(inst.port) : "-";
3081
4307
  const ageStr = age !== null ? formatAge(age) : "-";
@@ -3083,12 +4309,24 @@ function bridgeStatusAll() {
3083
4309
  log(
3084
4310
  `${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${statusColor.padEnd(10)} ${pidStr.padEnd(8)} ${portStr.padEnd(6)} ${ageStr}`
3085
4311
  );
4312
+ if (bridgeState?.appServer) {
4313
+ log(` App server: ${formatAppServerState(bridgeState.appServer)}`);
4314
+ if (bridgeState.appServer.logPath) {
4315
+ log(` Server log: ${bridgeState.appServer.logPath}`);
4316
+ }
4317
+ if (bridgeState.appServer.auth) {
4318
+ log(
4319
+ ` Protected: ${redactProtectedUrl(bridgeState.appServer.auth.protectedUrl)}`
4320
+ );
4321
+ }
4322
+ }
3086
4323
  bridges[instanceId] = {
3087
4324
  status,
3088
4325
  runtime: inst.runtime,
3089
4326
  pid,
3090
4327
  port: inst.port,
3091
- lastHeartbeat: heartbeat
4328
+ lastHeartbeat: heartbeat,
4329
+ appServer: bridgeState?.appServer ?? null
3092
4330
  };
3093
4331
  }
3094
4332
  if (instanceIds.length === 0) {
@@ -3105,7 +4343,7 @@ function bridgeStatusAll() {
3105
4343
  };
3106
4344
  }
3107
4345
  function bridgeStatusOne(identifier) {
3108
- const repoRoot = findRepoRoot2();
4346
+ const repoRoot = findRepoRoot();
3109
4347
  const state = loadState(repoRoot);
3110
4348
  if (!state) {
3111
4349
  return {
@@ -3162,7 +4400,8 @@ function bridgeStatusOne(identifier) {
3162
4400
  bridgeMode: inst.bridgeMode,
3163
4401
  pid: null,
3164
4402
  port: inst.port,
3165
- lastHeartbeat: null
4403
+ lastHeartbeat: null,
4404
+ appServer: null
3166
4405
  }
3167
4406
  };
3168
4407
  }
@@ -3171,15 +4410,45 @@ function bridgeStatusOne(identifier) {
3171
4410
  const status = getBridgeStatus(stateDir, instanceId);
3172
4411
  const bridgeState = loadBridgeState(stateDir, instanceId);
3173
4412
  const age = getHeartbeatAge(stateDir, instanceId);
4413
+ const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
3174
4414
  log(`Status: ${status}`);
3175
4415
  if (bridgeState) {
3176
4416
  log(`PID: ${bridgeState.pid}`);
3177
4417
  log(
3178
- `Heartbeat: ${bridgeState.lastHeartbeat}${age !== null ? ` (${formatAge(age)})` : ""}`
4418
+ `Heartbeat: ${heartbeat ?? "-"}${age !== null ? ` (${formatAge(age)})` : ""}`
3179
4419
  );
3180
4420
  log(
3181
- `Log: ${path13.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
4421
+ `Log: ${path14.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
3182
4422
  );
4423
+ if (bridgeState.appServer) {
4424
+ log(`App server: ${bridgeState.appServer.url}`);
4425
+ log(`Server PID: ${bridgeState.appServer.pid ?? "-"}`);
4426
+ log(
4427
+ `Server mode: ${bridgeState.appServer.managed ? "managed" : "external"}`
4428
+ );
4429
+ log(
4430
+ `Health: ${bridgeState.appServer.healthy ? "healthy" : "unhealthy"}`
4431
+ );
4432
+ log(`Checked: ${bridgeState.appServer.lastCheckedAt}`);
4433
+ if (bridgeState.appServer.logPath) {
4434
+ log(`Server log: ${bridgeState.appServer.logPath}`);
4435
+ }
4436
+ if (bridgeState.appServer.auth) {
4437
+ log(`Auth: ${bridgeState.appServer.auth.mode}`);
4438
+ log(
4439
+ `Protected: ${redactProtectedUrl(bridgeState.appServer.auth.protectedUrl)}`
4440
+ );
4441
+ log(`Upstream: ${bridgeState.appServer.auth.upstreamUrl}`);
4442
+ log(`TUI connect: ${bridgeState.appServer.auth.upstreamUrl}`);
4443
+ log(`Gateway PID: ${bridgeState.appServer.auth.gatewayPid ?? "-"}`);
4444
+ if (bridgeState.appServer.auth.gatewayLogPath) {
4445
+ log(`Gateway log: ${bridgeState.appServer.auth.gatewayLogPath}`);
4446
+ }
4447
+ } else if (bridgeState.appServer.managed) {
4448
+ log(`Auth: none (--no-auth)`);
4449
+ log(`TUI connect: ${bridgeState.appServer.url}`);
4450
+ }
4451
+ }
3183
4452
  }
3184
4453
  log("");
3185
4454
  return {
@@ -3195,7 +4464,8 @@ function bridgeStatusOne(identifier) {
3195
4464
  bridgeMode: inst.bridgeMode,
3196
4465
  pid: bridgeState?.pid ?? null,
3197
4466
  port: inst.port,
3198
- lastHeartbeat: bridgeState?.lastHeartbeat ?? null
4467
+ lastHeartbeat: heartbeat,
4468
+ appServer: bridgeState?.appServer ?? null
3199
4469
  }
3200
4470
  };
3201
4471
  }
@@ -3217,12 +4487,29 @@ async function bridgeCommand(args) {
3217
4487
  }
3218
4488
  switch (subcommand) {
3219
4489
  case "start": {
4490
+ const wantsAll = flags["all"] === true || identifierArg === "--all";
4491
+ const hasInstance = identifierArg && identifierArg !== "--all";
4492
+ if (wantsAll && hasInstance) {
4493
+ return {
4494
+ ok: false,
4495
+ command: "bridge",
4496
+ code: "TAP_INVALID_ARGUMENT",
4497
+ message: `Cannot combine <instance> with --all. Use either:
4498
+ tap bridge start ${identifierArg}
4499
+ tap bridge start --all`,
4500
+ warnings: [],
4501
+ data: {}
4502
+ };
4503
+ }
4504
+ if (wantsAll) {
4505
+ return bridgeStartAll(flags);
4506
+ }
3220
4507
  if (!identifierArg) {
3221
4508
  return {
3222
4509
  ok: false,
3223
4510
  command: "bridge",
3224
4511
  code: "TAP_INVALID_ARGUMENT",
3225
- message: "Missing instance. Usage: npx @hua-labs/tap bridge start <instance>",
4512
+ message: "Missing instance. Usage: npx @hua-labs/tap bridge start <instance> or --all",
3226
4513
  warnings: [],
3227
4514
  data: {}
3228
4515
  };
@@ -3253,41 +4540,268 @@ async function bridgeCommand(args) {
3253
4540
  }
3254
4541
  }
3255
4542
 
4543
+ // src/engine/dashboard.ts
4544
+ import * as fs15 from "fs";
4545
+ import * as path15 from "path";
4546
+ import { execSync as execSync4 } from "child_process";
4547
+ function collectAgents(commsDir) {
4548
+ const heartbeatsPath = path15.join(commsDir, "heartbeats.json");
4549
+ if (!fs15.existsSync(heartbeatsPath)) return [];
4550
+ try {
4551
+ const raw = fs15.readFileSync(heartbeatsPath, "utf-8");
4552
+ const data = JSON.parse(raw);
4553
+ return Object.entries(data).map(([name, info]) => ({
4554
+ name: info.agent ?? name,
4555
+ status: info.status ?? null,
4556
+ lastActivity: info.lastActivity ?? info.timestamp ?? null,
4557
+ joinedAt: info.joinedAt ?? null
4558
+ }));
4559
+ } catch {
4560
+ return [];
4561
+ }
4562
+ }
4563
+ function collectBridges(repoRoot) {
4564
+ const state = loadState(repoRoot);
4565
+ const { config } = resolveConfig({}, repoRoot);
4566
+ const stateDir = config.stateDir;
4567
+ const bridges = [];
4568
+ if (state) {
4569
+ for (const [id, inst] of Object.entries(state.instances)) {
4570
+ if (!inst?.installed) continue;
4571
+ if (inst.bridgeMode !== "app-server") continue;
4572
+ const instanceId = id;
4573
+ const status = getBridgeStatus(stateDir, instanceId);
4574
+ const bridgeState = loadBridgeState(stateDir, instanceId);
4575
+ const age = getHeartbeatAge(stateDir, instanceId);
4576
+ bridges.push({
4577
+ instanceId: id,
4578
+ runtime: inst.runtime,
4579
+ status,
4580
+ pid: bridgeState?.pid ?? null,
4581
+ port: inst.port ?? null,
4582
+ heartbeatAge: age,
4583
+ headless: inst.headless?.enabled ?? false
4584
+ });
4585
+ }
4586
+ }
4587
+ const tmpDir = path15.join(repoRoot, ".tmp");
4588
+ if (fs15.existsSync(tmpDir)) {
4589
+ try {
4590
+ const dirs = fs15.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
4591
+ for (const dir of dirs) {
4592
+ const daemonPath = path15.join(tmpDir, dir, "bridge-daemon.json");
4593
+ if (!fs15.existsSync(daemonPath)) continue;
4594
+ try {
4595
+ const raw = fs15.readFileSync(daemonPath, "utf-8");
4596
+ const daemon = JSON.parse(raw);
4597
+ const alreadyCovered = bridges.some(
4598
+ (b) => b.pid === daemon.pid && b.pid !== null
4599
+ );
4600
+ if (alreadyCovered) continue;
4601
+ const agentFile = path15.join(tmpDir, dir, "agent-name.txt");
4602
+ const agentName = fs15.existsSync(agentFile) ? fs15.readFileSync(agentFile, "utf-8").trim() : dir;
4603
+ const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
4604
+ const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
4605
+ const port = portMatch ? parseInt(portMatch[1], 10) : null;
4606
+ bridges.push({
4607
+ instanceId: agentName,
4608
+ runtime: "codex",
4609
+ status: running ? "running" : "stale",
4610
+ pid: daemon.pid ?? null,
4611
+ port,
4612
+ heartbeatAge: null,
4613
+ headless: false
4614
+ });
4615
+ } catch {
4616
+ }
4617
+ }
4618
+ } catch {
4619
+ }
4620
+ }
4621
+ return bridges;
4622
+ }
4623
+ function collectPRs() {
4624
+ try {
4625
+ const output = execSync4(
4626
+ "gh pr list --state all --limit 10 --json number,title,author,state,url",
4627
+ { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
4628
+ );
4629
+ const prs = JSON.parse(output);
4630
+ return prs.map((pr) => ({
4631
+ number: pr.number,
4632
+ title: pr.title,
4633
+ author: pr.author.login,
4634
+ state: pr.state,
4635
+ url: pr.url
4636
+ }));
4637
+ } catch {
4638
+ return [];
4639
+ }
4640
+ }
4641
+ function collectWarnings(bridges, agents) {
4642
+ const warnings = [];
4643
+ for (const bridge of bridges) {
4644
+ if (bridge.status === "stale") {
4645
+ warnings.push({
4646
+ level: "warn",
4647
+ message: `Bridge ${bridge.instanceId} is stale (PID ${bridge.pid} dead)`
4648
+ });
4649
+ }
4650
+ if (bridge.status === "running" && bridge.heartbeatAge !== null && bridge.heartbeatAge > 60) {
4651
+ warnings.push({
4652
+ level: "warn",
4653
+ message: `Bridge ${bridge.instanceId} heartbeat stale (${bridge.heartbeatAge}s ago)`
4654
+ });
4655
+ }
4656
+ }
4657
+ if (bridges.length === 0) {
4658
+ warnings.push({
4659
+ level: "warn",
4660
+ message: "No bridges configured"
4661
+ });
4662
+ }
4663
+ if (agents.length === 0) {
4664
+ warnings.push({
4665
+ level: "warn",
4666
+ message: "No agent heartbeats found"
4667
+ });
4668
+ }
4669
+ return warnings;
4670
+ }
4671
+ function collectDashboardSnapshot(repoRoot, commsDirOverride) {
4672
+ const { config } = resolveConfig(
4673
+ commsDirOverride ? { commsDir: commsDirOverride } : {},
4674
+ repoRoot
4675
+ );
4676
+ const resolved = config;
4677
+ const agents = collectAgents(resolved.commsDir);
4678
+ const bridges = collectBridges(resolved.repoRoot);
4679
+ const prs = collectPRs();
4680
+ const warnings = collectWarnings(bridges, agents);
4681
+ return {
4682
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4683
+ repoRoot: resolved.repoRoot,
4684
+ commsDir: resolved.commsDir,
4685
+ agents,
4686
+ bridges,
4687
+ prs,
4688
+ warnings
4689
+ };
4690
+ }
4691
+
4692
+ // src/commands/up.ts
4693
+ var UP_HELP = `
4694
+ Usage:
4695
+ tap-comms up [bridge-start options]
4696
+
4697
+ Description:
4698
+ Start all registered app-server bridge daemons with one command.
4699
+ This is the orchestration entrypoint for headless/background TAP operation.
4700
+
4701
+ Examples:
4702
+ npx @hua-labs/tap up
4703
+ npx @hua-labs/tap up --no-auth
4704
+ npx @hua-labs/tap up --busy-mode wait
4705
+ `.trim();
4706
+ async function upCommand(args) {
4707
+ if (args.includes("--help") || args.includes("-h")) {
4708
+ log(UP_HELP);
4709
+ return {
4710
+ ok: true,
4711
+ command: "up",
4712
+ code: "TAP_NO_OP",
4713
+ message: UP_HELP,
4714
+ warnings: [],
4715
+ data: {}
4716
+ };
4717
+ }
4718
+ const repoRoot = findRepoRoot();
4719
+ const result = await bridgeCommand(["start", "--all", ...args]);
4720
+ const snapshot = collectDashboardSnapshot(repoRoot);
4721
+ const activeBridges = snapshot.bridges.filter(
4722
+ (bridge) => bridge.status === "running"
4723
+ ).length;
4724
+ if (!result.ok) {
4725
+ return {
4726
+ ...result,
4727
+ command: "up",
4728
+ data: {
4729
+ ...result.data,
4730
+ snapshot
4731
+ }
4732
+ };
4733
+ }
4734
+ return {
4735
+ ok: true,
4736
+ command: "up",
4737
+ code: "TAP_UP_OK",
4738
+ message: `tap up: ${activeBridges} bridge(s) running`,
4739
+ warnings: result.warnings,
4740
+ data: {
4741
+ ...result.data,
4742
+ snapshot
4743
+ }
4744
+ };
4745
+ }
4746
+
4747
+ // src/commands/down.ts
4748
+ var DOWN_HELP = `
4749
+ Usage:
4750
+ tap-comms down
4751
+
4752
+ Description:
4753
+ Stop all running bridge daemons and managed app-servers.
4754
+
4755
+ Examples:
4756
+ npx @hua-labs/tap down
4757
+ `.trim();
4758
+ async function downCommand(args) {
4759
+ if (args.includes("--help") || args.includes("-h")) {
4760
+ log(DOWN_HELP);
4761
+ return {
4762
+ ok: true,
4763
+ command: "down",
4764
+ code: "TAP_NO_OP",
4765
+ message: DOWN_HELP,
4766
+ warnings: [],
4767
+ data: {}
4768
+ };
4769
+ }
4770
+ const repoRoot = findRepoRoot();
4771
+ const result = await bridgeCommand(["stop"]);
4772
+ const snapshot = collectDashboardSnapshot(repoRoot);
4773
+ if (!result.ok) {
4774
+ return {
4775
+ ...result,
4776
+ command: "down",
4777
+ data: {
4778
+ ...result.data,
4779
+ snapshot
4780
+ }
4781
+ };
4782
+ }
4783
+ return {
4784
+ ok: true,
4785
+ command: "down",
4786
+ code: "TAP_DOWN_OK",
4787
+ message: `tap down: ${snapshot.bridges.filter((bridge) => bridge.status === "running").length} bridge(s) still running`,
4788
+ warnings: result.warnings,
4789
+ data: {
4790
+ ...result.data,
4791
+ snapshot
4792
+ }
4793
+ };
4794
+ }
4795
+
3256
4796
  // src/commands/serve.ts
3257
- import * as fs14 from "fs";
3258
- import * as path14 from "path";
3259
- import { execSync as execSync4, spawn as spawn2 } from "child_process";
3260
- function findServerEntry(repoRoot) {
3261
- const candidates = [
3262
- path14.join(repoRoot, "packages", "tap-plugin", "channels", "tap-comms.ts"),
3263
- path14.join(
3264
- repoRoot,
3265
- "node_modules",
3266
- "@hua-labs",
3267
- "tap-plugin",
3268
- "channels",
3269
- "tap-comms.ts"
3270
- )
3271
- ];
3272
- for (const p of candidates) {
3273
- if (fs14.existsSync(p)) return p;
3274
- }
3275
- return null;
3276
- }
3277
- function isBunInstalled() {
3278
- try {
3279
- execSync4("bun --version", { stdio: "pipe" });
3280
- return true;
3281
- } catch {
3282
- return false;
3283
- }
3284
- }
4797
+ import * as path16 from "path";
4798
+ import { spawn as spawn2 } from "child_process";
3285
4799
  async function serveCommand(args) {
3286
- const repoRoot = findRepoRoot2();
4800
+ const repoRoot = findRepoRoot();
3287
4801
  let commsDir;
3288
4802
  const commsDirIdx = args.indexOf("--comms-dir");
3289
4803
  if (commsDirIdx !== -1 && args[commsDirIdx + 1]) {
3290
- commsDir = path14.resolve(args[commsDirIdx + 1]);
4804
+ commsDir = path16.resolve(args[commsDirIdx + 1]);
3291
4805
  }
3292
4806
  if (!commsDir && process.env.TAP_COMMS_DIR) {
3293
4807
  commsDir = process.env.TAP_COMMS_DIR;
@@ -3308,37 +4822,29 @@ async function serveCommand(args) {
3308
4822
  data: {}
3309
4823
  };
3310
4824
  }
3311
- if (!isBunInstalled()) {
3312
- return {
3313
- ok: false,
3314
- command: "serve",
3315
- code: "TAP_SERVE_BUN_REQUIRED",
3316
- message: "bun is required to run the tap-comms MCP server. Install: https://bun.sh",
3317
- warnings: [],
3318
- data: {}
3319
- };
3320
- }
3321
- const serverEntry = findServerEntry(repoRoot);
3322
- if (!serverEntry) {
4825
+ const ctx = createAdapterContext(commsDir, repoRoot);
4826
+ const managed = buildManagedMcpServerSpec(ctx);
4827
+ if (!managed.command || !managed.sourcePath) {
4828
+ const fallbackMessage = managed.issues[0] ?? "tap-comms MCP server not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/.";
3323
4829
  return {
3324
4830
  ok: false,
3325
4831
  command: "serve",
3326
- code: "TAP_SERVE_NO_SERVER",
3327
- message: "tap-comms MCP server not found. Run from a repo with packages/tap-plugin/channels/.",
4832
+ code: managed.sourcePath ? "TAP_SERVE_BUN_REQUIRED" : "TAP_SERVE_NO_SERVER",
4833
+ message: fallbackMessage,
3328
4834
  warnings: [],
3329
4835
  data: {}
3330
4836
  };
3331
4837
  }
3332
- const child = spawn2("bun", [serverEntry], {
4838
+ const child = spawn2(managed.command, managed.args, {
3333
4839
  stdio: "inherit",
3334
4840
  env: {
3335
4841
  ...process.env,
3336
4842
  TAP_COMMS_DIR: commsDir
3337
4843
  }
3338
4844
  });
3339
- return new Promise((resolve10) => {
4845
+ return new Promise((resolve11) => {
3340
4846
  child.on("error", (err) => {
3341
- resolve10({
4847
+ resolve11({
3342
4848
  ok: false,
3343
4849
  command: "serve",
3344
4850
  code: "TAP_INTERNAL_ERROR",
@@ -3348,7 +4854,7 @@ async function serveCommand(args) {
3348
4854
  });
3349
4855
  });
3350
4856
  child.on("exit", (code) => {
3351
- resolve10({
4857
+ resolve11({
3352
4858
  ok: code === 0,
3353
4859
  command: "serve",
3354
4860
  code: code === 0 ? "TAP_SERVE_OK" : "TAP_INTERNAL_ERROR",
@@ -3361,8 +4867,8 @@ async function serveCommand(args) {
3361
4867
  }
3362
4868
 
3363
4869
  // src/commands/init-worktree.ts
3364
- import * as fs15 from "fs";
3365
- import * as path15 from "path";
4870
+ import * as fs16 from "fs";
4871
+ import * as path17 from "path";
3366
4872
  import { execSync as execSync5 } from "child_process";
3367
4873
  var INIT_WORKTREE_HELP = `
3368
4874
  Usage:
@@ -3399,7 +4905,7 @@ function run(cmd, opts) {
3399
4905
  }
3400
4906
  }
3401
4907
  function toAbsolute(p) {
3402
- const resolved = path15.resolve(p);
4908
+ const resolved = path17.resolve(p);
3403
4909
  return resolved.replace(/\\/g, "/");
3404
4910
  }
3405
4911
  function probeBun(candidate) {
@@ -3430,18 +4936,18 @@ function findBun() {
3430
4936
  }
3431
4937
  }
3432
4938
  const home = process.env.HOME || process.env.USERPROFILE || "";
3433
- const bunHome = path15.join(
4939
+ const bunHome = path17.join(
3434
4940
  home,
3435
4941
  ".bun",
3436
4942
  "bin",
3437
4943
  process.platform === "win32" ? "bun.exe" : "bun"
3438
4944
  );
3439
- if (fs15.existsSync(bunHome) && probeBun(bunHome)) return bunHome;
4945
+ if (fs16.existsSync(bunHome) && probeBun(bunHome)) return bunHome;
3440
4946
  return null;
3441
4947
  }
3442
4948
  function step1CreateWorktree(opts) {
3443
4949
  log("Step 1/9: Creating worktree...");
3444
- if (fs15.existsSync(opts.worktreePath)) {
4950
+ if (fs16.existsSync(opts.worktreePath)) {
3445
4951
  logWarn(`Directory already exists: ${opts.worktreePath}`);
3446
4952
  try {
3447
4953
  run("git rev-parse --git-dir", { cwd: opts.worktreePath });
@@ -3503,22 +5009,22 @@ function step2MergeMain(opts, warnings) {
3503
5009
  }
3504
5010
  function step3CopyPermissions(opts, warnings) {
3505
5011
  log("Step 3/9: Copying permissions...");
3506
- const srcSettings = path15.join(
5012
+ const srcSettings = path17.join(
3507
5013
  opts.repoRoot,
3508
5014
  ".claude",
3509
5015
  "settings.local.json"
3510
5016
  );
3511
- const destDir = path15.join(opts.worktreePath, ".claude");
3512
- const destSettings = path15.join(destDir, "settings.local.json");
3513
- if (!fs15.existsSync(srcSettings)) {
5017
+ const destDir = path17.join(opts.worktreePath, ".claude");
5018
+ const destSettings = path17.join(destDir, "settings.local.json");
5019
+ if (!fs16.existsSync(srcSettings)) {
3514
5020
  warn(
3515
5021
  warnings,
3516
5022
  "No .claude/settings.local.json found in main repo. Skipping."
3517
5023
  );
3518
5024
  return;
3519
5025
  }
3520
- fs15.mkdirSync(destDir, { recursive: true });
3521
- fs15.copyFileSync(srcSettings, destSettings);
5026
+ fs16.mkdirSync(destDir, { recursive: true });
5027
+ fs16.copyFileSync(srcSettings, destSettings);
3522
5028
  logSuccess("Copied settings.local.json");
3523
5029
  try {
3524
5030
  run("git update-index --skip-worktree .claude/settings.local.json", {
@@ -3543,7 +5049,7 @@ function step4GenerateMcpJson(opts, warnings) {
3543
5049
  const wtAbs = toAbsolute(opts.worktreePath);
3544
5050
  const bunAbs = toAbsolute(bunPath);
3545
5051
  const commsAbs = toAbsolute(opts.commsDir);
3546
- const channelEntry = path15.join(
5052
+ const channelEntry = path17.join(
3547
5053
  wtAbs,
3548
5054
  "packages/tap-plugin/channels/tap-comms.ts"
3549
5055
  );
@@ -3560,8 +5066,8 @@ function step4GenerateMcpJson(opts, warnings) {
3560
5066
  }
3561
5067
  }
3562
5068
  };
3563
- const mcpPath = path15.join(opts.worktreePath, ".mcp.json");
3564
- fs15.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
5069
+ const mcpPath = path17.join(opts.worktreePath, ".mcp.json");
5070
+ fs16.writeFileSync(mcpPath, JSON.stringify(mcpConfig, null, 2) + "\n", "utf-8");
3565
5071
  logSuccess(`.mcp.json generated (absolute paths + cwd)`);
3566
5072
  log(` bun: ${bunAbs}`);
3567
5073
  log(` comms: ${commsAbs}`);
@@ -3599,275 +5105,126 @@ function step6BuildEslintPlugin(opts, warnings) {
3599
5105
  }
3600
5106
  function step7VerifyComms(opts, warnings) {
3601
5107
  log("Step 7/9: Verifying comms directory...");
3602
- if (!fs15.existsSync(opts.commsDir)) {
5108
+ if (!fs16.existsSync(opts.commsDir)) {
3603
5109
  warn(warnings, `Comms directory not found: ${opts.commsDir}`);
3604
- warn(warnings, "Create it or run: npx @hua-labs/tap init");
3605
- return;
3606
- }
3607
- const requiredDirs = ["inbox", "findings", "reviews", "letters"];
3608
- for (const dir of requiredDirs) {
3609
- const dirPath = path15.join(opts.commsDir, dir);
3610
- if (!fs15.existsSync(dirPath)) {
3611
- fs15.mkdirSync(dirPath, { recursive: true });
3612
- logSuccess(`Created ${dir}/`);
3613
- }
3614
- }
3615
- logSuccess(`Comms verified: ${opts.commsDir}`);
3616
- }
3617
- function step8VerifyBun(warnings) {
3618
- log("Step 8/9: Verifying bun...");
3619
- const bunPath = findBun();
3620
- if (!bunPath) {
3621
- warn(warnings, "bun not found in PATH.");
3622
- warn(warnings, "Install: curl -fsSL https://bun.sh/install | bash");
3623
- return;
3624
- }
3625
- try {
3626
- const version2 = run(`"${bunPath}" --version`);
3627
- logSuccess(`bun ${version2} found: ${bunPath}`);
3628
- } catch {
3629
- warn(warnings, "bun found but version check failed.");
3630
- }
3631
- }
3632
- function step9Ready(opts) {
3633
- logHeader("Ready!");
3634
- log(`Worktree: ${toAbsolute(opts.worktreePath)}`);
3635
- log(`Branch: ${opts.branch}`);
3636
- log(`Comms: ${toAbsolute(opts.commsDir)}`);
3637
- if (opts.mission) log(`Mission: ${opts.mission}`);
3638
- log("");
3639
- log("Next steps:");
3640
- log(` cd ${opts.worktreePath}`);
3641
- log(" claude # Start Claude Code session");
3642
- log("");
3643
- }
3644
- async function initWorktreeCommand(args) {
3645
- const { flags } = parseArgs(args);
3646
- if (flags["help"] === true || flags["h"] === true) {
3647
- log(INIT_WORKTREE_HELP);
3648
- return {
3649
- ok: true,
3650
- command: "init-worktree",
3651
- code: "TAP_NO_OP",
3652
- message: "init-worktree help",
3653
- warnings: [],
3654
- data: {}
3655
- };
3656
- }
3657
- const worktreePath = typeof flags["path"] === "string" ? flags["path"] : void 0;
3658
- if (!worktreePath) {
3659
- return {
3660
- ok: false,
3661
- command: "init-worktree",
3662
- code: "TAP_INVALID_ARGUMENT",
3663
- message: "Missing --path. Usage: npx @hua-labs/tap init-worktree --path ../hua-wt-3",
3664
- warnings: [],
3665
- data: {}
3666
- };
3667
- }
3668
- const repoRoot = findRepoRoot2();
3669
- const { config } = resolveConfig({}, repoRoot);
3670
- const branch = typeof flags["branch"] === "string" ? flags["branch"] : path15.basename(path15.resolve(worktreePath));
3671
- const base = typeof flags["base"] === "string" ? flags["base"] : "origin/main";
3672
- const mission = typeof flags["mission"] === "string" ? flags["mission"] : void 0;
3673
- const commsDir = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : config.commsDir;
3674
- const skipInstall = flags["skip-install"] === true;
3675
- const opts = {
3676
- worktreePath: path15.resolve(worktreePath),
3677
- branch,
3678
- base,
3679
- mission,
3680
- commsDir: path15.resolve(commsDir),
3681
- skipInstall,
3682
- repoRoot
3683
- };
3684
- logHeader(`@hua-labs/tap init-worktree`);
3685
- log(`Path: ${opts.worktreePath}`);
3686
- log(`Branch: ${opts.branch}`);
3687
- log(`Base: ${opts.base}`);
3688
- log(`Comms: ${opts.commsDir}`);
3689
- if (mission) log(`Mission: ${mission}`);
3690
- log("");
3691
- const warnings = [];
3692
- const created = step1CreateWorktree(opts);
3693
- if (!created) {
3694
- return {
3695
- ok: false,
3696
- command: "init-worktree",
3697
- code: "TAP_PATCH_FAILED",
3698
- message: "Failed to create worktree.",
3699
- warnings,
3700
- data: {}
3701
- };
3702
- }
3703
- step2MergeMain(opts, warnings);
3704
- step3CopyPermissions(opts, warnings);
3705
- step4GenerateMcpJson(opts, warnings);
3706
- step5Install(opts, warnings);
3707
- step6BuildEslintPlugin(opts, warnings);
3708
- step7VerifyComms(opts, warnings);
3709
- step8VerifyBun(warnings);
3710
- step9Ready(opts);
3711
- return {
3712
- ok: true,
3713
- command: "init-worktree",
3714
- code: "TAP_INIT_OK",
3715
- message: `Worktree initialized: ${opts.worktreePath}`,
3716
- warnings,
3717
- data: {
3718
- path: opts.worktreePath,
3719
- branch: opts.branch,
3720
- commsDir: opts.commsDir
3721
- }
3722
- };
3723
- }
3724
-
3725
- // src/engine/dashboard.ts
3726
- import * as fs16 from "fs";
3727
- import * as path16 from "path";
3728
- import { execSync as execSync6 } from "child_process";
3729
- function collectAgents(commsDir) {
3730
- const heartbeatsPath = path16.join(commsDir, "heartbeats.json");
3731
- if (!fs16.existsSync(heartbeatsPath)) return [];
3732
- try {
3733
- const raw = fs16.readFileSync(heartbeatsPath, "utf-8");
3734
- const data = JSON.parse(raw);
3735
- return Object.entries(data).map(([name, info]) => ({
3736
- name: info.agent ?? name,
3737
- status: info.status ?? null,
3738
- lastActivity: info.lastActivity ?? info.timestamp ?? null,
3739
- joinedAt: info.joinedAt ?? null
3740
- }));
3741
- } catch {
3742
- return [];
3743
- }
3744
- }
3745
- function collectBridges(repoRoot) {
3746
- const state = loadState(repoRoot);
3747
- const { config } = resolveConfig({}, repoRoot);
3748
- const stateDir = config.stateDir;
3749
- const bridges = [];
3750
- if (state) {
3751
- for (const [id, inst] of Object.entries(state.instances)) {
3752
- if (!inst?.installed) continue;
3753
- if (inst.bridgeMode !== "app-server") continue;
3754
- const instanceId = id;
3755
- const status = getBridgeStatus(stateDir, instanceId);
3756
- const bridgeState = loadBridgeState(stateDir, instanceId);
3757
- const age = getHeartbeatAge(stateDir, instanceId);
3758
- bridges.push({
3759
- instanceId: id,
3760
- runtime: inst.runtime,
3761
- status,
3762
- pid: bridgeState?.pid ?? null,
3763
- port: inst.port ?? null,
3764
- heartbeatAge: age,
3765
- headless: inst.headless?.enabled ?? false
3766
- });
3767
- }
3768
- }
3769
- const tmpDir = path16.join(repoRoot, ".tmp");
3770
- if (fs16.existsSync(tmpDir)) {
3771
- try {
3772
- const dirs = fs16.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
3773
- for (const dir of dirs) {
3774
- const daemonPath = path16.join(tmpDir, dir, "bridge-daemon.json");
3775
- if (!fs16.existsSync(daemonPath)) continue;
3776
- try {
3777
- const raw = fs16.readFileSync(daemonPath, "utf-8");
3778
- const daemon = JSON.parse(raw);
3779
- const alreadyCovered = bridges.some(
3780
- (b) => b.pid === daemon.pid && b.pid !== null
3781
- );
3782
- if (alreadyCovered) continue;
3783
- const agentFile = path16.join(tmpDir, dir, "agent-name.txt");
3784
- const agentName = fs16.existsSync(agentFile) ? fs16.readFileSync(agentFile, "utf-8").trim() : dir;
3785
- const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
3786
- const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
3787
- const port = portMatch ? parseInt(portMatch[1], 10) : null;
3788
- bridges.push({
3789
- instanceId: agentName,
3790
- runtime: "codex",
3791
- status: running ? "running" : "stale",
3792
- pid: daemon.pid ?? null,
3793
- port,
3794
- heartbeatAge: null,
3795
- headless: false
3796
- });
3797
- } catch {
3798
- }
3799
- }
3800
- } catch {
3801
- }
3802
- }
3803
- return bridges;
3804
- }
3805
- function collectPRs() {
3806
- try {
3807
- const output = execSync6(
3808
- "gh pr list --state all --limit 10 --json number,title,author,state,url",
3809
- { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
3810
- );
3811
- const prs = JSON.parse(output);
3812
- return prs.map((pr) => ({
3813
- number: pr.number,
3814
- title: pr.title,
3815
- author: pr.author.login,
3816
- state: pr.state,
3817
- url: pr.url
3818
- }));
3819
- } catch {
3820
- return [];
3821
- }
3822
- }
3823
- function collectWarnings(bridges, agents) {
3824
- const warnings = [];
3825
- for (const bridge of bridges) {
3826
- if (bridge.status === "stale") {
3827
- warnings.push({
3828
- level: "warn",
3829
- message: `Bridge ${bridge.instanceId} is stale (PID ${bridge.pid} dead)`
3830
- });
3831
- }
3832
- if (bridge.status === "running" && bridge.heartbeatAge !== null && bridge.heartbeatAge > 60) {
3833
- warnings.push({
3834
- level: "warn",
3835
- message: `Bridge ${bridge.instanceId} heartbeat stale (${bridge.heartbeatAge}s ago)`
3836
- });
5110
+ warn(warnings, "Create it or run: npx @hua-labs/tap init");
5111
+ return;
5112
+ }
5113
+ const requiredDirs = ["inbox", "findings", "reviews", "letters"];
5114
+ for (const dir of requiredDirs) {
5115
+ const dirPath = path17.join(opts.commsDir, dir);
5116
+ if (!fs16.existsSync(dirPath)) {
5117
+ fs16.mkdirSync(dirPath, { recursive: true });
5118
+ logSuccess(`Created ${dir}/`);
3837
5119
  }
3838
5120
  }
3839
- if (bridges.length === 0) {
3840
- warnings.push({
3841
- level: "warn",
3842
- message: "No bridges configured"
3843
- });
5121
+ logSuccess(`Comms verified: ${opts.commsDir}`);
5122
+ }
5123
+ function step8VerifyBun(warnings) {
5124
+ log("Step 8/9: Verifying bun...");
5125
+ const bunPath = findBun();
5126
+ if (!bunPath) {
5127
+ warn(warnings, "bun not found in PATH.");
5128
+ warn(warnings, "Install: curl -fsSL https://bun.sh/install | bash");
5129
+ return;
3844
5130
  }
3845
- if (agents.length === 0) {
3846
- warnings.push({
3847
- level: "warn",
3848
- message: "No agent heartbeats found"
3849
- });
5131
+ try {
5132
+ const version2 = run(`"${bunPath}" --version`);
5133
+ logSuccess(`bun ${version2} found: ${bunPath}`);
5134
+ } catch {
5135
+ warn(warnings, "bun found but version check failed.");
3850
5136
  }
3851
- return warnings;
3852
5137
  }
3853
- function collectDashboardSnapshot(repoRoot, commsDirOverride) {
3854
- const { config } = resolveConfig(
3855
- commsDirOverride ? { commsDir: commsDirOverride } : {},
5138
+ function step9Ready(opts) {
5139
+ logHeader("Ready!");
5140
+ log(`Worktree: ${toAbsolute(opts.worktreePath)}`);
5141
+ log(`Branch: ${opts.branch}`);
5142
+ log(`Comms: ${toAbsolute(opts.commsDir)}`);
5143
+ if (opts.mission) log(`Mission: ${opts.mission}`);
5144
+ log("");
5145
+ log("Next steps:");
5146
+ log(` cd ${opts.worktreePath}`);
5147
+ log(" claude # Start Claude Code session");
5148
+ log("");
5149
+ }
5150
+ async function initWorktreeCommand(args) {
5151
+ const { flags } = parseArgs(args);
5152
+ if (flags["help"] === true || flags["h"] === true) {
5153
+ log(INIT_WORKTREE_HELP);
5154
+ return {
5155
+ ok: true,
5156
+ command: "init-worktree",
5157
+ code: "TAP_NO_OP",
5158
+ message: "init-worktree help",
5159
+ warnings: [],
5160
+ data: {}
5161
+ };
5162
+ }
5163
+ const worktreePath = typeof flags["path"] === "string" ? flags["path"] : void 0;
5164
+ if (!worktreePath) {
5165
+ return {
5166
+ ok: false,
5167
+ command: "init-worktree",
5168
+ code: "TAP_INVALID_ARGUMENT",
5169
+ message: "Missing --path. Usage: npx @hua-labs/tap init-worktree --path ../hua-wt-3",
5170
+ warnings: [],
5171
+ data: {}
5172
+ };
5173
+ }
5174
+ const repoRoot = findRepoRoot();
5175
+ const { config } = resolveConfig({}, repoRoot);
5176
+ const branch = typeof flags["branch"] === "string" ? flags["branch"] : path17.basename(path17.resolve(worktreePath));
5177
+ const base = typeof flags["base"] === "string" ? flags["base"] : "origin/main";
5178
+ const mission = typeof flags["mission"] === "string" ? flags["mission"] : void 0;
5179
+ const commsDir = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : config.commsDir;
5180
+ const skipInstall = flags["skip-install"] === true;
5181
+ const opts = {
5182
+ worktreePath: path17.resolve(worktreePath),
5183
+ branch,
5184
+ base,
5185
+ mission,
5186
+ commsDir: path17.resolve(commsDir),
5187
+ skipInstall,
3856
5188
  repoRoot
3857
- );
3858
- const resolved = config;
3859
- const agents = collectAgents(resolved.commsDir);
3860
- const bridges = collectBridges(resolved.repoRoot);
3861
- const prs = collectPRs();
3862
- const warnings = collectWarnings(bridges, agents);
5189
+ };
5190
+ logHeader(`@hua-labs/tap init-worktree`);
5191
+ log(`Path: ${opts.worktreePath}`);
5192
+ log(`Branch: ${opts.branch}`);
5193
+ log(`Base: ${opts.base}`);
5194
+ log(`Comms: ${opts.commsDir}`);
5195
+ if (mission) log(`Mission: ${mission}`);
5196
+ log("");
5197
+ const warnings = [];
5198
+ const created = step1CreateWorktree(opts);
5199
+ if (!created) {
5200
+ return {
5201
+ ok: false,
5202
+ command: "init-worktree",
5203
+ code: "TAP_PATCH_FAILED",
5204
+ message: "Failed to create worktree.",
5205
+ warnings,
5206
+ data: {}
5207
+ };
5208
+ }
5209
+ step2MergeMain(opts, warnings);
5210
+ step3CopyPermissions(opts, warnings);
5211
+ step4GenerateMcpJson(opts, warnings);
5212
+ step5Install(opts, warnings);
5213
+ step6BuildEslintPlugin(opts, warnings);
5214
+ step7VerifyComms(opts, warnings);
5215
+ step8VerifyBun(warnings);
5216
+ step9Ready(opts);
3863
5217
  return {
3864
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3865
- repoRoot: resolved.repoRoot,
3866
- commsDir: resolved.commsDir,
3867
- agents,
3868
- bridges,
3869
- prs,
3870
- warnings
5218
+ ok: true,
5219
+ command: "init-worktree",
5220
+ code: "TAP_INIT_OK",
5221
+ message: `Worktree initialized: ${opts.worktreePath}`,
5222
+ warnings,
5223
+ data: {
5224
+ path: opts.worktreePath,
5225
+ branch: opts.branch,
5226
+ commsDir: opts.commsDir
5227
+ }
3871
5228
  };
3872
5229
  }
3873
5230
 
@@ -3967,7 +5324,7 @@ async function dashboardCommand(args) {
3967
5324
  const intervalStr = typeof flags["interval"] === "string" ? flags["interval"] : "5";
3968
5325
  const intervalSeconds = Math.max(2, parseInt(intervalStr, 10) || 5);
3969
5326
  const commsDirOverride = typeof flags["comms-dir"] === "string" ? flags["comms-dir"] : void 0;
3970
- const repoRoot = findRepoRoot2();
5327
+ const repoRoot = findRepoRoot();
3971
5328
  if (watchMode) {
3972
5329
  const run2 = () => {
3973
5330
  const snapshot2 = collectDashboardSnapshot(repoRoot, commsDirOverride);
@@ -4015,6 +5372,371 @@ async function dashboardCommand(args) {
4015
5372
  };
4016
5373
  }
4017
5374
 
5375
+ // src/commands/doctor.ts
5376
+ import {
5377
+ existsSync as existsSync16,
5378
+ mkdirSync as mkdirSync10,
5379
+ readdirSync as readdirSync3,
5380
+ readFileSync as readFileSync14,
5381
+ statSync as statSync2,
5382
+ unlinkSync as unlinkSync3
5383
+ } from "fs";
5384
+ import { join as join17 } from "path";
5385
+ var PASS = "pass";
5386
+ var WARN = "warn";
5387
+ var FAIL = "fail";
5388
+ function countFiles(dir, ext = ".md") {
5389
+ if (!existsSync16(dir)) return 0;
5390
+ try {
5391
+ return readdirSync3(dir).filter((f) => f.endsWith(ext)).length;
5392
+ } catch {
5393
+ return 0;
5394
+ }
5395
+ }
5396
+ function recentFileCount(dir, withinMs) {
5397
+ if (!existsSync16(dir)) return 0;
5398
+ const cutoff = Date.now() - withinMs;
5399
+ let count = 0;
5400
+ try {
5401
+ for (const f of readdirSync3(dir)) {
5402
+ if (!f.endsWith(".md")) continue;
5403
+ try {
5404
+ if (statSync2(join17(dir, f)).mtimeMs > cutoff) count++;
5405
+ } catch {
5406
+ }
5407
+ }
5408
+ } catch {
5409
+ }
5410
+ return count;
5411
+ }
5412
+ function checkComms(commsDir) {
5413
+ const checks = [];
5414
+ checks.push({
5415
+ name: "comms directory",
5416
+ status: existsSync16(commsDir) ? PASS : FAIL,
5417
+ message: existsSync16(commsDir) ? commsDir : `Not found: ${commsDir}`,
5418
+ fix: existsSync16(commsDir) ? void 0 : () => {
5419
+ mkdirSync10(commsDir, { recursive: true });
5420
+ return `Created ${commsDir}`;
5421
+ }
5422
+ });
5423
+ for (const [subdir, required] of [
5424
+ ["inbox", true],
5425
+ ["reviews", false],
5426
+ ["findings", false]
5427
+ ]) {
5428
+ const dir = join17(commsDir, subdir);
5429
+ const exists = existsSync16(dir);
5430
+ checks.push({
5431
+ name: `${subdir} directory`,
5432
+ status: exists ? PASS : required ? FAIL : WARN,
5433
+ message: exists ? subdir === "findings" ? `${countFiles(dir)} findings` : subdir === "inbox" ? `${countFiles(dir)} messages` : "exists" : `Missing${required ? "" : " (optional)"}`,
5434
+ fix: exists ? void 0 : () => {
5435
+ mkdirSync10(dir, { recursive: true });
5436
+ return `Created ${dir}`;
5437
+ }
5438
+ });
5439
+ }
5440
+ const heartbeats = join17(commsDir, "heartbeats.json");
5441
+ if (existsSync16(heartbeats)) {
5442
+ try {
5443
+ const store = JSON.parse(readFileSync14(heartbeats, "utf-8"));
5444
+ const agents = Object.keys(store);
5445
+ const now = Date.now();
5446
+ const active = agents.filter((a) => {
5447
+ const ts = store[a]?.lastActivity;
5448
+ return ts && now - new Date(ts).getTime() < 10 * 60 * 1e3;
5449
+ });
5450
+ checks.push({
5451
+ name: "heartbeats",
5452
+ status: active.length > 0 ? PASS : WARN,
5453
+ message: `${active.length} active / ${agents.length} total`
5454
+ });
5455
+ } catch {
5456
+ checks.push({
5457
+ name: "heartbeats",
5458
+ status: WARN,
5459
+ message: "File exists but unreadable"
5460
+ });
5461
+ }
5462
+ } else {
5463
+ checks.push({
5464
+ name: "heartbeats",
5465
+ status: WARN,
5466
+ message: "No heartbeats file"
5467
+ });
5468
+ }
5469
+ return checks;
5470
+ }
5471
+ function checkInstances(repoRoot, stateDir) {
5472
+ const checks = [];
5473
+ const state = loadState(repoRoot);
5474
+ if (!state) {
5475
+ checks.push({
5476
+ name: "tap state",
5477
+ status: FAIL,
5478
+ message: "Not initialized. Run: tap init"
5479
+ });
5480
+ return checks;
5481
+ }
5482
+ checks.push({
5483
+ name: "tap state",
5484
+ status: PASS,
5485
+ message: `v${state.schemaVersion}, ${getInstalledInstances(state).length} instance(s)`
5486
+ });
5487
+ const installed = getInstalledInstances(state);
5488
+ for (const id of installed) {
5489
+ const inst = state.instances[id];
5490
+ if (!inst) continue;
5491
+ if (inst.bridgeMode === "app-server") {
5492
+ const running = isBridgeRunning(stateDir, id);
5493
+ const bridgeState = loadBridgeState(stateDir, id);
5494
+ const heartbeatAge = getHeartbeatAge(stateDir, id);
5495
+ let status;
5496
+ let message;
5497
+ let fix;
5498
+ if (running && bridgeState) {
5499
+ if (heartbeatAge !== null && heartbeatAge > 120) {
5500
+ status = WARN;
5501
+ message = `PID ${bridgeState.pid} alive but heartbeat stale (${Math.round(heartbeatAge)}s ago)`;
5502
+ } else {
5503
+ status = PASS;
5504
+ message = `PID ${bridgeState.pid}, port ${inst.port ?? "auto"}`;
5505
+ }
5506
+ } else if (bridgeState && !running) {
5507
+ status = WARN;
5508
+ message = `Stale PID ${bridgeState.pid} (process dead)`;
5509
+ fix = () => {
5510
+ const appServer = bridgeState.appServer;
5511
+ if (appServer?.managed) {
5512
+ for (const pid of [appServer.auth?.gatewayPid, appServer.pid]) {
5513
+ if (pid) {
5514
+ try {
5515
+ process.kill(pid);
5516
+ } catch {
5517
+ }
5518
+ }
5519
+ }
5520
+ }
5521
+ const pidPath = join17(stateDir, "pids", `bridge-${id}.json`);
5522
+ try {
5523
+ unlinkSync3(pidPath);
5524
+ } catch {
5525
+ }
5526
+ const currentState = loadState(repoRoot);
5527
+ if (currentState?.instances[id]) {
5528
+ currentState.instances[id].bridge = null;
5529
+ currentState.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
5530
+ saveState(repoRoot, currentState);
5531
+ }
5532
+ return `Cleaned stale bridge + managed processes for ${id}`;
5533
+ };
5534
+ } else {
5535
+ status = WARN;
5536
+ message = "Not running";
5537
+ }
5538
+ checks.push({ name: `bridge: ${id}`, status, message, fix });
5539
+ } else {
5540
+ checks.push({
5541
+ name: `instance: ${id}`,
5542
+ status: PASS,
5543
+ message: `${inst.runtime} (${inst.bridgeMode})`
5544
+ });
5545
+ }
5546
+ }
5547
+ return checks;
5548
+ }
5549
+ function checkMessageLifecycle(commsDir) {
5550
+ const checks = [];
5551
+ const inbox = join17(commsDir, "inbox");
5552
+ if (!existsSync16(inbox)) {
5553
+ checks.push({
5554
+ name: "message flow",
5555
+ status: FAIL,
5556
+ message: "No inbox"
5557
+ });
5558
+ return checks;
5559
+ }
5560
+ const total = countFiles(inbox);
5561
+ const recent1h = recentFileCount(inbox, 60 * 60 * 1e3);
5562
+ const recent10m = recentFileCount(inbox, 10 * 60 * 1e3);
5563
+ checks.push({
5564
+ name: "message flow",
5565
+ status: recent10m > 0 ? PASS : total > 0 ? WARN : FAIL,
5566
+ message: `${total} total, ${recent1h} in last 1h, ${recent10m} in last 10m`
5567
+ });
5568
+ const receiptsPath = join17(commsDir, "receipts", "receipts.json");
5569
+ if (existsSync16(receiptsPath)) {
5570
+ try {
5571
+ const receipts = JSON.parse(readFileSync14(receiptsPath, "utf-8"));
5572
+ const receiptCount = Object.keys(receipts).length;
5573
+ checks.push({
5574
+ name: "read receipts",
5575
+ status: PASS,
5576
+ message: `${receiptCount} receipts tracked`
5577
+ });
5578
+ } catch {
5579
+ checks.push({
5580
+ name: "read receipts",
5581
+ status: WARN,
5582
+ message: "File exists but unreadable"
5583
+ });
5584
+ }
5585
+ }
5586
+ return checks;
5587
+ }
5588
+ function checkMcpServer(repoRoot) {
5589
+ const checks = [];
5590
+ const mcpJson = join17(repoRoot, ".mcp.json");
5591
+ if (existsSync16(mcpJson)) {
5592
+ try {
5593
+ const config = JSON.parse(readFileSync14(mcpJson, "utf-8"));
5594
+ const hasTapComms = config?.mcpServers?.["tap-comms"];
5595
+ checks.push({
5596
+ name: "MCP config (.mcp.json)",
5597
+ status: hasTapComms ? PASS : WARN,
5598
+ message: hasTapComms ? `command: ${hasTapComms.command}` : "tap-comms not configured"
5599
+ });
5600
+ if (hasTapComms?.args?.[0]) {
5601
+ const mcpScript = hasTapComms.args[0];
5602
+ checks.push({
5603
+ name: "MCP server script",
5604
+ status: existsSync16(mcpScript) ? PASS : FAIL,
5605
+ message: existsSync16(mcpScript) ? mcpScript : `Not found: ${mcpScript}`
5606
+ });
5607
+ }
5608
+ } catch {
5609
+ checks.push({
5610
+ name: "MCP config (.mcp.json)",
5611
+ status: WARN,
5612
+ message: "File exists but invalid JSON"
5613
+ });
5614
+ }
5615
+ } else {
5616
+ checks.push({
5617
+ name: "MCP config (.mcp.json)",
5618
+ status: WARN,
5619
+ message: "Not found \u2014 MCP channel notifications won't work"
5620
+ });
5621
+ }
5622
+ return checks;
5623
+ }
5624
+ function renderCheck(check, fixMode) {
5625
+ const icons = {
5626
+ pass: "[OK]",
5627
+ warn: "[!!]",
5628
+ fail: "[XX]",
5629
+ skip: "[--]"
5630
+ };
5631
+ const icon = icons[check.status] || "[??]";
5632
+ const fixable = fixMode && check.fix ? " (fixable)" : "";
5633
+ const msg = check.message ? ` \u2014 ${check.message}${fixable}` : "";
5634
+ return ` ${icon} ${check.name}${msg}`;
5635
+ }
5636
+ async function doctorCommand(args) {
5637
+ const repoRoot = findRepoRoot();
5638
+ const overrides = {};
5639
+ let fixMode = false;
5640
+ for (let i = 0; i < args.length; i++) {
5641
+ if (args[i] === "--comms-dir" && args[i + 1]) {
5642
+ overrides.commsDir = args[i + 1];
5643
+ }
5644
+ if (args[i] === "--fix") {
5645
+ fixMode = true;
5646
+ }
5647
+ }
5648
+ const { config } = resolveConfig(overrides, repoRoot);
5649
+ const state = loadState(repoRoot);
5650
+ const commsDir = overrides.commsDir ? config.commsDir : state?.commsDir ?? config.commsDir;
5651
+ logHeader(`@hua-labs/tap doctor (v${version})${fixMode ? " --fix" : ""}`);
5652
+ function runAllChecks() {
5653
+ const checks = [];
5654
+ checks.push(...checkComms(commsDir));
5655
+ checks.push(...checkInstances(repoRoot, config.stateDir));
5656
+ checks.push(...checkMessageLifecycle(commsDir));
5657
+ checks.push(...checkMcpServer(repoRoot));
5658
+ return checks;
5659
+ }
5660
+ const initialChecks = runAllChecks();
5661
+ for (const section of ["Comms", "Instances", "Messages", "MCP"]) {
5662
+ const sectionChecks = {
5663
+ Comms: initialChecks.filter(
5664
+ (c) => [
5665
+ "comms directory",
5666
+ "inbox directory",
5667
+ "reviews directory",
5668
+ "findings directory",
5669
+ "heartbeats"
5670
+ ].includes(c.name)
5671
+ ),
5672
+ Instances: initialChecks.filter(
5673
+ (c) => c.name.startsWith("bridge:") || c.name.startsWith("instance:") || c.name === "tap state"
5674
+ ),
5675
+ Messages: initialChecks.filter(
5676
+ (c) => ["message flow", "read receipts"].includes(c.name)
5677
+ ),
5678
+ MCP: initialChecks.filter(
5679
+ (c) => c.name.startsWith("MCP") || c.name === "MCP server script"
5680
+ )
5681
+ }[section];
5682
+ if (sectionChecks.length > 0) {
5683
+ log(`${section}:`);
5684
+ for (const c of sectionChecks) log(renderCheck(c, fixMode));
5685
+ log("");
5686
+ }
5687
+ }
5688
+ const fixed = [];
5689
+ let finalChecks = initialChecks;
5690
+ if (fixMode) {
5691
+ const fixable = initialChecks.filter(
5692
+ (c) => (c.status === "warn" || c.status === "fail") && c.fix
5693
+ );
5694
+ if (fixable.length > 0) {
5695
+ log("Fixes:");
5696
+ for (const c of fixable) {
5697
+ try {
5698
+ const desc = c.fix();
5699
+ fixed.push(desc);
5700
+ logSuccess(` ${desc}`);
5701
+ } catch (err) {
5702
+ logWarn(
5703
+ ` Failed to fix ${c.name}: ${err instanceof Error ? err.message : String(err)}`
5704
+ );
5705
+ }
5706
+ }
5707
+ log("");
5708
+ log("Re-verifying...");
5709
+ finalChecks = runAllChecks();
5710
+ const postFails = finalChecks.filter((c) => c.status === "fail").length;
5711
+ const postWarns = finalChecks.filter((c) => c.status === "warn").length;
5712
+ log(
5713
+ ` ${postFails === 0 ? "All clear" : `${postFails} remaining failures, ${postWarns} warnings`}`
5714
+ );
5715
+ } else {
5716
+ log("Nothing to fix.");
5717
+ }
5718
+ }
5719
+ const passes = finalChecks.filter((c) => c.status === "pass").length;
5720
+ const warns = finalChecks.filter((c) => c.status === "warn").length;
5721
+ const fails = finalChecks.filter((c) => c.status === "fail").length;
5722
+ log("");
5723
+ log(
5724
+ `${finalChecks.length} checks: ${passes} passed, ${warns} warnings, ${fails} failures` + (fixed.length > 0 ? ` (${fixed.length} fixed)` : "")
5725
+ );
5726
+ return {
5727
+ ok: fails === 0,
5728
+ command: "doctor",
5729
+ code: fails === 0 ? "TAP_STATUS_OK" : "TAP_VERIFY_FAILED",
5730
+ message: `${passes} passed, ${warns} warnings, ${fails} failures`,
5731
+ warnings: finalChecks.filter((c) => c.status === "warn").map((c) => `${c.name}: ${c.message}`),
5732
+ data: {
5733
+ checks: finalChecks.map(({ fix, ...rest }) => rest),
5734
+ summary: { total: finalChecks.length, passes, warns, fails },
5735
+ fixed
5736
+ }
5737
+ };
5738
+ }
5739
+
4018
5740
  // src/output.ts
4019
5741
  function emitResult(result, jsonMode) {
4020
5742
  if (jsonMode) {
@@ -4053,7 +5775,10 @@ Commands:
4053
5775
  remove <instance> Remove an instance and rollback config
4054
5776
  status Show installed instances and bridge status
4055
5777
  bridge <sub> [inst] Manage bridges (start, stop, status)
5778
+ up Start all registered bridge daemons
5779
+ down Stop all running bridge daemons
4056
5780
  dashboard Show unified ops dashboard
5781
+ doctor Diagnose tap infrastructure health
4057
5782
  serve Start tap-comms MCP server (stdio)
4058
5783
  version Show version
4059
5784
 
@@ -4077,7 +5802,10 @@ function normalizeCommandName(command) {
4077
5802
  case "remove":
4078
5803
  case "status":
4079
5804
  case "bridge":
5805
+ case "up":
5806
+ case "down":
4080
5807
  case "dashboard":
5808
+ case "doctor":
4081
5809
  case "serve":
4082
5810
  return command;
4083
5811
  default:
@@ -4127,9 +5855,18 @@ async function main() {
4127
5855
  case "bridge":
4128
5856
  result = await bridgeCommand(commandArgs);
4129
5857
  break;
5858
+ case "up":
5859
+ result = await upCommand(commandArgs);
5860
+ break;
5861
+ case "down":
5862
+ result = await downCommand(commandArgs);
5863
+ break;
4130
5864
  case "dashboard":
4131
5865
  result = await dashboardCommand(commandArgs);
4132
5866
  break;
5867
+ case "doctor":
5868
+ result = await doctorCommand(commandArgs);
5869
+ break;
4133
5870
  case "serve": {
4134
5871
  const serveResult = await serveCommand(commandArgs);
4135
5872
  if (!serveResult.ok) {