@hua-labs/tap 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,45 +1,199 @@
1
- // src/state.ts
2
- import * as fs2 from "fs";
3
- import * as path2 from "path";
4
- import * as crypto from "crypto";
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
5
10
 
6
- // src/config/resolve.ts
11
+ // src/utils.ts
7
12
  import * as fs from "fs";
8
13
  import * as path from "path";
9
- var SHARED_CONFIG_FILE = "tap-config.json";
10
- var LOCAL_CONFIG_FILE = "tap-config.local.json";
11
- var DEFAULT_RUNTIME_COMMAND = "node";
12
- var DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
14
+ function isValidRuntime(name) {
15
+ return VALID_RUNTIMES.includes(name);
16
+ }
17
+ function detectPlatform() {
18
+ return process.platform;
19
+ }
20
+ function _setNoGitWarned() {
21
+ _noGitWarned = true;
22
+ }
13
23
  function findRepoRoot(startDir = process.cwd()) {
14
24
  let dir = path.resolve(startDir);
15
25
  while (true) {
16
26
  if (fs.existsSync(path.join(dir, ".git"))) return dir;
17
- if (fs.existsSync(path.join(dir, "package.json"))) return dir;
27
+ if (fs.existsSync(path.join(dir, "package.json"))) {
28
+ if (!_noGitWarned) {
29
+ _setNoGitWarned();
30
+ logWarn(
31
+ "No .git directory found. Resolved repo root via package.json \u2014 comms directory may be created in an unexpected location. Use --comms-dir to specify explicitly."
32
+ );
33
+ }
34
+ return dir;
35
+ }
18
36
  const parent = path.dirname(dir);
19
37
  if (parent === dir) break;
20
38
  dir = parent;
21
39
  }
40
+ if (!_noGitWarned) {
41
+ _setNoGitWarned();
42
+ logWarn(
43
+ "No git repository or package.json found. Using current directory as root. Run 'git init' first, or use --comms-dir to specify the comms path."
44
+ );
45
+ }
46
+ return process.cwd();
47
+ }
48
+ function createAdapterContext(commsDir, repoRoot) {
49
+ const { config } = resolveConfig({}, repoRoot);
50
+ return {
51
+ commsDir: path.resolve(commsDir),
52
+ repoRoot: path.resolve(repoRoot),
53
+ stateDir: config.stateDir,
54
+ platform: detectPlatform()
55
+ };
56
+ }
57
+ function parseArgs(args) {
58
+ const positional = [];
59
+ const flags = {};
60
+ for (let i = 0; i < args.length; i++) {
61
+ const arg = args[i];
62
+ if (arg.startsWith("--")) {
63
+ const key = arg.slice(2);
64
+ const next = args[i + 1];
65
+ if (next && !next.startsWith("--")) {
66
+ flags[key] = next;
67
+ i++;
68
+ } else {
69
+ flags[key] = true;
70
+ }
71
+ } else if (arg.startsWith("-")) {
72
+ flags[arg.slice(1)] = true;
73
+ } else {
74
+ positional.push(arg);
75
+ }
76
+ }
77
+ return { positional, flags };
78
+ }
79
+ function log(message) {
80
+ if (!_jsonMode) console.log(` ${message}`);
81
+ }
82
+ function logSuccess(message) {
83
+ if (!_jsonMode) console.log(` + ${message}`);
84
+ }
85
+ function logWarn(message) {
86
+ if (!_jsonMode) console.log(` ! ${message}`);
87
+ }
88
+ function logError(message) {
89
+ if (!_jsonMode) console.error(` x ${message}`);
90
+ }
91
+ function logHeader(message) {
92
+ if (!_jsonMode) console.log(`
93
+ ${message}
94
+ `);
95
+ }
96
+ function resolveInstanceId(identifier, state) {
97
+ if (state.instances[identifier]) {
98
+ return { ok: true, instanceId: identifier };
99
+ }
100
+ if (isValidRuntime(identifier)) {
101
+ const matches = Object.values(state.instances).filter(
102
+ (inst) => inst.runtime === identifier
103
+ );
104
+ if (matches.length === 1) {
105
+ return { ok: true, instanceId: matches[0].instanceId };
106
+ }
107
+ if (matches.length > 1) {
108
+ const ids = matches.map((m) => m.instanceId).join(", ");
109
+ return {
110
+ ok: false,
111
+ code: "TAP_INSTANCE_AMBIGUOUS",
112
+ message: `Multiple ${identifier} instances found: ${ids}. Specify one explicitly.`
113
+ };
114
+ }
115
+ }
116
+ return {
117
+ ok: false,
118
+ code: "TAP_INSTANCE_NOT_FOUND",
119
+ message: `Instance not found: ${identifier}`
120
+ };
121
+ }
122
+ var VALID_RUNTIMES, _noGitWarned, _jsonMode;
123
+ var init_utils = __esm({
124
+ "src/utils.ts"() {
125
+ "use strict";
126
+ init_config();
127
+ VALID_RUNTIMES = ["claude", "codex", "gemini"];
128
+ _noGitWarned = false;
129
+ _jsonMode = false;
130
+ }
131
+ });
132
+
133
+ // src/config/resolve.ts
134
+ import * as fs2 from "fs";
135
+ import * as path2 from "path";
136
+ function findRepoRoot2(startDir = process.cwd()) {
137
+ let dir = path2.resolve(startDir);
138
+ while (true) {
139
+ if (fs2.existsSync(path2.join(dir, ".git"))) return dir;
140
+ if (fs2.existsSync(path2.join(dir, "package.json"))) {
141
+ if (!_noGitWarned) {
142
+ _setNoGitWarned();
143
+ console.error(
144
+ "[tap] warning: No .git directory found. Resolved via package.json. Use --comms-dir to specify explicitly."
145
+ );
146
+ }
147
+ return dir;
148
+ }
149
+ const parent = path2.dirname(dir);
150
+ if (parent === dir) break;
151
+ dir = parent;
152
+ }
153
+ if (!_noGitWarned) {
154
+ _setNoGitWarned();
155
+ console.error(
156
+ "[tap] warning: No git repository found. Using cwd as root. Run 'git init' or use --comms-dir."
157
+ );
158
+ }
22
159
  return process.cwd();
23
160
  }
24
161
  function loadJsonFile(filePath) {
25
- if (!fs.existsSync(filePath)) return null;
162
+ if (!fs2.existsSync(filePath)) return null;
26
163
  try {
27
- const raw = fs.readFileSync(filePath, "utf-8");
164
+ const raw = fs2.readFileSync(filePath, "utf-8");
28
165
  return JSON.parse(raw);
29
166
  } catch {
30
167
  return null;
31
168
  }
32
169
  }
33
170
  function loadSharedConfig(repoRoot) {
34
- return loadJsonFile(path.join(repoRoot, SHARED_CONFIG_FILE));
171
+ return loadJsonFile(path2.join(repoRoot, SHARED_CONFIG_FILE));
35
172
  }
36
173
  function loadLocalConfig(repoRoot) {
37
- return loadJsonFile(path.join(repoRoot, LOCAL_CONFIG_FILE));
174
+ return loadJsonFile(path2.join(repoRoot, LOCAL_CONFIG_FILE));
175
+ }
176
+ function readLegacyShellValue(configText, key) {
177
+ const match = configText.match(new RegExp(`^${key}="?(.+?)"?$`, "m"));
178
+ return match?.[1]?.trim() || null;
179
+ }
180
+ function loadLegacyShellConfig(repoRoot) {
181
+ const filePath = path2.join(repoRoot, LEGACY_CONFIG_FILE);
182
+ if (!fs2.existsSync(filePath)) return null;
183
+ try {
184
+ const raw = fs2.readFileSync(filePath, "utf-8");
185
+ const commsDir = readLegacyShellValue(raw, "TAP_COMMS_DIR");
186
+ if (!commsDir) return null;
187
+ return { commsDir };
188
+ } catch {
189
+ return null;
190
+ }
38
191
  }
39
192
  function resolveConfig(overrides = {}, startDir) {
40
- const repoRoot = findRepoRoot(startDir);
193
+ const repoRoot = findRepoRoot2(startDir);
41
194
  const shared = loadSharedConfig(repoRoot) ?? {};
42
195
  const local = loadLocalConfig(repoRoot) ?? {};
196
+ const legacy = loadLegacyShellConfig(repoRoot) ?? {};
43
197
  const sources = {
44
198
  repoRoot: "auto",
45
199
  commsDir: "auto",
@@ -49,10 +203,10 @@ function resolveConfig(overrides = {}, startDir) {
49
203
  };
50
204
  let commsDir;
51
205
  if (overrides.commsDir) {
52
- commsDir = path.resolve(overrides.commsDir);
206
+ commsDir = resolvePath(repoRoot, overrides.commsDir);
53
207
  sources.commsDir = "cli-flag";
54
208
  } else if (process.env.TAP_COMMS_DIR) {
55
- commsDir = path.resolve(process.env.TAP_COMMS_DIR);
209
+ commsDir = resolvePath(repoRoot, process.env.TAP_COMMS_DIR);
56
210
  sources.commsDir = "env";
57
211
  } else if (local.commsDir) {
58
212
  commsDir = resolvePath(repoRoot, local.commsDir);
@@ -60,15 +214,18 @@ function resolveConfig(overrides = {}, startDir) {
60
214
  } else if (shared.commsDir) {
61
215
  commsDir = resolvePath(repoRoot, shared.commsDir);
62
216
  sources.commsDir = "shared-config";
217
+ } else if (legacy.commsDir) {
218
+ commsDir = resolvePath(repoRoot, legacy.commsDir);
219
+ sources.commsDir = "legacy-shell-config";
63
220
  } else {
64
- commsDir = path.join(path.dirname(repoRoot), "tap-comms");
221
+ commsDir = path2.join(repoRoot, "tap-comms");
65
222
  }
66
223
  let stateDir;
67
224
  if (overrides.stateDir) {
68
- stateDir = path.resolve(overrides.stateDir);
225
+ stateDir = resolvePath(repoRoot, overrides.stateDir);
69
226
  sources.stateDir = "cli-flag";
70
227
  } else if (process.env.TAP_STATE_DIR) {
71
- stateDir = path.resolve(process.env.TAP_STATE_DIR);
228
+ stateDir = resolvePath(repoRoot, process.env.TAP_STATE_DIR);
72
229
  sources.stateDir = "env";
73
230
  } else if (local.stateDir) {
74
231
  stateDir = resolvePath(repoRoot, local.stateDir);
@@ -77,7 +234,7 @@ function resolveConfig(overrides = {}, startDir) {
77
234
  stateDir = resolvePath(repoRoot, shared.stateDir);
78
235
  sources.stateDir = "shared-config";
79
236
  } else {
80
- stateDir = path.join(repoRoot, ".tap-comms");
237
+ stateDir = path2.join(repoRoot, ".tap-comms");
81
238
  }
82
239
  let runtimeCommand;
83
240
  if (overrides.runtimeCommand) {
@@ -117,33 +274,68 @@ function resolveConfig(overrides = {}, startDir) {
117
274
  };
118
275
  }
119
276
  function saveSharedConfig(repoRoot, config) {
120
- const filePath = path.join(repoRoot, SHARED_CONFIG_FILE);
277
+ const filePath = path2.join(repoRoot, SHARED_CONFIG_FILE);
121
278
  const tmp = `${filePath}.tmp.${process.pid}`;
122
- fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
123
- fs.renameSync(tmp, filePath);
279
+ fs2.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
280
+ fs2.renameSync(tmp, filePath);
124
281
  }
125
282
  function saveLocalConfig(repoRoot, config) {
126
- const filePath = path.join(repoRoot, LOCAL_CONFIG_FILE);
283
+ const filePath = path2.join(repoRoot, LOCAL_CONFIG_FILE);
127
284
  const tmp = `${filePath}.tmp.${process.pid}`;
128
- fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
129
- fs.renameSync(tmp, filePath);
285
+ fs2.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
286
+ fs2.renameSync(tmp, filePath);
130
287
  }
131
288
  function resolvePath(repoRoot, p) {
132
- return path.isAbsolute(p) ? p : path.resolve(repoRoot, p);
289
+ const normalized = normalizeTapPath(p);
290
+ return path2.isAbsolute(normalized) ? normalized : path2.resolve(repoRoot, normalized);
291
+ }
292
+ function normalizeTapPath(input) {
293
+ const trimmed = input.trim().replace(/^["'`]+|["'`]+$/g, "");
294
+ if (/^[A-Za-z]:[\\/]/.test(trimmed)) {
295
+ return trimmed;
296
+ }
297
+ if (process.platform === "win32") {
298
+ const match = trimmed.match(/^\/([A-Za-z])\/(.*)$/);
299
+ if (match) {
300
+ return `${match[1].toUpperCase()}:\\${match[2].replace(/\//g, "\\")}`;
301
+ }
302
+ }
303
+ return trimmed;
133
304
  }
305
+ var SHARED_CONFIG_FILE, LOCAL_CONFIG_FILE, LEGACY_CONFIG_FILE, DEFAULT_RUNTIME_COMMAND, DEFAULT_APP_SERVER_URL;
306
+ var init_resolve = __esm({
307
+ "src/config/resolve.ts"() {
308
+ "use strict";
309
+ init_utils();
310
+ SHARED_CONFIG_FILE = "tap-config.json";
311
+ LOCAL_CONFIG_FILE = "tap-config.local.json";
312
+ LEGACY_CONFIG_FILE = ".tap-config";
313
+ DEFAULT_RUNTIME_COMMAND = "node";
314
+ DEFAULT_APP_SERVER_URL = "ws://127.0.0.1:4501";
315
+ }
316
+ });
317
+
318
+ // src/config/index.ts
319
+ var init_config = __esm({
320
+ "src/config/index.ts"() {
321
+ "use strict";
322
+ init_resolve();
323
+ }
324
+ });
134
325
 
135
326
  // src/state.ts
136
- var STATE_FILE = "state.json";
137
- var SCHEMA_VERSION = 2;
327
+ import * as fs3 from "fs";
328
+ import * as path3 from "path";
329
+ import * as crypto from "crypto";
138
330
  function getStateDir(repoRoot) {
139
331
  const { config } = resolveConfig({}, repoRoot);
140
332
  return config.stateDir;
141
333
  }
142
334
  function getStatePath(repoRoot) {
143
- return path2.join(getStateDir(repoRoot), STATE_FILE);
335
+ return path3.join(getStateDir(repoRoot), STATE_FILE);
144
336
  }
145
337
  function stateExists(repoRoot) {
146
- return fs2.existsSync(getStatePath(repoRoot));
338
+ return fs3.existsSync(getStatePath(repoRoot));
147
339
  }
148
340
  function migrateStateV1toV2(v1) {
149
341
  const instances = {};
@@ -171,8 +363,8 @@ function migrateStateV1toV2(v1) {
171
363
  }
172
364
  function loadState(repoRoot) {
173
365
  const statePath = getStatePath(repoRoot);
174
- if (!fs2.existsSync(statePath)) return null;
175
- const raw = fs2.readFileSync(statePath, "utf-8");
366
+ if (!fs3.existsSync(statePath)) return null;
367
+ const raw = fs3.readFileSync(statePath, "utf-8");
176
368
  const parsed = JSON.parse(raw);
177
369
  if (parsed.schemaVersion === 1 || parsed.runtimes) {
178
370
  const migrated = migrateStateV1toV2(parsed);
@@ -183,11 +375,11 @@ function loadState(repoRoot) {
183
375
  }
184
376
  function saveState(repoRoot, state) {
185
377
  const stateDir = getStateDir(repoRoot);
186
- fs2.mkdirSync(stateDir, { recursive: true });
378
+ fs3.mkdirSync(stateDir, { recursive: true });
187
379
  const statePath = getStatePath(repoRoot);
188
380
  const tmp = `${statePath}.tmp.${process.pid}`;
189
- fs2.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
190
- fs2.renameSync(tmp, statePath);
381
+ fs3.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
382
+ fs3.renameSync(tmp, statePath);
191
383
  }
192
384
  function createInitialState(commsDir, repoRoot, packageVersion) {
193
385
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -195,30 +387,197 @@ function createInitialState(commsDir, repoRoot, packageVersion) {
195
387
  schemaVersion: SCHEMA_VERSION,
196
388
  createdAt: now,
197
389
  updatedAt: now,
198
- commsDir: path2.resolve(commsDir),
199
- repoRoot: path2.resolve(repoRoot),
390
+ commsDir: path3.resolve(commsDir),
391
+ repoRoot: path3.resolve(repoRoot),
200
392
  packageVersion,
201
393
  instances: {}
202
394
  };
203
395
  }
396
+ function updateInstanceState(state, instanceId, instanceState) {
397
+ return {
398
+ ...state,
399
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
400
+ instances: {
401
+ ...state.instances,
402
+ [instanceId]: instanceState
403
+ }
404
+ };
405
+ }
406
+ function ensureBackupDir(stateDir, instanceId) {
407
+ const backupDir = path3.join(stateDir, "backups", instanceId);
408
+ fs3.mkdirSync(backupDir, { recursive: true });
409
+ return backupDir;
410
+ }
411
+ function backupFile(filePath, backupDir) {
412
+ const basename2 = path3.basename(filePath);
413
+ const hash = fileHash(filePath);
414
+ const backupPath = path3.join(backupDir, `${basename2}.${hash}.bak`);
415
+ fs3.copyFileSync(filePath, backupPath);
416
+ return backupPath;
417
+ }
418
+ function fileHash(filePath) {
419
+ if (!fs3.existsSync(filePath)) return "";
420
+ const content = fs3.readFileSync(filePath);
421
+ return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
422
+ }
423
+ var STATE_FILE, SCHEMA_VERSION;
424
+ var init_state = __esm({
425
+ "src/state.ts"() {
426
+ "use strict";
427
+ init_config();
428
+ STATE_FILE = "state.json";
429
+ SCHEMA_VERSION = 2;
430
+ }
431
+ });
204
432
 
205
- // src/version.ts
206
- var version = "0.1.0";
207
-
208
- // src/engine/bridge.ts
209
- import * as fs4 from "fs";
210
- import * as path4 from "path";
211
- import { spawn, execSync as execSync2 } from "child_process";
433
+ // src/adapters/common.ts
434
+ import * as fs5 from "fs";
435
+ import * as os from "os";
436
+ import * as path5 from "path";
437
+ import { spawnSync } from "child_process";
438
+ import { fileURLToPath as fileURLToPath2 } from "url";
439
+ function probeCommand(candidates) {
440
+ for (const candidate of candidates) {
441
+ const result = spawnSync(candidate, ["--version"], {
442
+ encoding: "utf-8",
443
+ shell: process.platform === "win32"
444
+ });
445
+ if (result.status === 0) {
446
+ const version2 = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim() || null;
447
+ return { command: candidate, version: version2 };
448
+ }
449
+ }
450
+ return { command: null, version: null };
451
+ }
452
+ function getHomeDir() {
453
+ return os.homedir();
454
+ }
455
+ function toForwardSlashPath(filePath) {
456
+ return path5.resolve(filePath).replace(/\\/g, "/");
457
+ }
458
+ function canWriteOrCreate(filePath) {
459
+ try {
460
+ if (fs5.existsSync(filePath)) {
461
+ fs5.accessSync(filePath, fs5.constants.W_OK);
462
+ return true;
463
+ }
464
+ const parent = path5.dirname(filePath);
465
+ fs5.mkdirSync(parent, { recursive: true });
466
+ fs5.accessSync(parent, fs5.constants.W_OK);
467
+ return true;
468
+ } catch {
469
+ return false;
470
+ }
471
+ }
472
+ function findLocalTapCommsSource(ctx) {
473
+ const candidates = [
474
+ path5.join(
475
+ ctx.repoRoot,
476
+ "packages",
477
+ "tap-plugin",
478
+ "channels",
479
+ "tap-comms.ts"
480
+ ),
481
+ path5.join(
482
+ ctx.repoRoot,
483
+ "node_modules",
484
+ "@hua-labs",
485
+ "tap-plugin",
486
+ "channels",
487
+ "tap-comms.ts"
488
+ )
489
+ ];
490
+ for (const candidate of candidates) {
491
+ if (fs5.existsSync(candidate)) return candidate;
492
+ }
493
+ return null;
494
+ }
495
+ function findBundledTapCommsSource(metaUrl = import.meta.url) {
496
+ const moduleDir = path5.dirname(fileURLToPath2(metaUrl));
497
+ const candidates = [
498
+ path5.join(moduleDir, "mcp-server.mjs"),
499
+ path5.join(moduleDir, "..", "mcp-server.mjs"),
500
+ path5.join(moduleDir, "..", "mcp-server.ts")
501
+ ];
502
+ for (const candidate of candidates) {
503
+ if (fs5.existsSync(candidate)) return candidate;
504
+ }
505
+ return null;
506
+ }
507
+ function findTapCommsServerEntry(ctx, metaUrl = import.meta.url) {
508
+ return findBundledTapCommsSource(metaUrl) ?? findLocalTapCommsSource(ctx);
509
+ }
510
+ function findPreferredBunCommand() {
511
+ const home = getHomeDir();
512
+ const candidates = process.platform === "win32" ? [path5.join(home, ".bun", "bin", "bun.exe"), "bun", "bun.cmd"] : [path5.join(home, ".bun", "bin", "bun"), "bun"];
513
+ for (const candidate of candidates) {
514
+ if (path5.isAbsolute(candidate) && !fs5.existsSync(candidate)) continue;
515
+ const result = spawnSync(candidate, ["--version"], {
516
+ encoding: "utf-8",
517
+ shell: process.platform === "win32"
518
+ });
519
+ if (result.status === 0) {
520
+ return path5.isAbsolute(candidate) ? toForwardSlashPath(candidate) : candidate;
521
+ }
522
+ }
523
+ return null;
524
+ }
525
+ function buildManagedMcpServerSpec(ctx, instanceId) {
526
+ const sourcePath = findTapCommsServerEntry(ctx);
527
+ const bunCommand = findPreferredBunCommand();
528
+ const warnings = [];
529
+ const issues = [];
530
+ const env = {
531
+ TAP_AGENT_NAME: "<set-per-session>",
532
+ TAP_COMMS_DIR: toForwardSlashPath(ctx.commsDir)
533
+ };
534
+ if (instanceId) {
535
+ env.TAP_AGENT_ID = instanceId;
536
+ }
537
+ if (!sourcePath) {
538
+ issues.push(
539
+ "tap-comms MCP server entry not found. Reinstall @hua-labs/tap or run from a repo with packages/tap-plugin/channels/ available."
540
+ );
541
+ return { command: null, args: [], env, sourcePath, warnings, issues };
542
+ }
543
+ const isBundled = sourcePath.endsWith(".mjs");
544
+ let command = bunCommand;
545
+ if (!command && isBundled) {
546
+ command = process.execPath;
547
+ warnings.push(
548
+ "bun not found; using node to run the compiled MCP server. Install bun for better performance."
549
+ );
550
+ }
551
+ if (!command) {
552
+ issues.push(
553
+ "bun is required to run the repo-local tap-comms MCP server (.ts source). Install bun: https://bun.sh"
554
+ );
555
+ return { command: null, args: [], env, sourcePath, warnings, issues };
556
+ }
557
+ return {
558
+ command: isBundled && command === process.execPath ? toForwardSlashPath(command) : command,
559
+ args: [toForwardSlashPath(sourcePath)],
560
+ env,
561
+ sourcePath,
562
+ warnings,
563
+ issues
564
+ };
565
+ }
566
+ var init_common = __esm({
567
+ "src/adapters/common.ts"() {
568
+ "use strict";
569
+ }
570
+ });
212
571
 
213
572
  // src/runtime/resolve-node.ts
214
- import * as fs3 from "fs";
215
- import * as path3 from "path";
573
+ import * as fs6 from "fs";
574
+ import * as path6 from "path";
216
575
  import { execSync } from "child_process";
217
576
  function readNodeVersion(repoRoot) {
218
- const nvFile = path3.join(repoRoot, ".node-version");
219
- if (!fs3.existsSync(nvFile)) return null;
577
+ const nvFile = path6.join(repoRoot, ".node-version");
578
+ if (!fs6.existsSync(nvFile)) return null;
220
579
  try {
221
- const raw = fs3.readFileSync(nvFile, "utf-8").trim();
580
+ const raw = fs6.readFileSync(nvFile, "utf-8").trim();
222
581
  return raw.length > 0 ? raw.replace(/^v/, "") : null;
223
582
  } catch {
224
583
  return null;
@@ -228,16 +587,16 @@ function fnmCandidateDirs() {
228
587
  if (process.platform === "win32") {
229
588
  return [
230
589
  process.env.FNM_DIR,
231
- process.env.APPDATA ? path3.join(process.env.APPDATA, "fnm") : null,
232
- process.env.LOCALAPPDATA ? path3.join(process.env.LOCALAPPDATA, "fnm") : null,
233
- process.env.USERPROFILE ? path3.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
590
+ process.env.APPDATA ? path6.join(process.env.APPDATA, "fnm") : null,
591
+ process.env.LOCALAPPDATA ? path6.join(process.env.LOCALAPPDATA, "fnm") : null,
592
+ process.env.USERPROFILE ? path6.join(process.env.USERPROFILE, "scoop", "persist", "fnm") : null
234
593
  ].filter(Boolean);
235
594
  }
236
595
  return [
237
596
  process.env.FNM_DIR,
238
- process.env.HOME ? path3.join(process.env.HOME, ".local", "share", "fnm") : null,
239
- process.env.HOME ? path3.join(process.env.HOME, ".fnm") : null,
240
- process.env.XDG_DATA_HOME ? path3.join(process.env.XDG_DATA_HOME, "fnm") : null
597
+ process.env.HOME ? path6.join(process.env.HOME, ".local", "share", "fnm") : null,
598
+ process.env.HOME ? path6.join(process.env.HOME, ".fnm") : null,
599
+ process.env.XDG_DATA_HOME ? path6.join(process.env.XDG_DATA_HOME, "fnm") : null
241
600
  ].filter(Boolean);
242
601
  }
243
602
  function nodeExecutableName() {
@@ -247,14 +606,14 @@ function probeFnmNode(desiredVersion) {
247
606
  const dirs = fnmCandidateDirs();
248
607
  const exe = nodeExecutableName();
249
608
  for (const baseDir of dirs) {
250
- const candidate = path3.join(
609
+ const candidate = path6.join(
251
610
  baseDir,
252
611
  "node-versions",
253
612
  `v${desiredVersion}`,
254
613
  "installation",
255
614
  exe
256
615
  );
257
- if (!fs3.existsSync(candidate)) continue;
616
+ if (!fs6.existsSync(candidate)) continue;
258
617
  try {
259
618
  const v = execSync(`"${candidate}" --version`, {
260
619
  encoding: "utf-8",
@@ -295,12 +654,12 @@ function checkStripTypesSupport(command) {
295
654
  }
296
655
  function findTsxFallback(repoRoot) {
297
656
  const candidates = [
298
- path3.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
299
- path3.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
300
- path3.join(repoRoot, "node_modules", ".bin", "tsx")
657
+ path6.join(repoRoot, "node_modules", ".bin", "tsx.exe"),
658
+ path6.join(repoRoot, "node_modules", ".bin", "tsx.CMD"),
659
+ path6.join(repoRoot, "node_modules", ".bin", "tsx")
301
660
  ];
302
661
  for (const c of candidates) {
303
- if (fs3.existsSync(c)) return c;
662
+ if (fs6.existsSync(c)) return c;
304
663
  }
305
664
  return null;
306
665
  }
@@ -309,7 +668,7 @@ function getFnmBinDir(repoRoot) {
309
668
  if (!desiredVersion) return null;
310
669
  const nodePath = probeFnmNode(desiredVersion);
311
670
  if (!nodePath) return null;
312
- return path3.dirname(nodePath);
671
+ return path6.dirname(nodePath);
313
672
  }
314
673
  function resolveNodeRuntime(configCommand, repoRoot) {
315
674
  if (configCommand === "bun" || configCommand.endsWith("bun.exe")) {
@@ -365,60 +724,3473 @@ function buildRuntimeEnv(repoRoot, baseEnv = process.env) {
365
724
  const currentPath = baseEnv[pathKey] ?? baseEnv.PATH ?? "";
366
725
  return {
367
726
  ...baseEnv,
368
- [pathKey]: `${fnmBin}${path3.delimiter}${currentPath}`
727
+ [pathKey]: `${fnmBin}${path6.delimiter}${currentPath}`
369
728
  };
370
729
  }
730
+ var init_resolve_node = __esm({
731
+ "src/runtime/resolve-node.ts"() {
732
+ "use strict";
733
+ }
734
+ });
735
+
736
+ // src/runtime/index.ts
737
+ var init_runtime = __esm({
738
+ "src/runtime/index.ts"() {
739
+ "use strict";
740
+ init_resolve_node();
741
+ }
742
+ });
371
743
 
372
744
  // src/engine/bridge.ts
373
- function pidFilePath(stateDir, instanceId) {
374
- return path4.join(stateDir, "pids", `bridge-${instanceId}.json`);
745
+ import * as fs7 from "fs";
746
+ import * as net from "net";
747
+ import * as path7 from "path";
748
+ import { randomBytes } from "crypto";
749
+ import { spawn, spawnSync as spawnSync2, execSync as execSync2 } from "child_process";
750
+ import { fileURLToPath as fileURLToPath3 } from "url";
751
+ function appServerLogFilePath(stateDir, instanceId) {
752
+ return path7.join(stateDir, "logs", `app-server-${instanceId}.log`);
375
753
  }
376
- function loadBridgeState(stateDir, instanceId) {
377
- const pidPath = pidFilePath(stateDir, instanceId);
378
- if (!fs4.existsSync(pidPath)) return null;
754
+ function appServerGatewayLogFilePath(stateDir, instanceId) {
755
+ return path7.join(stateDir, "logs", `app-server-gateway-${instanceId}.log`);
756
+ }
757
+ function appServerGatewayTokenFilePath(stateDir, instanceId) {
758
+ return path7.join(
759
+ stateDir,
760
+ "secrets",
761
+ `app-server-gateway-${instanceId}.token`
762
+ );
763
+ }
764
+ function stderrLogFilePath(logPath) {
765
+ return `${logPath}.stderr`;
766
+ }
767
+ function writeProtectedTextFile(filePath, content) {
768
+ fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
769
+ const tmp = `${filePath}.tmp.${process.pid}`;
770
+ fs7.writeFileSync(tmp, content, {
771
+ encoding: "utf-8",
772
+ mode: APP_SERVER_AUTH_FILE_MODE
773
+ });
774
+ fs7.chmodSync(tmp, APP_SERVER_AUTH_FILE_MODE);
775
+ fs7.renameSync(tmp, filePath);
776
+ fs7.chmodSync(filePath, APP_SERVER_AUTH_FILE_MODE);
777
+ }
778
+ function removeFileIfExists(filePath) {
779
+ if (!filePath || !fs7.existsSync(filePath)) {
780
+ return;
781
+ }
379
782
  try {
380
- const raw = fs4.readFileSync(pidPath, "utf-8");
381
- return JSON.parse(raw);
783
+ fs7.unlinkSync(filePath);
382
784
  } catch {
785
+ }
786
+ }
787
+ function getWebSocketCtor() {
788
+ const candidate = globalThis.WebSocket;
789
+ return typeof candidate === "function" ? candidate : null;
790
+ }
791
+ function delay(ms) {
792
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
793
+ }
794
+ function isLoopbackHost(hostname) {
795
+ return hostname === "127.0.0.1" || hostname === "localhost";
796
+ }
797
+ function resolveCodexCommand(platform) {
798
+ const candidates = platform === "win32" ? ["codex.cmd", "codex.exe", "codex", "codex.ps1"] : ["codex"];
799
+ return probeCommand(candidates).command;
800
+ }
801
+ function formatCodexAppServerCommand(command, url) {
802
+ return `${command} app-server --listen ${url}`;
803
+ }
804
+ function resolvePowerShellCommand() {
805
+ return probeCommand(["pwsh", "powershell", "powershell.exe"]).command ?? "powershell";
806
+ }
807
+ function resolveAuthGatewayScript(repoRoot) {
808
+ const moduleDir = path7.dirname(fileURLToPath3(import.meta.url));
809
+ const candidates = [
810
+ path7.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.mjs"),
811
+ path7.join(moduleDir, "..", "bridges", "codex-app-server-auth-gateway.ts"),
812
+ path7.join(
813
+ repoRoot,
814
+ "packages",
815
+ "tap-comms",
816
+ "dist",
817
+ "bridges",
818
+ "codex-app-server-auth-gateway.mjs"
819
+ ),
820
+ path7.join(
821
+ repoRoot,
822
+ "packages",
823
+ "tap-comms",
824
+ "src",
825
+ "bridges",
826
+ "codex-app-server-auth-gateway.ts"
827
+ )
828
+ ];
829
+ for (const candidate of candidates) {
830
+ if (fs7.existsSync(candidate)) {
831
+ return candidate;
832
+ }
833
+ }
834
+ return null;
835
+ }
836
+ function getBridgeRuntimeStateDir(repoRoot, instanceId) {
837
+ return path7.join(repoRoot, ".tmp", `codex-app-server-bridge-${instanceId}`);
838
+ }
839
+ async function allocateLoopbackPort(hostname) {
840
+ const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
841
+ return await new Promise((resolve8, reject) => {
842
+ const server = net.createServer();
843
+ server.unref();
844
+ server.once("error", reject);
845
+ server.listen(0, bindHost, () => {
846
+ const address = server.address();
847
+ if (!address || typeof address === "string") {
848
+ server.close(() => {
849
+ reject(new Error("Failed to allocate a loopback port"));
850
+ });
851
+ return;
852
+ }
853
+ const port = address.port;
854
+ server.close((error) => {
855
+ if (error) {
856
+ reject(error);
857
+ return;
858
+ }
859
+ resolve8(port);
860
+ });
861
+ });
862
+ });
863
+ }
864
+ function buildProtectedAppServerUrl(publicUrl, token) {
865
+ const url = new URL(publicUrl);
866
+ url.searchParams.set(APP_SERVER_AUTH_QUERY_PARAM, token);
867
+ return url.toString().replace(/\/(?=\?|$)/, "");
868
+ }
869
+ function readGatewayTokenFromPath(tokenPath) {
870
+ return fs7.readFileSync(tokenPath, "utf8").trim();
871
+ }
872
+ function readGatewayToken(auth) {
873
+ if (!auth) {
383
874
  return null;
384
875
  }
876
+ const legacyToken = auth.token;
877
+ if (legacyToken?.trim()) {
878
+ return legacyToken.trim();
879
+ }
880
+ if (!auth.tokenPath || !fs7.existsSync(auth.tokenPath)) {
881
+ return null;
882
+ }
883
+ const fileToken = readGatewayTokenFromPath(auth.tokenPath);
884
+ return fileToken || null;
385
885
  }
386
- function saveBridgeState(stateDir, instanceId, state) {
387
- const pidPath = pidFilePath(stateDir, instanceId);
388
- fs4.mkdirSync(path4.dirname(pidPath), { recursive: true });
389
- const tmp = `${pidPath}.tmp.${process.pid}`;
390
- fs4.writeFileSync(tmp, JSON.stringify(state, null, 2), "utf-8");
391
- fs4.renameSync(tmp, pidPath);
886
+ function materializeGatewayTokenFile(stateDir, instanceId, publicUrl, auth) {
887
+ if (auth.tokenPath && fs7.existsSync(auth.tokenPath)) {
888
+ return auth;
889
+ }
890
+ const token = readGatewayToken(auth);
891
+ if (!token) {
892
+ throw new Error(`Missing auth gateway token for ${instanceId}`);
893
+ }
894
+ const tokenPath = appServerGatewayTokenFilePath(stateDir, instanceId);
895
+ writeProtectedTextFile(tokenPath, `${token}
896
+ `);
897
+ return {
898
+ ...auth,
899
+ protectedUrl: buildProtectedAppServerUrl(publicUrl, "***"),
900
+ tokenPath
901
+ };
392
902
  }
393
- function rotateLog(logPath) {
394
- if (!fs4.existsSync(logPath)) return;
903
+ async function createManagedAppServerAuth(options) {
904
+ const publicUrl = new URL(options.publicUrl);
905
+ const upstreamUrl = new URL(options.publicUrl);
906
+ upstreamUrl.port = String(await allocateLoopbackPort(publicUrl.hostname));
907
+ upstreamUrl.search = "";
908
+ upstreamUrl.hash = "";
909
+ const gatewayScript = resolveAuthGatewayScript(options.repoRoot);
910
+ if (!gatewayScript) {
911
+ throw new Error("Auth gateway script not found");
912
+ }
913
+ const token = randomBytes(24).toString("base64url");
914
+ const tokenPath = appServerGatewayTokenFilePath(
915
+ options.stateDir,
916
+ options.instanceId
917
+ );
918
+ writeProtectedTextFile(tokenPath, `${token}
919
+ `);
920
+ const protectedUrl = buildProtectedAppServerUrl(options.publicUrl, "***");
921
+ const gatewayLogPath = appServerGatewayLogFilePath(
922
+ options.stateDir,
923
+ options.instanceId
924
+ );
925
+ fs7.mkdirSync(path7.dirname(gatewayLogPath), { recursive: true });
926
+ rotateLog(gatewayLogPath);
927
+ const runtime = resolveNodeRuntime(process.execPath, options.repoRoot);
928
+ const gatewayArgs = [];
929
+ if (gatewayScript.endsWith(".ts")) {
930
+ if (!runtime.supportsStripTypes) {
931
+ throw new Error(
932
+ "Current Node runtime cannot start the auth gateway from TypeScript source. Rebuild @hua-labs/tap or use Node 22.6+."
933
+ );
934
+ }
935
+ gatewayArgs.push("--experimental-strip-types");
936
+ }
937
+ gatewayArgs.push(gatewayScript);
938
+ const gatewayEnv = {
939
+ ...buildRuntimeEnv(options.repoRoot),
940
+ TAP_GATEWAY_LISTEN_URL: options.publicUrl,
941
+ TAP_GATEWAY_UPSTREAM_URL: upstreamUrl.toString().replace(/\/$/, ""),
942
+ TAP_GATEWAY_TOKEN_FILE: tokenPath
943
+ };
944
+ let gatewayPid;
945
+ {
946
+ let logFd = null;
947
+ try {
948
+ if (options.platform === "win32") {
949
+ gatewayPid = startWindowsDetachedProcess(
950
+ runtime.command,
951
+ gatewayArgs,
952
+ options.repoRoot,
953
+ gatewayLogPath,
954
+ gatewayEnv
955
+ );
956
+ } else {
957
+ logFd = fs7.openSync(gatewayLogPath, "a");
958
+ const child = spawn(runtime.command, gatewayArgs, {
959
+ cwd: options.repoRoot,
960
+ detached: true,
961
+ stdio: ["ignore", logFd, logFd],
962
+ env: gatewayEnv,
963
+ windowsHide: true
964
+ });
965
+ child.unref();
966
+ gatewayPid = child.pid ?? null;
967
+ }
968
+ } catch (error) {
969
+ removeFileIfExists(tokenPath);
970
+ throw error;
971
+ } finally {
972
+ if (logFd != null) {
973
+ fs7.closeSync(logFd);
974
+ }
975
+ }
976
+ }
977
+ if (gatewayPid == null) {
978
+ removeFileIfExists(tokenPath);
979
+ throw new Error("Failed to spawn app-server auth gateway");
980
+ }
981
+ return {
982
+ mode: "query-token",
983
+ protectedUrl,
984
+ upstreamUrl: upstreamUrl.toString().replace(/\/$/, ""),
985
+ tokenPath,
986
+ gatewayPid,
987
+ gatewayLogPath
988
+ };
989
+ }
990
+ function canReuseManagedAppServer(appServer) {
991
+ if (!appServer?.managed) {
992
+ return false;
993
+ }
994
+ if (appServer.pid != null && !isProcessAlive(appServer.pid)) {
995
+ return false;
996
+ }
997
+ const auth = appServer.auth;
998
+ if (auth) {
999
+ if (!auth.protectedUrl) {
1000
+ return false;
1001
+ }
1002
+ if (!readGatewayToken(auth)) {
1003
+ return false;
1004
+ }
1005
+ if (auth.gatewayPid != null && !isProcessAlive(auth.gatewayPid)) {
1006
+ return false;
1007
+ }
1008
+ }
1009
+ return true;
1010
+ }
1011
+ function markAppServerHealthy(appServer) {
1012
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
1013
+ return {
1014
+ ...appServer,
1015
+ healthy: true,
1016
+ lastCheckedAt: checkedAt,
1017
+ lastHealthyAt: checkedAt
1018
+ };
1019
+ }
1020
+ function findReusableManagedAppServer(stateDir, publicUrl) {
1021
+ const pidDir = path7.join(stateDir, "pids");
1022
+ if (!fs7.existsSync(pidDir)) {
1023
+ return null;
1024
+ }
1025
+ for (const name of fs7.readdirSync(pidDir)) {
1026
+ if (!name.startsWith("bridge-") || !name.endsWith(".json")) {
1027
+ continue;
1028
+ }
1029
+ try {
1030
+ const raw = fs7.readFileSync(path7.join(pidDir, name), "utf-8");
1031
+ const parsed = JSON.parse(raw);
1032
+ if (parsed.appServer?.url !== publicUrl) {
1033
+ continue;
1034
+ }
1035
+ if (canReuseManagedAppServer(parsed.appServer)) {
1036
+ return markAppServerHealthy(parsed.appServer);
1037
+ }
1038
+ } catch {
1039
+ }
1040
+ }
1041
+ return null;
1042
+ }
1043
+ function startWindowsDetachedProcess(command, args, repoRoot, logPath, env = process.env) {
1044
+ const ext = path7.extname(command).toLowerCase();
1045
+ const stderrLogPath = stderrLogFilePath(logPath);
1046
+ const stdoutFd = fs7.openSync(logPath, "a");
1047
+ const stderrFd = fs7.openSync(stderrLogPath, "a");
395
1048
  try {
396
- const stats = fs4.statSync(logPath);
397
- if (stats.size === 0) return;
398
- const prevPath = `${logPath}.prev`;
399
- fs4.renameSync(logPath, prevPath);
1049
+ const child = ext === ".ps1" ? spawn(
1050
+ resolvePowerShellCommand(),
1051
+ ["-NoLogo", "-NoProfile", "-File", command, ...args],
1052
+ {
1053
+ cwd: repoRoot,
1054
+ detached: true,
1055
+ stdio: ["ignore", stdoutFd, stderrFd],
1056
+ env,
1057
+ windowsHide: true
1058
+ }
1059
+ ) : spawn(command, args, {
1060
+ cwd: repoRoot,
1061
+ detached: true,
1062
+ stdio: ["ignore", stdoutFd, stderrFd],
1063
+ env,
1064
+ windowsHide: true,
1065
+ shell: ext === ".cmd" || ext === ".bat"
1066
+ });
1067
+ child.unref();
1068
+ return child.pid ?? null;
1069
+ } finally {
1070
+ fs7.closeSync(stdoutFd);
1071
+ fs7.closeSync(stderrFd);
1072
+ }
1073
+ }
1074
+ function startWindowsCodexAppServer(command, url, repoRoot, logPath) {
1075
+ return startWindowsDetachedProcess(
1076
+ command,
1077
+ ["app-server", "--listen", url],
1078
+ repoRoot,
1079
+ logPath
1080
+ );
1081
+ }
1082
+ function findListeningProcessId(url, platform) {
1083
+ if (platform !== "win32") {
1084
+ return null;
1085
+ }
1086
+ let port;
1087
+ try {
1088
+ const parsed = new URL(url);
1089
+ port = parsed.port ? Number.parseInt(parsed.port, 10) : null;
400
1090
  } catch {
1091
+ return null;
1092
+ }
1093
+ if (port == null || !Number.isFinite(port)) {
1094
+ return null;
1095
+ }
1096
+ const result = spawnSync2(
1097
+ resolvePowerShellCommand(),
1098
+ [
1099
+ "-NoLogo",
1100
+ "-NoProfile",
1101
+ "-Command",
1102
+ [
1103
+ `$port = ${port}`,
1104
+ "$processId = Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty OwningProcess",
1105
+ "if ($processId) { $processId }"
1106
+ ].join("; ")
1107
+ ],
1108
+ {
1109
+ encoding: "utf-8",
1110
+ windowsHide: true
1111
+ }
1112
+ );
1113
+ if (result.status !== 0) {
1114
+ return null;
401
1115
  }
1116
+ const parsedPid = Number.parseInt((result.stdout ?? "").trim(), 10);
1117
+ return Number.isFinite(parsedPid) ? parsedPid : null;
402
1118
  }
403
- function updateBridgeHeartbeat(stateDir, instanceId) {
404
- const state = loadBridgeState(stateDir, instanceId);
405
- if (!state) return;
406
- if (state.pid !== process.pid) return;
407
- state.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
408
- saveBridgeState(stateDir, instanceId, state);
1119
+ function resolveAppServerUrl(baseUrl, port) {
1120
+ const resolvedBase = (baseUrl ?? DEFAULT_APP_SERVER_URL2).replace(/\/$/, "");
1121
+ if (port == null) {
1122
+ return resolvedBase;
1123
+ }
1124
+ try {
1125
+ const parsed = new URL(resolvedBase);
1126
+ parsed.port = String(port);
1127
+ return parsed.toString().replace(/\/$/, "");
1128
+ } catch {
1129
+ return resolvedBase;
1130
+ }
409
1131
  }
410
- function getHeartbeatAge(stateDir, instanceId) {
411
- const state = loadBridgeState(stateDir, instanceId);
412
- if (!state?.lastHeartbeat) return null;
413
- const heartbeatTime = new Date(state.lastHeartbeat).getTime();
414
- if (isNaN(heartbeatTime)) return null;
415
- return Math.floor((Date.now() - heartbeatTime) / 1e3);
1132
+ async function isTcpPortAvailable(hostname, port) {
1133
+ const bindHost = hostname === "localhost" ? "127.0.0.1" : hostname;
1134
+ return await new Promise((resolve8) => {
1135
+ const server = net.createServer();
1136
+ server.unref();
1137
+ server.once("error", () => resolve8(false));
1138
+ server.listen(port, bindHost, () => {
1139
+ server.close((error) => resolve8(!error));
1140
+ });
1141
+ });
1142
+ }
1143
+ async function findNextAvailableAppServerPort(state, baseUrl, basePort = 4501, excludeInstanceId) {
1144
+ let hostname = "127.0.0.1";
1145
+ try {
1146
+ hostname = new URL(baseUrl ?? DEFAULT_APP_SERVER_URL2).hostname;
1147
+ } catch {
1148
+ }
1149
+ const maxAttempts = 1e3;
1150
+ let port = basePort;
1151
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1, port += 1) {
1152
+ const claimedInState = Object.entries(state.instances).some(
1153
+ ([id, inst]) => id !== excludeInstanceId && inst.port === port
1154
+ );
1155
+ if (claimedInState) {
1156
+ continue;
1157
+ }
1158
+ if (!isLoopbackHost(hostname)) {
1159
+ return port;
1160
+ }
1161
+ if (await isTcpPortAvailable(hostname, port)) {
1162
+ return port;
1163
+ }
1164
+ }
1165
+ throw new Error(
1166
+ `Failed to find a free app-server port starting at ${basePort}`
1167
+ );
1168
+ }
1169
+ async function checkAppServerHealth(url, timeoutMs = APP_SERVER_HEALTH_TIMEOUT_MS) {
1170
+ const WebSocket = getWebSocketCtor();
1171
+ if (!WebSocket) {
1172
+ return false;
1173
+ }
1174
+ return new Promise((resolve8) => {
1175
+ let settled = false;
1176
+ let socket = null;
1177
+ const finish = (healthy) => {
1178
+ if (settled) {
1179
+ return;
1180
+ }
1181
+ settled = true;
1182
+ clearTimeout(timer);
1183
+ try {
1184
+ socket?.close();
1185
+ } catch {
1186
+ }
1187
+ resolve8(healthy);
1188
+ };
1189
+ const timer = setTimeout(() => finish(false), timeoutMs);
1190
+ try {
1191
+ socket = new WebSocket(url);
1192
+ socket.addEventListener("open", () => finish(true), { once: true });
1193
+ socket.addEventListener("error", () => finish(false), { once: true });
1194
+ socket.addEventListener("close", () => finish(false), { once: true });
1195
+ } catch {
1196
+ finish(false);
1197
+ }
1198
+ });
416
1199
  }
1200
+ async function waitForAppServerHealth(url, timeoutMs) {
1201
+ const deadline = Date.now() + timeoutMs;
1202
+ while (Date.now() < deadline) {
1203
+ if (await checkAppServerHealth(url)) {
1204
+ return true;
1205
+ }
1206
+ await delay(APP_SERVER_HEALTH_RETRY_MS);
1207
+ }
1208
+ return false;
1209
+ }
1210
+ async function terminateProcess(pid, platform) {
1211
+ if (!isProcessAlive(pid)) {
1212
+ return false;
1213
+ }
1214
+ try {
1215
+ if (platform === "win32") {
1216
+ execSync2(`taskkill /PID ${pid} /F /T`, { stdio: "pipe" });
1217
+ } else {
1218
+ process.kill(pid, "SIGTERM");
1219
+ await delay(2e3);
1220
+ if (isProcessAlive(pid)) {
1221
+ process.kill(pid, "SIGKILL");
1222
+ }
1223
+ }
1224
+ } catch {
1225
+ }
1226
+ return !isProcessAlive(pid);
1227
+ }
1228
+ async function stopManagedAppServer(appServer, platform) {
1229
+ if (!appServer.managed) {
1230
+ return false;
1231
+ }
1232
+ let stopped = false;
1233
+ if (appServer.auth?.gatewayPid != null) {
1234
+ stopped = await terminateProcess(appServer.auth.gatewayPid, platform) || stopped;
1235
+ }
1236
+ if (appServer.pid != null) {
1237
+ stopped = await terminateProcess(appServer.pid, platform) || stopped;
1238
+ }
1239
+ removeFileIfExists(appServer.auth?.tokenPath);
1240
+ return stopped;
1241
+ }
1242
+ async function ensureCodexAppServer(options) {
1243
+ const effectiveUrl = resolveAppServerUrl(options.appServerUrl);
1244
+ const fallbackManualCommand = formatCodexAppServerCommand(
1245
+ "codex",
1246
+ effectiveUrl
1247
+ );
1248
+ if (options.existingAppServer?.url === effectiveUrl && canReuseManagedAppServer(options.existingAppServer)) {
1249
+ return markAppServerHealthy(options.existingAppServer);
1250
+ }
1251
+ const sharedManaged = findReusableManagedAppServer(
1252
+ options.stateDir,
1253
+ effectiveUrl
1254
+ );
1255
+ if (sharedManaged) {
1256
+ return sharedManaged;
1257
+ }
1258
+ let parsedUrl;
1259
+ try {
1260
+ parsedUrl = new URL(effectiveUrl);
1261
+ } catch {
1262
+ throw new Error(
1263
+ `Invalid app-server URL: ${effectiveUrl}
1264
+ Start it manually:
1265
+ ${fallbackManualCommand}`
1266
+ );
1267
+ }
1268
+ if (!isLoopbackHost(parsedUrl.hostname)) {
1269
+ throw new Error(
1270
+ `Auto-start only supports loopback app-server URLs. Current URL: ${effectiveUrl}
1271
+ Start it manually:
1272
+ ${fallbackManualCommand}`
1273
+ );
1274
+ }
1275
+ if (await checkAppServerHealth(effectiveUrl)) {
1276
+ 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.";
1277
+ throw new Error(`${effectiveUrl}: ${hint}`);
1278
+ }
1279
+ const resolvedCommand = resolveCodexCommand(options.platform);
1280
+ if (!resolvedCommand) {
1281
+ throw new Error(
1282
+ `Codex CLI not found in PATH.
1283
+ Start the app-server manually:
1284
+ ${fallbackManualCommand}`
1285
+ );
1286
+ }
1287
+ const logPath = appServerLogFilePath(options.stateDir, options.instanceId);
1288
+ fs7.mkdirSync(path7.dirname(logPath), { recursive: true });
1289
+ rotateLog(logPath);
1290
+ if (options.noAuth) {
1291
+ const manualCommand2 = formatCodexAppServerCommand("codex", effectiveUrl);
1292
+ let pid2;
1293
+ if (options.platform === "win32") {
1294
+ try {
1295
+ pid2 = startWindowsCodexAppServer(
1296
+ resolvedCommand,
1297
+ effectiveUrl,
1298
+ options.repoRoot,
1299
+ logPath
1300
+ );
1301
+ } catch (err) {
1302
+ throw new Error(
1303
+ `Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
1304
+ Start it manually:
1305
+ ${manualCommand2}`,
1306
+ { cause: err }
1307
+ );
1308
+ }
1309
+ } else {
1310
+ const logFd = fs7.openSync(logPath, "a");
1311
+ try {
1312
+ const child = spawn(
1313
+ resolvedCommand,
1314
+ ["app-server", "--listen", effectiveUrl],
1315
+ {
1316
+ cwd: options.repoRoot,
1317
+ detached: true,
1318
+ stdio: ["ignore", logFd, logFd],
1319
+ env: process.env,
1320
+ windowsHide: true
1321
+ }
1322
+ );
1323
+ child.unref();
1324
+ pid2 = child.pid ?? null;
1325
+ } catch (err) {
1326
+ throw new Error(
1327
+ `Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
1328
+ Start it manually:
1329
+ ${manualCommand2}`,
1330
+ { cause: err }
1331
+ );
1332
+ } finally {
1333
+ fs7.closeSync(logFd);
1334
+ }
1335
+ }
1336
+ if (pid2 == null) {
1337
+ throw new Error(
1338
+ `Failed to spawn Codex app-server.
1339
+ Start it manually:
1340
+ ${manualCommand2}`
1341
+ );
1342
+ }
1343
+ const healthy2 = await waitForAppServerHealth(
1344
+ effectiveUrl,
1345
+ APP_SERVER_START_TIMEOUT_MS
1346
+ );
1347
+ if (!healthy2) {
1348
+ await terminateProcess(pid2, options.platform);
1349
+ throw new Error(
1350
+ `Codex app-server did not become healthy at ${effectiveUrl}.
1351
+ Check ${logPath}
1352
+ Or start it manually:
1353
+ ${manualCommand2}`
1354
+ );
1355
+ }
1356
+ pid2 = findListeningProcessId(effectiveUrl, options.platform) ?? pid2;
1357
+ const healthyAt2 = (/* @__PURE__ */ new Date()).toISOString();
1358
+ return {
1359
+ url: effectiveUrl,
1360
+ pid: pid2,
1361
+ managed: true,
1362
+ healthy: true,
1363
+ lastCheckedAt: healthyAt2,
1364
+ lastHealthyAt: healthyAt2,
1365
+ logPath,
1366
+ manualCommand: manualCommand2,
1367
+ auth: null
1368
+ };
1369
+ }
1370
+ const auth = await createManagedAppServerAuth({
1371
+ instanceId: options.instanceId,
1372
+ stateDir: options.stateDir,
1373
+ repoRoot: options.repoRoot,
1374
+ platform: options.platform,
1375
+ publicUrl: effectiveUrl
1376
+ });
1377
+ const manualCommand = formatCodexAppServerCommand("codex", auth.upstreamUrl);
1378
+ let pid;
1379
+ if (options.platform === "win32") {
1380
+ try {
1381
+ pid = startWindowsCodexAppServer(
1382
+ resolvedCommand,
1383
+ auth.upstreamUrl,
1384
+ options.repoRoot,
1385
+ logPath
1386
+ );
1387
+ } catch (err) {
1388
+ if (auth.gatewayPid != null) {
1389
+ await terminateProcess(auth.gatewayPid, options.platform);
1390
+ }
1391
+ removeFileIfExists(auth.tokenPath);
1392
+ throw new Error(
1393
+ `Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
1394
+ Start it manually:
1395
+ ${manualCommand}`,
1396
+ { cause: err }
1397
+ );
1398
+ }
1399
+ } else {
1400
+ const logFd = fs7.openSync(logPath, "a");
1401
+ try {
1402
+ const child = spawn(
1403
+ resolvedCommand,
1404
+ ["app-server", "--listen", auth.upstreamUrl],
1405
+ {
1406
+ cwd: options.repoRoot,
1407
+ detached: true,
1408
+ stdio: ["ignore", logFd, logFd],
1409
+ env: process.env,
1410
+ windowsHide: true
1411
+ }
1412
+ );
1413
+ child.unref();
1414
+ pid = child.pid ?? null;
1415
+ } catch (err) {
1416
+ if (auth.gatewayPid != null) {
1417
+ await terminateProcess(auth.gatewayPid, options.platform);
1418
+ }
1419
+ removeFileIfExists(auth.tokenPath);
1420
+ throw new Error(
1421
+ `Failed to spawn Codex app-server: ${err instanceof Error ? err.message : String(err)}
1422
+ Start it manually:
1423
+ ${manualCommand}`,
1424
+ { cause: err }
1425
+ );
1426
+ } finally {
1427
+ fs7.closeSync(logFd);
1428
+ }
1429
+ }
1430
+ if (pid == null) {
1431
+ if (auth.gatewayPid != null) {
1432
+ await terminateProcess(auth.gatewayPid, options.platform);
1433
+ }
1434
+ removeFileIfExists(auth.tokenPath);
1435
+ throw new Error(
1436
+ `Failed to spawn Codex app-server.
1437
+ Start it manually:
1438
+ ${manualCommand}`
1439
+ );
1440
+ }
1441
+ const healthy = await waitForAppServerHealth(
1442
+ auth.upstreamUrl,
1443
+ APP_SERVER_START_TIMEOUT_MS
1444
+ );
1445
+ if (!healthy) {
1446
+ await terminateProcess(pid, options.platform);
1447
+ if (auth.gatewayPid != null) {
1448
+ await terminateProcess(auth.gatewayPid, options.platform);
1449
+ }
1450
+ removeFileIfExists(auth.tokenPath);
1451
+ throw new Error(
1452
+ `Codex app-server did not become healthy at ${auth.upstreamUrl}.
1453
+ Check ${logPath}
1454
+ Or start it manually:
1455
+ ${manualCommand}`
1456
+ );
1457
+ }
1458
+ const gatewayToken = readGatewayToken(auth);
1459
+ if (!gatewayToken) {
1460
+ await terminateProcess(pid, options.platform);
1461
+ if (auth.gatewayPid != null) {
1462
+ await terminateProcess(auth.gatewayPid, options.platform);
1463
+ }
1464
+ removeFileIfExists(auth.tokenPath);
1465
+ throw new Error("Tap auth gateway token is missing after startup.");
1466
+ }
1467
+ const gatewayHealthy = await waitForAppServerHealth(
1468
+ buildProtectedAppServerUrl(effectiveUrl, gatewayToken),
1469
+ APP_SERVER_GATEWAY_START_TIMEOUT_MS
1470
+ );
1471
+ if (!gatewayHealthy) {
1472
+ await terminateProcess(pid, options.platform);
1473
+ if (auth.gatewayPid != null) {
1474
+ await terminateProcess(auth.gatewayPid, options.platform);
1475
+ }
1476
+ removeFileIfExists(auth.tokenPath);
1477
+ throw new Error(
1478
+ `Tap auth gateway did not become healthy at ${effectiveUrl}.
1479
+ Check ${auth.gatewayLogPath ?? "the gateway log"} and ${logPath}.`
1480
+ );
1481
+ }
1482
+ const healthyAt = (/* @__PURE__ */ new Date()).toISOString();
1483
+ pid = findListeningProcessId(auth.upstreamUrl, options.platform) ?? pid;
1484
+ return {
1485
+ url: effectiveUrl,
1486
+ pid,
1487
+ managed: true,
1488
+ healthy: true,
1489
+ lastCheckedAt: healthyAt,
1490
+ lastHealthyAt: healthyAt,
1491
+ logPath,
1492
+ manualCommand,
1493
+ auth
1494
+ };
1495
+ }
1496
+ function pidFilePath(stateDir, instanceId) {
1497
+ return path7.join(stateDir, "pids", `bridge-${instanceId}.json`);
1498
+ }
1499
+ function logFilePath(stateDir, instanceId) {
1500
+ return path7.join(stateDir, "logs", `bridge-${instanceId}.log`);
1501
+ }
1502
+ function runtimeHeartbeatFilePath(runtimeStateDir) {
1503
+ return path7.join(runtimeStateDir, "heartbeat.json");
1504
+ }
1505
+ function loadRuntimeHeartbeatTimestamp(runtimeStateDir) {
1506
+ if (!runtimeStateDir) {
1507
+ return null;
1508
+ }
1509
+ const heartbeatPath = runtimeHeartbeatFilePath(runtimeStateDir);
1510
+ if (!fs7.existsSync(heartbeatPath)) {
1511
+ return null;
1512
+ }
1513
+ try {
1514
+ const raw = fs7.readFileSync(heartbeatPath, "utf-8");
1515
+ const parsed = JSON.parse(raw);
1516
+ return typeof parsed.updatedAt === "string" ? parsed.updatedAt : null;
1517
+ } catch {
1518
+ return null;
1519
+ }
1520
+ }
1521
+ function resolveHeartbeatTimestamp(state) {
1522
+ return loadRuntimeHeartbeatTimestamp(state?.runtimeStateDir) ?? state?.lastHeartbeat ?? null;
1523
+ }
1524
+ function loadBridgeState(stateDir, instanceId) {
1525
+ const pidPath = pidFilePath(stateDir, instanceId);
1526
+ if (!fs7.existsSync(pidPath)) return null;
1527
+ try {
1528
+ const raw = fs7.readFileSync(pidPath, "utf-8");
1529
+ return JSON.parse(raw);
1530
+ } catch {
1531
+ return null;
1532
+ }
1533
+ }
1534
+ function saveBridgeState(stateDir, instanceId, state) {
1535
+ const pidPath = pidFilePath(stateDir, instanceId);
1536
+ const serializable = JSON.parse(JSON.stringify(state));
1537
+ if (serializable.appServer?.auth) {
1538
+ delete serializable.appServer.auth.token;
1539
+ }
1540
+ writeProtectedTextFile(pidPath, JSON.stringify(serializable, null, 2));
1541
+ }
1542
+ function clearBridgeState(stateDir, instanceId) {
1543
+ const pidPath = pidFilePath(stateDir, instanceId);
1544
+ if (fs7.existsSync(pidPath)) {
1545
+ fs7.unlinkSync(pidPath);
1546
+ }
1547
+ }
1548
+ function isProcessAlive(pid) {
1549
+ try {
1550
+ process.kill(pid, 0);
1551
+ return true;
1552
+ } catch {
1553
+ return false;
1554
+ }
1555
+ }
1556
+ function isBridgeRunning(stateDir, instanceId) {
1557
+ const state = loadBridgeState(stateDir, instanceId);
1558
+ if (!state) return false;
1559
+ return isProcessAlive(state.pid);
1560
+ }
1561
+ async function startBridge(options) {
1562
+ const {
1563
+ instanceId,
1564
+ runtime,
1565
+ stateDir,
1566
+ commsDir,
1567
+ bridgeScript,
1568
+ agentName,
1569
+ port
1570
+ } = options;
1571
+ const resolvedAgent = agentName || process.env.TAP_AGENT_NAME || process.env.CODEX_TAP_AGENT_NAME;
1572
+ if (!resolvedAgent) {
1573
+ throw new Error(
1574
+ `No agent name for ${instanceId} bridge. Set TAP_AGENT_NAME env var or pass --agent-name flag.`
1575
+ );
1576
+ }
1577
+ if (isBridgeRunning(stateDir, instanceId)) {
1578
+ const existing = loadBridgeState(stateDir, instanceId);
1579
+ throw new Error(
1580
+ `Bridge for ${instanceId} is already running (PID: ${existing.pid})`
1581
+ );
1582
+ }
1583
+ const previousBridgeState = loadBridgeState(stateDir, instanceId);
1584
+ const previousAppServer = previousBridgeState?.appServer ?? null;
1585
+ clearBridgeState(stateDir, instanceId);
1586
+ const logPath = logFilePath(stateDir, instanceId);
1587
+ fs7.mkdirSync(path7.dirname(logPath), { recursive: true });
1588
+ rotateLog(logPath);
1589
+ let logFd = null;
1590
+ const repoRoot = options.repoRoot ?? path7.resolve(stateDir, "..");
1591
+ const runtimeStateDir = getBridgeRuntimeStateDir(repoRoot, instanceId);
1592
+ const resolved = resolveNodeRuntime(
1593
+ options.runtimeCommand ?? "node",
1594
+ repoRoot
1595
+ );
1596
+ const command = resolved.command;
1597
+ const runtimeEnv = buildRuntimeEnv(repoRoot);
1598
+ const effectiveAppServerUrl = resolveAppServerUrl(options.appServerUrl, port);
1599
+ let appServer = null;
1600
+ let bridgeAppServerUrl = effectiveAppServerUrl;
1601
+ if (runtime === "codex" && options.manageAppServer) {
1602
+ appServer = await ensureCodexAppServer({
1603
+ instanceId,
1604
+ stateDir,
1605
+ repoRoot,
1606
+ platform: options.platform,
1607
+ appServerUrl: effectiveAppServerUrl,
1608
+ existingAppServer: previousAppServer,
1609
+ noAuth: options.noAuth
1610
+ });
1611
+ if (appServer.auth) {
1612
+ appServer = {
1613
+ ...appServer,
1614
+ auth: materializeGatewayTokenFile(
1615
+ stateDir,
1616
+ instanceId,
1617
+ effectiveAppServerUrl,
1618
+ appServer.auth
1619
+ )
1620
+ };
1621
+ }
1622
+ bridgeAppServerUrl = effectiveAppServerUrl;
1623
+ }
1624
+ try {
1625
+ const bridgeEnv = {
1626
+ ...runtimeEnv,
1627
+ TAP_COMMS_DIR: commsDir,
1628
+ TAP_STATE_DIR: runtimeStateDir,
1629
+ TAP_BRIDGE_RUNTIME: runtime,
1630
+ TAP_BRIDGE_INSTANCE_ID: instanceId,
1631
+ TAP_AGENT_ID: instanceId,
1632
+ TAP_AGENT_NAME: resolvedAgent,
1633
+ CODEX_TAP_AGENT_NAME: resolvedAgent,
1634
+ TAP_RESOLVED_NODE: resolved.command,
1635
+ TAP_STRIP_TYPES: resolved.supportsStripTypes ? "1" : "0",
1636
+ ...bridgeAppServerUrl ? { CODEX_APP_SERVER_URL: bridgeAppServerUrl } : {},
1637
+ ...appServer?.auth?.tokenPath ? { TAP_GATEWAY_TOKEN_FILE: appServer.auth.tokenPath } : {},
1638
+ ...port != null ? { TAP_BRIDGE_PORT: String(port) } : {},
1639
+ ...options.headless?.enabled ? {
1640
+ TAP_HEADLESS: "true",
1641
+ TAP_AGENT_ROLE: options.headless.role,
1642
+ TAP_MAX_REVIEW_ROUNDS: String(options.headless.maxRounds),
1643
+ TAP_QUALITY_FLOOR: options.headless.qualitySeverityFloor
1644
+ } : {},
1645
+ ...options.busyMode ? { TAP_BUSY_MODE: options.busyMode } : {},
1646
+ ...options.pollSeconds != null ? { TAP_POLL_SECONDS: String(options.pollSeconds) } : {},
1647
+ ...options.reconnectSeconds != null ? { TAP_RECONNECT_SECONDS: String(options.reconnectSeconds) } : {},
1648
+ ...options.messageLookbackMinutes != null ? {
1649
+ TAP_MESSAGE_LOOKBACK_MINUTES: String(
1650
+ options.messageLookbackMinutes
1651
+ )
1652
+ } : {},
1653
+ ...options.threadId ? { TAP_THREAD_ID: options.threadId } : {},
1654
+ ...options.ephemeral ? { TAP_EPHEMERAL: "true" } : {},
1655
+ ...options.processExistingMessages ? { TAP_PROCESS_EXISTING: "true" } : {}
1656
+ };
1657
+ let bridgePid = null;
1658
+ if (options.platform === "win32") {
1659
+ bridgePid = startWindowsDetachedProcess(
1660
+ command,
1661
+ [bridgeScript],
1662
+ repoRoot,
1663
+ logPath,
1664
+ bridgeEnv
1665
+ );
1666
+ } else {
1667
+ logFd = fs7.openSync(logPath, "a");
1668
+ const child = spawn(command, [bridgeScript], {
1669
+ detached: true,
1670
+ stdio: ["ignore", logFd, logFd],
1671
+ env: bridgeEnv,
1672
+ windowsHide: true
1673
+ });
1674
+ child.unref();
1675
+ bridgePid = child.pid ?? null;
1676
+ }
1677
+ if (logFd != null) {
1678
+ fs7.closeSync(logFd);
1679
+ logFd = null;
1680
+ }
1681
+ if (!bridgePid) {
1682
+ throw new Error(`Failed to spawn bridge process for ${instanceId}`);
1683
+ }
1684
+ const state = {
1685
+ pid: bridgePid,
1686
+ statePath: pidFilePath(stateDir, instanceId),
1687
+ lastHeartbeat: (/* @__PURE__ */ new Date()).toISOString(),
1688
+ appServer,
1689
+ runtimeStateDir
1690
+ };
1691
+ saveBridgeState(stateDir, instanceId, state);
1692
+ return state;
1693
+ } catch (err) {
1694
+ if (logFd != null) {
1695
+ try {
1696
+ fs7.closeSync(logFd);
1697
+ } catch {
1698
+ }
1699
+ }
1700
+ if (appServer?.managed) {
1701
+ await stopManagedAppServer(appServer, options.platform);
1702
+ }
1703
+ throw err;
1704
+ }
1705
+ }
1706
+ async function stopBridge(options) {
1707
+ const { instanceId, stateDir, platform } = options;
1708
+ const state = loadBridgeState(stateDir, instanceId);
1709
+ if (!state) {
1710
+ return false;
1711
+ }
1712
+ if (!isProcessAlive(state.pid)) {
1713
+ clearBridgeState(stateDir, instanceId);
1714
+ return false;
1715
+ }
1716
+ try {
1717
+ await terminateProcess(state.pid, platform);
1718
+ } catch {
1719
+ }
1720
+ clearBridgeState(stateDir, instanceId);
1721
+ return true;
1722
+ }
1723
+ function rotateLog(logPath) {
1724
+ if (!fs7.existsSync(logPath)) return;
1725
+ try {
1726
+ const stats = fs7.statSync(logPath);
1727
+ if (stats.size === 0) return;
1728
+ const prevPath = `${logPath}.prev`;
1729
+ fs7.renameSync(logPath, prevPath);
1730
+ } catch {
1731
+ }
1732
+ }
1733
+ function updateBridgeHeartbeat(stateDir, instanceId) {
1734
+ const state = loadBridgeState(stateDir, instanceId);
1735
+ if (!state) return;
1736
+ if (state.pid !== process.pid) return;
1737
+ state.lastHeartbeat = (/* @__PURE__ */ new Date()).toISOString();
1738
+ saveBridgeState(stateDir, instanceId, state);
1739
+ }
1740
+ function getHeartbeatAge(stateDir, instanceId) {
1741
+ const state = loadBridgeState(stateDir, instanceId);
1742
+ const heartbeat = resolveHeartbeatTimestamp(state);
1743
+ if (!heartbeat) return null;
1744
+ const heartbeatTime = new Date(heartbeat).getTime();
1745
+ if (isNaN(heartbeatTime)) return null;
1746
+ return Math.floor((Date.now() - heartbeatTime) / 1e3);
1747
+ }
1748
+ function getBridgeHeartbeatTimestamp(stateDir, instanceId) {
1749
+ return resolveHeartbeatTimestamp(loadBridgeState(stateDir, instanceId));
1750
+ }
1751
+ function getBridgeStatus(stateDir, instanceId) {
1752
+ const state = loadBridgeState(stateDir, instanceId);
1753
+ if (!state) return "stopped";
1754
+ if (!isProcessAlive(state.pid)) {
1755
+ clearBridgeState(stateDir, instanceId);
1756
+ return "stale";
1757
+ }
1758
+ return "running";
1759
+ }
1760
+ var DEFAULT_APP_SERVER_URL2, APP_SERVER_HEALTH_TIMEOUT_MS, APP_SERVER_START_TIMEOUT_MS, APP_SERVER_GATEWAY_START_TIMEOUT_MS, APP_SERVER_HEALTH_RETRY_MS, APP_SERVER_AUTH_QUERY_PARAM, APP_SERVER_AUTH_FILE_MODE;
1761
+ var init_bridge = __esm({
1762
+ "src/engine/bridge.ts"() {
1763
+ "use strict";
1764
+ init_common();
1765
+ init_runtime();
1766
+ DEFAULT_APP_SERVER_URL2 = "ws://127.0.0.1:4501";
1767
+ APP_SERVER_HEALTH_TIMEOUT_MS = 1500;
1768
+ APP_SERVER_START_TIMEOUT_MS = 2e4;
1769
+ APP_SERVER_GATEWAY_START_TIMEOUT_MS = 5e3;
1770
+ APP_SERVER_HEALTH_RETRY_MS = 250;
1771
+ APP_SERVER_AUTH_QUERY_PARAM = "tap_token";
1772
+ APP_SERVER_AUTH_FILE_MODE = 384;
1773
+ }
1774
+ });
1775
+
1776
+ // src/engine/dashboard.ts
1777
+ import * as fs8 from "fs";
1778
+ import * as path8 from "path";
1779
+ import { execSync as execSync3 } from "child_process";
1780
+ function collectAgents(commsDir) {
1781
+ const heartbeatsPath = path8.join(commsDir, "heartbeats.json");
1782
+ if (!fs8.existsSync(heartbeatsPath)) return [];
1783
+ try {
1784
+ const raw = fs8.readFileSync(heartbeatsPath, "utf-8");
1785
+ const data = JSON.parse(raw);
1786
+ return Object.entries(data).map(([name, info]) => ({
1787
+ name: info.agent ?? name,
1788
+ status: info.status ?? null,
1789
+ lastActivity: info.lastActivity ?? info.timestamp ?? null,
1790
+ joinedAt: info.joinedAt ?? null
1791
+ }));
1792
+ } catch {
1793
+ return [];
1794
+ }
1795
+ }
1796
+ function collectBridges(repoRoot) {
1797
+ const state = loadState(repoRoot);
1798
+ const { config } = resolveConfig({}, repoRoot);
1799
+ const stateDir = config.stateDir;
1800
+ const bridges = [];
1801
+ if (state) {
1802
+ for (const [id, inst] of Object.entries(state.instances)) {
1803
+ if (!inst?.installed) continue;
1804
+ if (inst.bridgeMode !== "app-server") continue;
1805
+ const instanceId = id;
1806
+ const status = getBridgeStatus(stateDir, instanceId);
1807
+ const bridgeState = loadBridgeState(stateDir, instanceId);
1808
+ const age = getHeartbeatAge(stateDir, instanceId);
1809
+ bridges.push({
1810
+ instanceId: id,
1811
+ runtime: inst.runtime,
1812
+ status,
1813
+ pid: bridgeState?.pid ?? null,
1814
+ port: inst.port ?? null,
1815
+ heartbeatAge: age,
1816
+ headless: inst.headless?.enabled ?? false
1817
+ });
1818
+ }
1819
+ }
1820
+ const tmpDir = path8.join(repoRoot, ".tmp");
1821
+ if (fs8.existsSync(tmpDir)) {
1822
+ try {
1823
+ const dirs = fs8.readdirSync(tmpDir).filter((d) => d.startsWith("codex-app-server-bridge"));
1824
+ for (const dir of dirs) {
1825
+ const daemonPath = path8.join(tmpDir, dir, "bridge-daemon.json");
1826
+ if (!fs8.existsSync(daemonPath)) continue;
1827
+ try {
1828
+ const raw = fs8.readFileSync(daemonPath, "utf-8");
1829
+ const daemon = JSON.parse(raw);
1830
+ const alreadyCovered = bridges.some(
1831
+ (b) => b.pid === daemon.pid && b.pid !== null
1832
+ );
1833
+ if (alreadyCovered) continue;
1834
+ const agentFile = path8.join(tmpDir, dir, "agent-name.txt");
1835
+ const agentName = fs8.existsSync(agentFile) ? fs8.readFileSync(agentFile, "utf-8").trim() : dir;
1836
+ const running = daemon.pid ? isProcessAlive(daemon.pid) : false;
1837
+ const portMatch = daemon.appServerUrl?.match(/:(\d+)/);
1838
+ const port = portMatch ? parseInt(portMatch[1], 10) : null;
1839
+ bridges.push({
1840
+ instanceId: agentName,
1841
+ runtime: "codex",
1842
+ status: running ? "running" : "stale",
1843
+ pid: daemon.pid ?? null,
1844
+ port,
1845
+ heartbeatAge: null,
1846
+ headless: false
1847
+ });
1848
+ } catch {
1849
+ }
1850
+ }
1851
+ } catch {
1852
+ }
1853
+ }
1854
+ return bridges;
1855
+ }
1856
+ function collectPRs() {
1857
+ try {
1858
+ const output = execSync3(
1859
+ "gh pr list --state all --limit 10 --json number,title,author,state,url",
1860
+ { encoding: "utf-8", timeout: 1e4, stdio: ["pipe", "pipe", "pipe"] }
1861
+ );
1862
+ const prs = JSON.parse(output);
1863
+ return prs.map((pr) => ({
1864
+ number: pr.number,
1865
+ title: pr.title,
1866
+ author: pr.author.login,
1867
+ state: pr.state,
1868
+ url: pr.url
1869
+ }));
1870
+ } catch {
1871
+ return [];
1872
+ }
1873
+ }
1874
+ function collectWarnings(bridges, agents) {
1875
+ const warnings = [];
1876
+ for (const bridge of bridges) {
1877
+ if (bridge.status === "stale") {
1878
+ warnings.push({
1879
+ level: "warn",
1880
+ message: `Bridge ${bridge.instanceId} is stale (PID ${bridge.pid} dead)`
1881
+ });
1882
+ }
1883
+ if (bridge.status === "running" && bridge.heartbeatAge !== null && bridge.heartbeatAge > 60) {
1884
+ warnings.push({
1885
+ level: "warn",
1886
+ message: `Bridge ${bridge.instanceId} heartbeat stale (${bridge.heartbeatAge}s ago)`
1887
+ });
1888
+ }
1889
+ }
1890
+ if (bridges.length === 0) {
1891
+ warnings.push({
1892
+ level: "warn",
1893
+ message: "No bridges configured"
1894
+ });
1895
+ }
1896
+ if (agents.length === 0) {
1897
+ warnings.push({
1898
+ level: "warn",
1899
+ message: "No agent heartbeats found"
1900
+ });
1901
+ }
1902
+ return warnings;
1903
+ }
1904
+ function collectDashboardSnapshot(repoRoot, commsDirOverride) {
1905
+ const { config } = resolveConfig(
1906
+ commsDirOverride ? { commsDir: commsDirOverride } : {},
1907
+ repoRoot
1908
+ );
1909
+ const resolved = config;
1910
+ const agents = collectAgents(resolved.commsDir);
1911
+ const bridges = collectBridges(resolved.repoRoot);
1912
+ const prs = collectPRs();
1913
+ const warnings = collectWarnings(bridges, agents);
1914
+ return {
1915
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1916
+ repoRoot: resolved.repoRoot,
1917
+ commsDir: resolved.commsDir,
1918
+ agents,
1919
+ bridges,
1920
+ prs,
1921
+ warnings
1922
+ };
1923
+ }
1924
+ var init_dashboard = __esm({
1925
+ "src/engine/dashboard.ts"() {
1926
+ "use strict";
1927
+ init_config();
1928
+ init_bridge();
1929
+ init_state();
1930
+ }
1931
+ });
1932
+
1933
+ // src/adapters/claude.ts
1934
+ import * as fs9 from "fs";
1935
+ import * as path9 from "path";
1936
+ import { execSync as execSync4 } from "child_process";
1937
+ function findMcpJsonPath(ctx) {
1938
+ return path9.join(ctx.repoRoot, ".mcp.json");
1939
+ }
1940
+ function findClaudeCommand() {
1941
+ try {
1942
+ execSync4("claude --version", { stdio: "pipe" });
1943
+ return "claude";
1944
+ } catch {
1945
+ return null;
1946
+ }
1947
+ }
1948
+ function buildMcpServerEntry(ctx) {
1949
+ const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
1950
+ if (!managed.command) return null;
1951
+ return {
1952
+ type: "stdio",
1953
+ command: managed.command,
1954
+ args: managed.args,
1955
+ env: managed.env
1956
+ };
1957
+ }
1958
+ function setNestedKey(obj, keyPath, value) {
1959
+ const keys = keyPath.split(".");
1960
+ let current = obj;
1961
+ for (let i = 0; i < keys.length - 1; i++) {
1962
+ const key = keys[i];
1963
+ if (typeof current[key] !== "object" || current[key] === null) {
1964
+ current[key] = {};
1965
+ }
1966
+ current = current[key];
1967
+ }
1968
+ current[keys[keys.length - 1]] = value;
1969
+ }
1970
+ function normalizeTapCommsDir(value) {
1971
+ return typeof value === "string" ? path9.resolve(value).replace(/\\/g, "/") : "";
1972
+ }
1973
+ var MCP_SERVER_KEY, claudeAdapter;
1974
+ var init_claude = __esm({
1975
+ "src/adapters/claude.ts"() {
1976
+ "use strict";
1977
+ init_state();
1978
+ init_common();
1979
+ MCP_SERVER_KEY = "tap-comms";
1980
+ claudeAdapter = {
1981
+ runtime: "claude",
1982
+ async probe(ctx) {
1983
+ const warnings = [];
1984
+ const issues = [];
1985
+ const configPath = findMcpJsonPath(ctx);
1986
+ const configExists = fs9.existsSync(configPath);
1987
+ const runtimeCommand = findClaudeCommand();
1988
+ const canWrite = configExists ? (() => {
1989
+ try {
1990
+ fs9.accessSync(configPath, fs9.constants.W_OK);
1991
+ return true;
1992
+ } catch {
1993
+ return false;
1994
+ }
1995
+ })() : true;
1996
+ if (!runtimeCommand) {
1997
+ warnings.push(
1998
+ "Claude CLI not found in PATH. Config will be created but may need manual setup."
1999
+ );
2000
+ }
2001
+ const managed = buildManagedMcpServerSpec(ctx);
2002
+ warnings.push(...managed.warnings);
2003
+ issues.push(...managed.issues);
2004
+ if (!fs9.existsSync(ctx.commsDir)) {
2005
+ issues.push(
2006
+ `Comms directory not found: ${ctx.commsDir}. Run "init" first.`
2007
+ );
2008
+ }
2009
+ return {
2010
+ installed: true,
2011
+ // Claude adapter always "installed" — .mcp.json is per-project
2012
+ configPath,
2013
+ configExists,
2014
+ runtimeCommand,
2015
+ version: null,
2016
+ canWrite,
2017
+ warnings,
2018
+ issues
2019
+ };
2020
+ },
2021
+ async plan(ctx, probe) {
2022
+ const configPath = probe.configPath ?? findMcpJsonPath(ctx);
2023
+ const conflicts = [];
2024
+ const warnings = [];
2025
+ const operations = [];
2026
+ const ownedArtifacts = [];
2027
+ if (probe.configExists) {
2028
+ const raw = fs9.readFileSync(configPath, "utf-8");
2029
+ try {
2030
+ const config = JSON.parse(raw);
2031
+ if (config.mcpServers?.[MCP_SERVER_KEY]) {
2032
+ conflicts.push(
2033
+ `Existing "${MCP_SERVER_KEY}" entry in .mcp.json will be overwritten.`
2034
+ );
2035
+ }
2036
+ } catch {
2037
+ warnings.push(
2038
+ ".mcp.json exists but is not valid JSON. Will be overwritten."
2039
+ );
2040
+ }
2041
+ }
2042
+ const serverEntry = buildMcpServerEntry(ctx);
2043
+ if (!serverEntry) {
2044
+ warnings.push(
2045
+ "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."
2046
+ );
2047
+ return {
2048
+ runtime: "claude",
2049
+ operations: [],
2050
+ ownedArtifacts: [],
2051
+ backupDir: ensureBackupDir(ctx.stateDir, "claude"),
2052
+ restartRequired: false,
2053
+ conflicts,
2054
+ warnings
2055
+ };
2056
+ }
2057
+ operations.push({
2058
+ type: probe.configExists ? "merge" : "set",
2059
+ path: configPath,
2060
+ key: `mcpServers.${MCP_SERVER_KEY}`,
2061
+ value: serverEntry
2062
+ });
2063
+ ownedArtifacts.push({
2064
+ kind: "json-path",
2065
+ path: configPath,
2066
+ selector: `mcpServers.${MCP_SERVER_KEY}`
2067
+ });
2068
+ const backupDir = ensureBackupDir(ctx.stateDir, "claude");
2069
+ return {
2070
+ runtime: "claude",
2071
+ operations,
2072
+ ownedArtifacts,
2073
+ backupDir,
2074
+ restartRequired: true,
2075
+ conflicts,
2076
+ warnings
2077
+ };
2078
+ },
2079
+ async apply(_ctx, plan) {
2080
+ const changedFiles = [];
2081
+ const warnings = [];
2082
+ let appliedOps = 0;
2083
+ for (const op of plan.operations) {
2084
+ try {
2085
+ if (op.type === "set" || op.type === "merge") {
2086
+ let config = {};
2087
+ if (fs9.existsSync(op.path)) {
2088
+ backupFile(op.path, plan.backupDir);
2089
+ const raw = fs9.readFileSync(op.path, "utf-8");
2090
+ try {
2091
+ config = JSON.parse(raw);
2092
+ } catch {
2093
+ warnings.push(
2094
+ `${op.path} was invalid JSON. Created backup and starting fresh.`
2095
+ );
2096
+ }
2097
+ }
2098
+ if (op.key) {
2099
+ setNestedKey(config, op.key, op.value);
2100
+ }
2101
+ const tmp = `${op.path}.tmp.${process.pid}`;
2102
+ fs9.writeFileSync(
2103
+ tmp,
2104
+ JSON.stringify(config, null, 2) + "\n",
2105
+ "utf-8"
2106
+ );
2107
+ fs9.renameSync(tmp, op.path);
2108
+ changedFiles.push(op.path);
2109
+ appliedOps++;
2110
+ }
2111
+ } catch (err) {
2112
+ warnings.push(
2113
+ `Failed to apply op on ${op.path}: ${err instanceof Error ? err.message : String(err)}`
2114
+ );
2115
+ }
2116
+ }
2117
+ const lastAppliedHash = changedFiles.length > 0 ? fileHash(changedFiles[0]) : "";
2118
+ return {
2119
+ success: appliedOps > 0,
2120
+ appliedOps,
2121
+ backupCreated: true,
2122
+ lastAppliedHash,
2123
+ ownedArtifacts: plan.ownedArtifacts,
2124
+ changedFiles,
2125
+ restartRequired: plan.restartRequired,
2126
+ warnings
2127
+ };
2128
+ },
2129
+ async verify(ctx, plan) {
2130
+ const checks = [];
2131
+ const warnings = [];
2132
+ const configPath = plan.operations[0]?.path;
2133
+ if (configPath) {
2134
+ checks.push({
2135
+ name: "Config file exists",
2136
+ passed: fs9.existsSync(configPath),
2137
+ message: fs9.existsSync(configPath) ? void 0 : `${configPath} not found`
2138
+ });
2139
+ if (fs9.existsSync(configPath)) {
2140
+ try {
2141
+ const raw = fs9.readFileSync(configPath, "utf-8");
2142
+ const config = JSON.parse(raw);
2143
+ checks.push({ name: "Config is valid JSON", passed: true });
2144
+ const entry = config.mcpServers?.[MCP_SERVER_KEY];
2145
+ checks.push({
2146
+ name: "tap-comms entry present",
2147
+ passed: !!entry,
2148
+ message: entry ? void 0 : `mcpServers.${MCP_SERVER_KEY} not found`
2149
+ });
2150
+ if (entry) {
2151
+ const hasCommsDir = normalizeTapCommsDir(entry.env?.TAP_COMMS_DIR) === normalizeTapCommsDir(ctx.commsDir);
2152
+ checks.push({
2153
+ name: "TAP_COMMS_DIR configured",
2154
+ passed: hasCommsDir,
2155
+ message: hasCommsDir ? void 0 : `Expected ${ctx.commsDir}`
2156
+ });
2157
+ }
2158
+ } catch {
2159
+ checks.push({
2160
+ name: "Config is valid JSON",
2161
+ passed: false,
2162
+ message: "Parse error"
2163
+ });
2164
+ }
2165
+ }
2166
+ }
2167
+ checks.push({
2168
+ name: "Comms directory exists",
2169
+ passed: fs9.existsSync(ctx.commsDir),
2170
+ message: fs9.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
2171
+ });
2172
+ const cmd = findClaudeCommand();
2173
+ checks.push({
2174
+ name: "Claude CLI found",
2175
+ passed: !!cmd,
2176
+ message: cmd ? void 0 : "claude not in PATH (non-blocking)"
2177
+ });
2178
+ if (!cmd) {
2179
+ warnings.push(
2180
+ "Claude CLI not in PATH. Config is ready but cannot verify runtime reads it."
2181
+ );
2182
+ }
2183
+ const ok = checks.filter((c) => c.name !== "Claude CLI found").every((c) => c.passed);
2184
+ return { ok, checks, restartRequired: true, warnings };
2185
+ },
2186
+ bridgeMode() {
2187
+ return "native-push";
2188
+ }
2189
+ };
2190
+ }
2191
+ });
2192
+
2193
+ // src/artifact-backups.ts
2194
+ import * as crypto2 from "crypto";
2195
+ import * as fs10 from "fs";
2196
+ import * as path10 from "path";
2197
+ function selectorHash(selector) {
2198
+ return crypto2.createHash("sha256").update(selector).digest("hex").slice(0, 12);
2199
+ }
2200
+ function artifactBackupPath(backupDir, kind, selector) {
2201
+ const safeKind = kind.replace(/[^a-z-]/gi, "-");
2202
+ return path10.join(backupDir, `${safeKind}-${selectorHash(selector)}.json`);
2203
+ }
2204
+ function writeArtifactBackup(backupPath, payload) {
2205
+ fs10.mkdirSync(path10.dirname(backupPath), { recursive: true });
2206
+ const tmp = `${backupPath}.tmp.${process.pid}`;
2207
+ fs10.writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
2208
+ fs10.renameSync(tmp, backupPath);
2209
+ }
2210
+ var init_artifact_backups = __esm({
2211
+ "src/artifact-backups.ts"() {
2212
+ "use strict";
2213
+ }
2214
+ });
2215
+
2216
+ // src/toml.ts
2217
+ function splitLines(content) {
2218
+ return content.replace(/\r\n/g, "\n").split("\n");
2219
+ }
2220
+ function tableHeader(selector) {
2221
+ return `[${selector}]`;
2222
+ }
2223
+ function findTableRange(lines, selector) {
2224
+ const header = tableHeader(selector);
2225
+ for (let i = 0; i < lines.length; i++) {
2226
+ if (lines[i].trim() !== header) continue;
2227
+ let end = lines.length;
2228
+ for (let j = i + 1; j < lines.length; j++) {
2229
+ const trimmed = lines[j].trim();
2230
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
2231
+ end = j;
2232
+ break;
2233
+ }
2234
+ }
2235
+ return { start: i, end };
2236
+ }
2237
+ return null;
2238
+ }
2239
+ function escapeBasicString(value) {
2240
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2241
+ }
2242
+ function renderValue(value) {
2243
+ if (Array.isArray(value)) {
2244
+ return `[${value.map((item) => `"${escapeBasicString(item)}"`).join(", ")}]`;
2245
+ }
2246
+ return `"${escapeBasicString(value)}"`;
2247
+ }
2248
+ function extractTomlTable(content, selector) {
2249
+ const lines = splitLines(content);
2250
+ const range = findTableRange(lines, selector);
2251
+ if (!range) return null;
2252
+ return `${lines.slice(range.start, range.end).join("\n")}
2253
+ `;
2254
+ }
2255
+ function replaceTomlTable(content, selector, replacement) {
2256
+ const lines = splitLines(content);
2257
+ const range = findTableRange(lines, selector);
2258
+ const replacementLines = replacement.replace(/\r\n/g, "\n").trimEnd().split("\n");
2259
+ if (!range) {
2260
+ const doc = trimTomlDocument(content);
2261
+ if (!doc) return `${replacement.trimEnd()}
2262
+ `;
2263
+ return `${doc}
2264
+
2265
+ ${replacement.trimEnd()}
2266
+ `;
2267
+ }
2268
+ const next = [
2269
+ ...lines.slice(0, range.start),
2270
+ ...replacementLines,
2271
+ ...lines.slice(range.end)
2272
+ ];
2273
+ return `${trimTomlDocument(next.join("\n"))}
2274
+ `;
2275
+ }
2276
+ function renderTomlTable(selector, entries, existingContent) {
2277
+ const preserved = parseTomlAssignments(existingContent ?? "");
2278
+ const merged = { ...preserved, ...entries };
2279
+ const lines = [tableHeader(selector)];
2280
+ for (const [key, value] of Object.entries(merged)) {
2281
+ lines.push(`${key} = ${renderValue(value)}`);
2282
+ }
2283
+ return `${lines.join("\n")}
2284
+ `;
2285
+ }
2286
+ function parseTomlAssignments(tableContent) {
2287
+ const lines = splitLines(tableContent);
2288
+ const values = {};
2289
+ for (const rawLine of lines) {
2290
+ const line = rawLine.trim();
2291
+ if (!line || line.startsWith("#") || line.startsWith("[") && line.endsWith("]")) {
2292
+ continue;
2293
+ }
2294
+ const match = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
2295
+ if (!match) continue;
2296
+ const [, key, rawValue] = match;
2297
+ const value = rawValue.trim();
2298
+ if (value.startsWith("[") && value.endsWith("]")) {
2299
+ const items = value.slice(1, -1).split(",").map((item) => item.trim()).filter(Boolean).map(unquoteTomlString);
2300
+ values[key] = items;
2301
+ continue;
2302
+ }
2303
+ values[key] = unquoteTomlString(value);
2304
+ }
2305
+ return values;
2306
+ }
2307
+ function trimTomlDocument(content) {
2308
+ return content.replace(/\s+$/g, "").replace(/\n{3,}/g, "\n\n");
2309
+ }
2310
+ function unquoteTomlString(value) {
2311
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2312
+ const inner = value.slice(1, -1);
2313
+ return value.startsWith('"') ? inner.replace(/\\"/g, '"').replace(/\\\\/g, "\\") : inner;
2314
+ }
2315
+ return value;
2316
+ }
2317
+ var init_toml = __esm({
2318
+ "src/toml.ts"() {
2319
+ "use strict";
2320
+ }
2321
+ });
2322
+
2323
+ // src/adapters/codex.ts
2324
+ import * as fs11 from "fs";
2325
+ import * as path11 from "path";
2326
+ import { fileURLToPath as fileURLToPath4 } from "url";
2327
+ function findCodexConfigPath() {
2328
+ return path11.join(getHomeDir(), ".codex", "config.toml");
2329
+ }
2330
+ function canonicalizeTrustPath(targetPath) {
2331
+ let resolved = path11.resolve(targetPath).replace(/\//g, "\\");
2332
+ const driveRoot = /^[A-Za-z]:\\$/;
2333
+ if (!driveRoot.test(resolved)) {
2334
+ resolved = resolved.replace(/\\+$/g, "");
2335
+ }
2336
+ return resolved.startsWith("\\\\?\\") ? resolved : `\\\\?\\${resolved}`;
2337
+ }
2338
+ function trustSelector(targetPath) {
2339
+ return `projects.'${canonicalizeTrustPath(targetPath)}'`;
2340
+ }
2341
+ function getTrustTargets(ctx) {
2342
+ const targets = [ctx.repoRoot, process.cwd()];
2343
+ return [...new Set(targets.map((value) => path11.resolve(value)))];
2344
+ }
2345
+ function buildManagedArtifacts(configPath, ctx) {
2346
+ const artifacts = [
2347
+ { kind: "toml-table", path: configPath, selector: MCP_SELECTOR },
2348
+ { kind: "toml-table", path: configPath, selector: ENV_SELECTOR }
2349
+ ];
2350
+ for (const target of getTrustTargets(ctx)) {
2351
+ artifacts.push({
2352
+ kind: "toml-table",
2353
+ path: configPath,
2354
+ selector: trustSelector(target)
2355
+ });
2356
+ }
2357
+ return artifacts;
2358
+ }
2359
+ function readConfigOrEmpty(configPath) {
2360
+ if (!fs11.existsSync(configPath)) return "";
2361
+ return fs11.readFileSync(configPath, "utf-8");
2362
+ }
2363
+ function writeTomlFile(filePath, content) {
2364
+ fs11.mkdirSync(path11.dirname(filePath), { recursive: true });
2365
+ const tmp = `${filePath}.tmp.${process.pid}`;
2366
+ fs11.writeFileSync(tmp, content, "utf-8");
2367
+ fs11.renameSync(tmp, filePath);
2368
+ }
2369
+ function verifyManagedToml(content, ctx, configPath) {
2370
+ const checks = [];
2371
+ const managed = buildManagedMcpServerSpec(ctx);
2372
+ const mainTable = extractTomlTable(content, MCP_SELECTOR);
2373
+ const envTable = extractTomlTable(content, ENV_SELECTOR);
2374
+ checks.push({
2375
+ name: "Codex config exists",
2376
+ passed: fs11.existsSync(configPath),
2377
+ message: fs11.existsSync(configPath) ? void 0 : `${configPath} not found`
2378
+ });
2379
+ checks.push({
2380
+ name: "tap-comms MCP table present",
2381
+ passed: !!mainTable,
2382
+ message: mainTable ? void 0 : `${MCP_SELECTOR} not found`
2383
+ });
2384
+ checks.push({
2385
+ name: "tap-comms env table present",
2386
+ passed: !!envTable,
2387
+ message: envTable ? void 0 : `${ENV_SELECTOR} not found`
2388
+ });
2389
+ for (const target of getTrustTargets(ctx)) {
2390
+ const selector = trustSelector(target);
2391
+ const trustTable = extractTomlTable(content, selector);
2392
+ checks.push({
2393
+ name: `Trust table present: ${canonicalizeTrustPath(target)}`,
2394
+ passed: !!trustTable && trustTable.includes('trust_level = "trusted"'),
2395
+ message: trustTable && trustTable.includes('trust_level = "trusted"') ? void 0 : `${selector} missing trust_level = "trusted"`
2396
+ });
2397
+ }
2398
+ if (mainTable && managed.command) {
2399
+ checks.push({
2400
+ name: "Managed command configured",
2401
+ passed: mainTable.includes(
2402
+ `command = "${managed.command.replace(/\\/g, "\\\\")}"`
2403
+ ) && mainTable.includes(
2404
+ `args = ["${managed.args[0]?.replace(/\\/g, "\\\\") ?? ""}"]`
2405
+ ),
2406
+ message: "Managed tap-comms command/args do not match expected values"
2407
+ });
2408
+ }
2409
+ return checks;
2410
+ }
2411
+ var MCP_SELECTOR, ENV_SELECTOR, codexAdapter;
2412
+ var init_codex = __esm({
2413
+ "src/adapters/codex.ts"() {
2414
+ "use strict";
2415
+ init_state();
2416
+ init_artifact_backups();
2417
+ init_toml();
2418
+ init_common();
2419
+ MCP_SELECTOR = "mcp_servers.tap-comms";
2420
+ ENV_SELECTOR = "mcp_servers.tap-comms.env";
2421
+ codexAdapter = {
2422
+ runtime: "codex",
2423
+ async probe(ctx) {
2424
+ const warnings = [];
2425
+ const issues = [];
2426
+ const configPath = findCodexConfigPath();
2427
+ const configExists = fs11.existsSync(configPath);
2428
+ const runtimeProbe = probeCommand(
2429
+ ctx.platform === "win32" ? ["codex", "codex.cmd"] : ["codex"]
2430
+ );
2431
+ if (!runtimeProbe.command) {
2432
+ warnings.push(
2433
+ "Codex CLI not found in PATH. Config can still be written, but runtime verification will be limited."
2434
+ );
2435
+ }
2436
+ if (!fs11.existsSync(ctx.commsDir)) {
2437
+ issues.push(
2438
+ `Comms directory not found: ${ctx.commsDir}. Run "init" first.`
2439
+ );
2440
+ }
2441
+ const managed = buildManagedMcpServerSpec(ctx);
2442
+ warnings.push(...managed.warnings);
2443
+ issues.push(...managed.issues);
2444
+ return {
2445
+ installed: true,
2446
+ configPath,
2447
+ configExists,
2448
+ runtimeCommand: runtimeProbe.command,
2449
+ version: runtimeProbe.version,
2450
+ canWrite: canWriteOrCreate(configPath),
2451
+ warnings,
2452
+ issues
2453
+ };
2454
+ },
2455
+ async plan(ctx, probe) {
2456
+ const configPath = probe.configPath ?? findCodexConfigPath();
2457
+ const conflicts = [];
2458
+ const warnings = [];
2459
+ const operations = [];
2460
+ const ownedArtifacts = buildManagedArtifacts(configPath, ctx);
2461
+ if (probe.configExists) {
2462
+ const content = readConfigOrEmpty(configPath);
2463
+ if (extractTomlTable(content, MCP_SELECTOR)) {
2464
+ conflicts.push(`Existing ${MCP_SELECTOR} table will be updated.`);
2465
+ }
2466
+ if (extractTomlTable(content, ENV_SELECTOR)) {
2467
+ conflicts.push(`Existing ${ENV_SELECTOR} table will be updated.`);
2468
+ }
2469
+ for (const target of getTrustTargets(ctx)) {
2470
+ const selector = trustSelector(target);
2471
+ if (extractTomlTable(content, selector)) {
2472
+ conflicts.push(`Existing ${selector} table will be updated.`);
2473
+ }
2474
+ }
2475
+ }
2476
+ for (const artifact of ownedArtifacts) {
2477
+ operations.push({
2478
+ type: probe.configExists ? "merge" : "set",
2479
+ path: configPath,
2480
+ key: artifact.selector
2481
+ });
2482
+ }
2483
+ return {
2484
+ runtime: "codex",
2485
+ operations,
2486
+ ownedArtifacts,
2487
+ backupDir: ensureBackupDir(ctx.stateDir, "codex"),
2488
+ restartRequired: true,
2489
+ conflicts,
2490
+ warnings
2491
+ };
2492
+ },
2493
+ async apply(ctx, plan) {
2494
+ const configPath = plan.operations[0]?.path ?? findCodexConfigPath();
2495
+ const warnings = [];
2496
+ const changedFiles = [];
2497
+ const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
2498
+ warnings.push(...managed.warnings);
2499
+ if (managed.issues.length > 0 || !managed.command) {
2500
+ return {
2501
+ success: false,
2502
+ appliedOps: 0,
2503
+ backupCreated: false,
2504
+ lastAppliedHash: "",
2505
+ ownedArtifacts: [],
2506
+ changedFiles,
2507
+ restartRequired: false,
2508
+ warnings: [...managed.warnings, ...managed.issues]
2509
+ };
2510
+ }
2511
+ const existingContent = readConfigOrEmpty(configPath);
2512
+ if (fs11.existsSync(configPath) && existingContent) {
2513
+ backupFile(configPath, plan.backupDir);
2514
+ }
2515
+ const artifactsWithBackups = plan.ownedArtifacts.map((artifact) => {
2516
+ const previousContent = artifact.kind === "toml-table" ? extractTomlTable(existingContent, artifact.selector) : null;
2517
+ const backupPath = artifactBackupPath(
2518
+ plan.backupDir,
2519
+ artifact.kind,
2520
+ artifact.selector
2521
+ );
2522
+ writeArtifactBackup(backupPath, {
2523
+ kind: "toml-table",
2524
+ selector: artifact.selector,
2525
+ existed: previousContent !== null,
2526
+ content: previousContent ?? void 0
2527
+ });
2528
+ return { ...artifact, backupPath };
2529
+ });
2530
+ let nextContent = existingContent;
2531
+ nextContent = replaceTomlTable(
2532
+ nextContent,
2533
+ MCP_SELECTOR,
2534
+ renderTomlTable(
2535
+ MCP_SELECTOR,
2536
+ {
2537
+ command: managed.command,
2538
+ args: managed.args
2539
+ },
2540
+ extractTomlTable(existingContent, MCP_SELECTOR)
2541
+ )
2542
+ );
2543
+ nextContent = replaceTomlTable(
2544
+ nextContent,
2545
+ ENV_SELECTOR,
2546
+ renderTomlTable(
2547
+ ENV_SELECTOR,
2548
+ managed.env,
2549
+ extractTomlTable(existingContent, ENV_SELECTOR)
2550
+ )
2551
+ );
2552
+ for (const target of getTrustTargets(ctx)) {
2553
+ const selector = trustSelector(target);
2554
+ nextContent = replaceTomlTable(
2555
+ nextContent,
2556
+ selector,
2557
+ renderTomlTable(
2558
+ selector,
2559
+ { trust_level: "trusted" },
2560
+ extractTomlTable(existingContent, selector)
2561
+ )
2562
+ );
2563
+ }
2564
+ writeTomlFile(configPath, nextContent);
2565
+ changedFiles.push(configPath);
2566
+ return {
2567
+ success: true,
2568
+ appliedOps: plan.operations.length,
2569
+ backupCreated: true,
2570
+ lastAppliedHash: fileHash(configPath),
2571
+ ownedArtifacts: artifactsWithBackups,
2572
+ changedFiles,
2573
+ restartRequired: true,
2574
+ warnings
2575
+ };
2576
+ },
2577
+ async verify(ctx, plan) {
2578
+ const warnings = [];
2579
+ const configPath = plan.operations[0]?.path ?? findCodexConfigPath();
2580
+ const content = readConfigOrEmpty(configPath);
2581
+ const runtimeProbe = probeCommand(
2582
+ ctx.platform === "win32" ? ["codex", "codex.cmd"] : ["codex"]
2583
+ );
2584
+ const checks = verifyManagedToml(content, ctx, configPath);
2585
+ checks.push({
2586
+ name: "Comms directory exists",
2587
+ passed: fs11.existsSync(ctx.commsDir),
2588
+ message: fs11.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
2589
+ });
2590
+ checks.push({
2591
+ name: "Codex CLI found",
2592
+ passed: !!runtimeProbe.command,
2593
+ message: runtimeProbe.command ? void 0 : "codex not in PATH (non-blocking)"
2594
+ });
2595
+ if (!runtimeProbe.command) {
2596
+ warnings.push(
2597
+ "Codex CLI not in PATH. Config is written, but runtime verification is partial."
2598
+ );
2599
+ }
2600
+ return {
2601
+ ok: checks.filter((check) => check.name !== "Codex CLI found").every((check) => check.passed),
2602
+ checks,
2603
+ restartRequired: true,
2604
+ warnings
2605
+ };
2606
+ },
2607
+ bridgeMode() {
2608
+ return "app-server";
2609
+ },
2610
+ resolveBridgeScript(ctx) {
2611
+ const distDir = path11.dirname(fileURLToPath4(import.meta.url));
2612
+ const candidates = [
2613
+ // 1. Relative to bundled CLI (npm install / npx)
2614
+ path11.join(distDir, "bridges", "codex-bridge-runner.mjs"),
2615
+ // 2. Monorepo development — dist inside repo
2616
+ path11.join(
2617
+ ctx.repoRoot,
2618
+ "packages",
2619
+ "tap-comms",
2620
+ "dist",
2621
+ "bridges",
2622
+ "codex-bridge-runner.mjs"
2623
+ ),
2624
+ // 3. Source file — dev mode with strip-types
2625
+ path11.join(
2626
+ ctx.repoRoot,
2627
+ "packages",
2628
+ "tap-comms",
2629
+ "src",
2630
+ "bridges",
2631
+ "codex-bridge-runner.ts"
2632
+ )
2633
+ ];
2634
+ for (const candidate of candidates) {
2635
+ if (fs11.existsSync(candidate)) return candidate;
2636
+ }
2637
+ return null;
2638
+ }
2639
+ };
2640
+ }
2641
+ });
2642
+
2643
+ // src/adapters/gemini.ts
2644
+ import * as fs12 from "fs";
2645
+ import * as path12 from "path";
2646
+ function candidateConfigPaths(ctx) {
2647
+ const home = getHomeDir();
2648
+ return [
2649
+ path12.join(ctx.repoRoot, ".gemini", "settings.json"),
2650
+ path12.join(home, ".gemini", "settings.json"),
2651
+ path12.join(home, ".gemini", "antigravity", "mcp_config.json")
2652
+ ];
2653
+ }
2654
+ function chooseGeminiConfigPath(ctx) {
2655
+ const [workspaceConfig, homeConfig, antigravityConfig] = candidateConfigPaths(ctx);
2656
+ if (fs12.existsSync(workspaceConfig)) return workspaceConfig;
2657
+ if (fs12.existsSync(homeConfig)) return homeConfig;
2658
+ if (fs12.existsSync(antigravityConfig)) {
2659
+ const raw = fs12.readFileSync(antigravityConfig, "utf-8").trim();
2660
+ if (raw) {
2661
+ try {
2662
+ JSON.parse(raw);
2663
+ return antigravityConfig;
2664
+ } catch {
2665
+ }
2666
+ }
2667
+ }
2668
+ return workspaceConfig;
2669
+ }
2670
+ function readJsonFile(filePath) {
2671
+ if (!fs12.existsSync(filePath)) return {};
2672
+ const raw = fs12.readFileSync(filePath, "utf-8").trim();
2673
+ if (!raw) return {};
2674
+ return JSON.parse(raw);
2675
+ }
2676
+ function setNestedKey2(obj, keyPath, value) {
2677
+ const keys = keyPath.split(".");
2678
+ let current = obj;
2679
+ for (let i = 0; i < keys.length - 1; i++) {
2680
+ const key = keys[i];
2681
+ if (typeof current[key] !== "object" || current[key] === null) {
2682
+ current[key] = {};
2683
+ }
2684
+ current = current[key];
2685
+ }
2686
+ current[keys[keys.length - 1]] = value;
2687
+ }
2688
+ function readNestedKey(obj, keyPath) {
2689
+ let current = obj;
2690
+ for (const key of keyPath.split(".")) {
2691
+ if (typeof current !== "object" || current === null || !(key in current)) {
2692
+ return void 0;
2693
+ }
2694
+ current = current[key];
2695
+ }
2696
+ return current;
2697
+ }
2698
+ function verifyGeminiConfig(config, configPath, ctx) {
2699
+ const checks = [];
2700
+ const entry = readNestedKey(config, GEMINI_SELECTOR);
2701
+ checks.push({
2702
+ name: "Gemini config exists",
2703
+ passed: fs12.existsSync(configPath),
2704
+ message: fs12.existsSync(configPath) ? void 0 : `${configPath} not found`
2705
+ });
2706
+ checks.push({
2707
+ name: "tap-comms entry present",
2708
+ passed: !!entry,
2709
+ message: entry ? void 0 : `${GEMINI_SELECTOR} not found`
2710
+ });
2711
+ checks.push({
2712
+ name: "Comms directory exists",
2713
+ passed: fs12.existsSync(ctx.commsDir),
2714
+ message: fs12.existsSync(ctx.commsDir) ? void 0 : `${ctx.commsDir} not found`
2715
+ });
2716
+ if (entry?.env && typeof entry.env === "object") {
2717
+ checks.push({
2718
+ name: "TAP_COMMS_DIR configured",
2719
+ passed: entry.env.TAP_COMMS_DIR === ctx.commsDir.replace(/\\/g, "/"),
2720
+ message: `Expected ${ctx.commsDir.replace(/\\/g, "/")}`
2721
+ });
2722
+ }
2723
+ return checks;
2724
+ }
2725
+ var GEMINI_SELECTOR, geminiAdapter;
2726
+ var init_gemini = __esm({
2727
+ "src/adapters/gemini.ts"() {
2728
+ "use strict";
2729
+ init_state();
2730
+ init_artifact_backups();
2731
+ init_common();
2732
+ GEMINI_SELECTOR = "mcpServers.tap-comms";
2733
+ geminiAdapter = {
2734
+ runtime: "gemini",
2735
+ async probe(ctx) {
2736
+ const warnings = [];
2737
+ const issues = [];
2738
+ const configPath = chooseGeminiConfigPath(ctx);
2739
+ const configExists = fs12.existsSync(configPath);
2740
+ const runtimeProbe = probeCommand(
2741
+ ctx.platform === "win32" ? ["gemini", "gemini.cmd"] : ["gemini"]
2742
+ );
2743
+ if (!runtimeProbe.command) {
2744
+ warnings.push(
2745
+ "Gemini CLI not found in PATH. Config can still be written, but runtime verification will be limited."
2746
+ );
2747
+ }
2748
+ if (!fs12.existsSync(ctx.commsDir)) {
2749
+ issues.push(
2750
+ `Comms directory not found: ${ctx.commsDir}. Run "init" first.`
2751
+ );
2752
+ }
2753
+ const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
2754
+ warnings.push(...managed.warnings);
2755
+ issues.push(...managed.issues);
2756
+ return {
2757
+ installed: true,
2758
+ configPath,
2759
+ configExists,
2760
+ runtimeCommand: runtimeProbe.command,
2761
+ version: runtimeProbe.version,
2762
+ canWrite: canWriteOrCreate(configPath),
2763
+ warnings,
2764
+ issues
2765
+ };
2766
+ },
2767
+ async plan(ctx, probe) {
2768
+ const configPath = probe.configPath ?? chooseGeminiConfigPath(ctx);
2769
+ const conflicts = [];
2770
+ const warnings = [];
2771
+ const operations = [];
2772
+ const ownedArtifacts = [
2773
+ { kind: "json-path", path: configPath, selector: GEMINI_SELECTOR }
2774
+ ];
2775
+ if (probe.configExists) {
2776
+ try {
2777
+ const config = readJsonFile(configPath);
2778
+ if (readNestedKey(config, GEMINI_SELECTOR) !== void 0) {
2779
+ conflicts.push(`Existing ${GEMINI_SELECTOR} entry will be updated.`);
2780
+ }
2781
+ } catch {
2782
+ warnings.push(
2783
+ `${configPath} exists but is not valid JSON. It will be replaced.`
2784
+ );
2785
+ }
2786
+ }
2787
+ operations.push({
2788
+ type: probe.configExists ? "merge" : "set",
2789
+ path: configPath,
2790
+ key: GEMINI_SELECTOR
2791
+ });
2792
+ return {
2793
+ runtime: "gemini",
2794
+ operations,
2795
+ ownedArtifacts,
2796
+ backupDir: ensureBackupDir(ctx.stateDir, "gemini"),
2797
+ restartRequired: true,
2798
+ conflicts,
2799
+ warnings
2800
+ };
2801
+ },
2802
+ async apply(ctx, plan) {
2803
+ const configPath = plan.operations[0]?.path ?? chooseGeminiConfigPath(ctx);
2804
+ const warnings = [];
2805
+ const changedFiles = [];
2806
+ const managed = buildManagedMcpServerSpec(ctx, ctx.instanceId);
2807
+ warnings.push(...managed.warnings);
2808
+ if (managed.issues.length > 0 || !managed.command) {
2809
+ return {
2810
+ success: false,
2811
+ appliedOps: 0,
2812
+ backupCreated: false,
2813
+ lastAppliedHash: "",
2814
+ ownedArtifacts: [],
2815
+ changedFiles,
2816
+ restartRequired: false,
2817
+ warnings: [...managed.warnings, ...managed.issues]
2818
+ };
2819
+ }
2820
+ let config = {};
2821
+ let previousValue = void 0;
2822
+ if (fs12.existsSync(configPath)) {
2823
+ if (fs12.readFileSync(configPath, "utf-8").trim()) {
2824
+ backupFile(configPath, plan.backupDir);
2825
+ }
2826
+ try {
2827
+ config = readJsonFile(configPath);
2828
+ } catch {
2829
+ warnings.push(
2830
+ `${configPath} was invalid JSON. Created backup and starting fresh.`
2831
+ );
2832
+ config = {};
2833
+ }
2834
+ previousValue = readNestedKey(config, GEMINI_SELECTOR);
2835
+ }
2836
+ const artifact = plan.ownedArtifacts[0];
2837
+ const backupPath = artifactBackupPath(
2838
+ plan.backupDir,
2839
+ artifact.kind,
2840
+ artifact.selector
2841
+ );
2842
+ writeArtifactBackup(backupPath, {
2843
+ kind: "json-path",
2844
+ selector: artifact.selector,
2845
+ existed: previousValue !== void 0,
2846
+ value: previousValue
2847
+ });
2848
+ setNestedKey2(config, GEMINI_SELECTOR, {
2849
+ command: managed.command,
2850
+ args: managed.args,
2851
+ env: managed.env
2852
+ });
2853
+ fs12.mkdirSync(path12.dirname(configPath), { recursive: true });
2854
+ const tmp = `${configPath}.tmp.${process.pid}`;
2855
+ fs12.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", "utf-8");
2856
+ fs12.renameSync(tmp, configPath);
2857
+ changedFiles.push(configPath);
2858
+ return {
2859
+ success: true,
2860
+ appliedOps: plan.operations.length,
2861
+ backupCreated: true,
2862
+ lastAppliedHash: fileHash(configPath),
2863
+ ownedArtifacts: [{ ...artifact, backupPath }],
2864
+ changedFiles,
2865
+ restartRequired: true,
2866
+ warnings
2867
+ };
2868
+ },
2869
+ async verify(ctx, plan) {
2870
+ const warnings = [];
2871
+ const configPath = plan.operations[0]?.path ?? chooseGeminiConfigPath(ctx);
2872
+ const runtimeProbe = probeCommand(
2873
+ ctx.platform === "win32" ? ["gemini", "gemini.cmd"] : ["gemini"]
2874
+ );
2875
+ let checks;
2876
+ try {
2877
+ const config = readJsonFile(configPath);
2878
+ checks = verifyGeminiConfig(config, configPath, ctx);
2879
+ } catch {
2880
+ checks = [
2881
+ {
2882
+ name: "Gemini config is valid JSON",
2883
+ passed: false,
2884
+ message: "Parse error"
2885
+ }
2886
+ ];
2887
+ }
2888
+ checks.push({
2889
+ name: "Gemini CLI found",
2890
+ passed: !!runtimeProbe.command,
2891
+ message: runtimeProbe.command ? void 0 : "gemini not in PATH (non-blocking)"
2892
+ });
2893
+ if (!runtimeProbe.command) {
2894
+ warnings.push(
2895
+ "Gemini CLI not in PATH. Config is written, but runtime verification is partial."
2896
+ );
2897
+ }
2898
+ return {
2899
+ ok: checks.filter((check) => check.name !== "Gemini CLI found").every((check) => check.passed),
2900
+ checks,
2901
+ restartRequired: true,
2902
+ warnings
2903
+ };
2904
+ },
2905
+ bridgeMode() {
2906
+ return "polling";
2907
+ }
2908
+ };
2909
+ }
2910
+ });
2911
+
2912
+ // src/adapters/index.ts
2913
+ function getAdapter(runtime) {
2914
+ const adapter = adapters[runtime];
2915
+ if (!adapter) {
2916
+ throw new Error(
2917
+ `Adapter for "${runtime}" is not yet available. Supported: ${Object.keys(adapters).join(", ")}`
2918
+ );
2919
+ }
2920
+ return adapter;
2921
+ }
2922
+ var adapters;
2923
+ var init_adapters = __esm({
2924
+ "src/adapters/index.ts"() {
2925
+ "use strict";
2926
+ init_claude();
2927
+ init_codex();
2928
+ init_gemini();
2929
+ adapters = {
2930
+ claude: claudeAdapter,
2931
+ codex: codexAdapter,
2932
+ gemini: geminiAdapter
2933
+ };
2934
+ }
2935
+ });
2936
+
2937
+ // src/commands/bridge.ts
2938
+ import * as path13 from "path";
2939
+ function formatAge(seconds) {
2940
+ if (seconds < 60) return `${seconds}s ago`;
2941
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
2942
+ return `${Math.floor(seconds / 3600)}h ${Math.floor(seconds % 3600 / 60)}m ago`;
2943
+ }
2944
+ function formatAppServerState(appServer) {
2945
+ const ownership = appServer.managed ? "managed" : "external";
2946
+ const pid = appServer.pid != null ? ` pid:${appServer.pid}` : "";
2947
+ const health = appServer.healthy ? "healthy" : "unhealthy";
2948
+ const auth = appServer.auth != null ? `, auth gateway:${appServer.auth.gatewayPid ?? "-"} -> ${appServer.auth.upstreamUrl}` : "";
2949
+ return `${health}, ${ownership}${pid}, ${appServer.url}${auth}`;
2950
+ }
2951
+ function redactProtectedUrl(url) {
2952
+ try {
2953
+ const parsed = new URL(url);
2954
+ if (parsed.searchParams.has("tap_token")) {
2955
+ parsed.searchParams.set("tap_token", "***");
2956
+ }
2957
+ return parsed.toString().replace(/\/$/, "");
2958
+ } catch {
2959
+ return url.replace(/tap_token=[^&]+/g, "tap_token=***");
2960
+ }
2961
+ }
2962
+ function loadCurrentBridgeState(stateDir, instanceId, fallback) {
2963
+ return loadBridgeState(stateDir, instanceId) ?? fallback ?? null;
2964
+ }
2965
+ function getSharedAppServerUsers(state, stateDir, currentInstanceId, appServerUrl) {
2966
+ const shared = [];
2967
+ for (const [id, inst] of Object.entries(state.instances)) {
2968
+ if (id === currentInstanceId || !inst?.installed) {
2969
+ continue;
2970
+ }
2971
+ const instanceId = id;
2972
+ if (getBridgeStatus(stateDir, instanceId) !== "running") {
2973
+ continue;
2974
+ }
2975
+ const bridgeState = loadCurrentBridgeState(
2976
+ stateDir,
2977
+ instanceId,
2978
+ inst.bridge
2979
+ );
2980
+ if (bridgeState?.appServer?.url === appServerUrl) {
2981
+ shared.push(instanceId);
2982
+ }
2983
+ }
2984
+ return shared;
2985
+ }
2986
+ function transferManagedAppServerOwnership(state, stateDir, recipientId, appServer) {
2987
+ const recipient = state.instances[recipientId];
2988
+ if (!recipient) {
2989
+ return false;
2990
+ }
2991
+ const bridgeState = loadCurrentBridgeState(
2992
+ stateDir,
2993
+ recipientId,
2994
+ recipient.bridge
2995
+ );
2996
+ if (!bridgeState) {
2997
+ return false;
2998
+ }
2999
+ const transferredAppServer = {
3000
+ ...appServer,
3001
+ managed: true,
3002
+ healthy: true,
3003
+ lastCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
3004
+ lastHealthyAt: appServer.lastHealthyAt ?? (/* @__PURE__ */ new Date()).toISOString()
3005
+ };
3006
+ const updatedBridge = {
3007
+ ...bridgeState,
3008
+ appServer: transferredAppServer
3009
+ };
3010
+ saveBridgeState(stateDir, recipientId, updatedBridge);
3011
+ state.instances[recipientId] = {
3012
+ ...recipient,
3013
+ bridge: updatedBridge
3014
+ };
3015
+ return true;
3016
+ }
3017
+ async function bridgeStart(identifier, agentName, flags = {}) {
3018
+ const repoRoot = findRepoRoot();
3019
+ let state = loadState(repoRoot);
3020
+ if (!state) {
3021
+ return {
3022
+ ok: false,
3023
+ command: "bridge",
3024
+ code: "TAP_NOT_INITIALIZED",
3025
+ message: "Not initialized. Run: npx @hua-labs/tap init",
3026
+ warnings: [],
3027
+ data: {}
3028
+ };
3029
+ }
3030
+ const resolved = resolveInstanceId(identifier, state);
3031
+ if (!resolved.ok) {
3032
+ return {
3033
+ ok: false,
3034
+ command: "bridge",
3035
+ code: resolved.code,
3036
+ message: resolved.message,
3037
+ warnings: [],
3038
+ data: {}
3039
+ };
3040
+ }
3041
+ const instanceId = resolved.instanceId;
3042
+ let instance = state.instances[instanceId];
3043
+ if (!instance?.installed) {
3044
+ return {
3045
+ ok: false,
3046
+ command: "bridge",
3047
+ instanceId,
3048
+ runtime: instance?.runtime,
3049
+ code: "TAP_INSTANCE_NOT_FOUND",
3050
+ message: `${instanceId} is not installed. Run: npx @hua-labs/tap add ${instance?.runtime ?? identifier}`,
3051
+ warnings: [],
3052
+ data: {}
3053
+ };
3054
+ }
3055
+ const adapter = getAdapter(instance.runtime);
3056
+ const mode = adapter.bridgeMode();
3057
+ if (mode !== "app-server") {
3058
+ return {
3059
+ ok: true,
3060
+ command: "bridge",
3061
+ instanceId,
3062
+ runtime: instance.runtime,
3063
+ code: "TAP_NO_OP",
3064
+ message: `${instanceId} uses ${mode} mode \u2014 no bridge needed.`,
3065
+ warnings: [],
3066
+ data: { bridgeMode: mode }
3067
+ };
3068
+ }
3069
+ const resolvedAgentName = agentName ?? instance.agentName ?? void 0;
3070
+ if (agentName && agentName !== instance.agentName) {
3071
+ instance = { ...instance, agentName };
3072
+ const updatedState = updateInstanceState(state, instanceId, instance);
3073
+ saveState(repoRoot, updatedState);
3074
+ state = updatedState;
3075
+ }
3076
+ const ctx = createAdapterContext(state.commsDir, repoRoot);
3077
+ const bridgeScript = adapter.resolveBridgeScript?.(ctx);
3078
+ if (!bridgeScript) {
3079
+ return {
3080
+ ok: false,
3081
+ command: "bridge",
3082
+ instanceId,
3083
+ runtime: instance.runtime,
3084
+ code: "TAP_BRIDGE_SCRIPT_MISSING",
3085
+ message: `Bridge script not found for ${instanceId}. Ensure the runtime is properly configured.`,
3086
+ warnings: [],
3087
+ data: {}
3088
+ };
3089
+ }
3090
+ const { config: resolvedConfig } = resolveConfig({}, repoRoot);
3091
+ const runtimeCommand = resolvedConfig.runtimeCommand;
3092
+ const manageAppServer = instance.runtime === "codex" && flags["no-server"] !== true;
3093
+ let effectivePort = instance.port;
3094
+ if (effectivePort == null && manageAppServer) {
3095
+ effectivePort = await findNextAvailableAppServerPort(
3096
+ state,
3097
+ resolvedConfig.appServerUrl,
3098
+ 4501,
3099
+ instanceId
3100
+ );
3101
+ instance = { ...instance, port: effectivePort };
3102
+ const updatedState = updateInstanceState(state, instanceId, instance);
3103
+ saveState(repoRoot, updatedState);
3104
+ state = updatedState;
3105
+ }
3106
+ const appServerUrl = resolveAppServerUrl(
3107
+ resolvedConfig.appServerUrl,
3108
+ effectivePort ?? void 0
3109
+ );
3110
+ logHeader(`@hua-labs/tap bridge start ${instanceId}`);
3111
+ log(`Bridge script: ${bridgeScript}`);
3112
+ log(`Bridge mode: ${mode}`);
3113
+ log(`Runtime cmd: ${runtimeCommand}`);
3114
+ log(`App server: ${appServerUrl}`);
3115
+ if (effectivePort != null) log(`Port: ${effectivePort}`);
3116
+ if (resolvedAgentName) log(`Agent name: ${resolvedAgentName}`);
3117
+ const noAuth = flags["no-auth"] === true;
3118
+ if (!manageAppServer && instance.runtime === "codex") {
3119
+ log("Auto server: disabled (--no-server)");
3120
+ }
3121
+ if (noAuth && manageAppServer) {
3122
+ log("Auth gateway: disabled (--no-auth)");
3123
+ }
3124
+ const willBeHeadless = flags["headless"] === true || instance.headless?.enabled;
3125
+ if (willBeHeadless) {
3126
+ const role = (typeof flags["role"] === "string" ? flags["role"] : null) ?? instance.headless?.role ?? "reviewer";
3127
+ log(`Headless: ${role}`);
3128
+ }
3129
+ try {
3130
+ if (!manageAppServer && instance.runtime === "codex") {
3131
+ log("Checking app-server health...");
3132
+ const healthy = await checkAppServerHealth(appServerUrl);
3133
+ if (healthy) {
3134
+ logSuccess("App server reachable");
3135
+ } else {
3136
+ logError(`App server not reachable at ${appServerUrl}`);
3137
+ return {
3138
+ ok: false,
3139
+ command: "bridge",
3140
+ instanceId,
3141
+ runtime: instance.runtime,
3142
+ code: "TAP_BRIDGE_START_FAILED",
3143
+ message: `App server not reachable at ${appServerUrl}. Start it first: codex app-server --listen ${appServerUrl}`,
3144
+ warnings: [],
3145
+ data: {}
3146
+ };
3147
+ }
3148
+ }
3149
+ const busyModeRaw = flags["busy-mode"];
3150
+ if (busyModeRaw !== void 0 && busyModeRaw !== "steer" && busyModeRaw !== "wait") {
3151
+ return {
3152
+ ok: false,
3153
+ command: "bridge",
3154
+ instanceId,
3155
+ runtime: instance.runtime,
3156
+ code: "TAP_INVALID_ARGUMENT",
3157
+ message: `Invalid --busy-mode: ${String(busyModeRaw)}. Must be "steer" or "wait".`,
3158
+ warnings: [],
3159
+ data: {}
3160
+ };
3161
+ }
3162
+ const busyMode = busyModeRaw;
3163
+ const pollSeconds = typeof flags["poll-seconds"] === "string" ? parseInt(flags["poll-seconds"], 10) : void 0;
3164
+ const reconnectSeconds = typeof flags["reconnect-seconds"] === "string" ? parseInt(flags["reconnect-seconds"], 10) : void 0;
3165
+ const messageLookbackMinutes = typeof flags["message-lookback-minutes"] === "string" ? parseInt(flags["message-lookback-minutes"], 10) : void 0;
3166
+ const threadId = typeof flags["thread-id"] === "string" ? flags["thread-id"] : void 0;
3167
+ const ephemeral = flags["ephemeral"] === true;
3168
+ const processExistingMessages = flags["process-existing-messages"] === true;
3169
+ const headlessFlag = flags["headless"] === true;
3170
+ const roleArg = typeof flags["role"] === "string" ? flags["role"] : void 0;
3171
+ const validRoles = ["reviewer", "validator", "long-running"];
3172
+ if (roleArg && !validRoles.includes(roleArg)) {
3173
+ return {
3174
+ ok: false,
3175
+ command: "bridge",
3176
+ instanceId,
3177
+ runtime: instance.runtime,
3178
+ code: "TAP_INVALID_ARGUMENT",
3179
+ message: `Invalid --role: ${roleArg}. Must be: ${validRoles.join(", ")}`,
3180
+ warnings: [],
3181
+ data: {}
3182
+ };
3183
+ }
3184
+ const headless = headlessFlag ? {
3185
+ enabled: true,
3186
+ role: roleArg ?? "reviewer",
3187
+ maxRounds: 5,
3188
+ qualitySeverityFloor: "high"
3189
+ } : instance.headless;
3190
+ const bridge = await startBridge({
3191
+ instanceId,
3192
+ runtime: instance.runtime,
3193
+ stateDir: ctx.stateDir,
3194
+ commsDir: ctx.commsDir,
3195
+ bridgeScript,
3196
+ platform: ctx.platform,
3197
+ agentName: resolvedAgentName,
3198
+ runtimeCommand,
3199
+ appServerUrl,
3200
+ repoRoot,
3201
+ port: effectivePort ?? void 0,
3202
+ manageAppServer,
3203
+ noAuth,
3204
+ headless,
3205
+ busyMode,
3206
+ pollSeconds,
3207
+ reconnectSeconds,
3208
+ messageLookbackMinutes,
3209
+ threadId,
3210
+ ephemeral,
3211
+ processExistingMessages
3212
+ });
3213
+ logSuccess(`Bridge started (PID: ${bridge.pid})`);
3214
+ log(`Log: ${path13.join(ctx.stateDir, "logs", `bridge-${instanceId}.log`)}`);
3215
+ if (bridge.appServer) {
3216
+ log(`App server: ${formatAppServerState(bridge.appServer)}`);
3217
+ if (bridge.appServer.logPath) {
3218
+ log(`Server log: ${bridge.appServer.logPath}`);
3219
+ }
3220
+ if (bridge.appServer.auth) {
3221
+ log(
3222
+ `Protected: ${redactProtectedUrl(bridge.appServer.auth.protectedUrl)}`
3223
+ );
3224
+ if (bridge.appServer.auth.gatewayLogPath) {
3225
+ log(`Gateway log: ${bridge.appServer.auth.gatewayLogPath}`);
3226
+ }
3227
+ log(`TUI connect: ${bridge.appServer.auth.upstreamUrl}`);
3228
+ }
3229
+ if (bridge.appServer.managed && !bridge.appServer.auth) {
3230
+ log(`TUI connect: ${bridge.appServer.url}`);
3231
+ }
3232
+ }
3233
+ const updated = { ...instance, bridge };
3234
+ const newState = updateInstanceState(state, instanceId, updated);
3235
+ saveState(repoRoot, newState);
3236
+ return {
3237
+ ok: true,
3238
+ command: "bridge",
3239
+ instanceId,
3240
+ runtime: instance.runtime,
3241
+ code: "TAP_BRIDGE_START_OK",
3242
+ message: `Bridge for ${instanceId} started (PID: ${bridge.pid})`,
3243
+ warnings: [],
3244
+ data: { pid: bridge.pid, appServer: bridge.appServer ?? null }
3245
+ };
3246
+ } catch (err) {
3247
+ const msg = err instanceof Error ? err.message : String(err);
3248
+ logError(msg);
3249
+ return {
3250
+ ok: false,
3251
+ command: "bridge",
3252
+ instanceId,
3253
+ runtime: instance.runtime,
3254
+ code: "TAP_BRIDGE_START_FAILED",
3255
+ message: msg,
3256
+ warnings: [],
3257
+ data: {}
3258
+ };
3259
+ }
3260
+ }
3261
+ async function bridgeStartAll(flags = {}) {
3262
+ const repoRoot = findRepoRoot();
3263
+ const state = loadState(repoRoot);
3264
+ if (!state) {
3265
+ return {
3266
+ ok: false,
3267
+ command: "bridge",
3268
+ code: "TAP_NOT_INITIALIZED",
3269
+ message: "Not initialized. Run: npx @hua-labs/tap init",
3270
+ warnings: [],
3271
+ data: {}
3272
+ };
3273
+ }
3274
+ const instanceIds = Object.keys(state.instances);
3275
+ const appServerInstances = instanceIds.filter((id) => {
3276
+ const inst = state.instances[id];
3277
+ if (!inst?.installed) return false;
3278
+ const adapter = getAdapter(inst.runtime);
3279
+ return adapter.bridgeMode() === "app-server";
3280
+ });
3281
+ if (appServerInstances.length === 0) {
3282
+ return {
3283
+ ok: true,
3284
+ command: "bridge",
3285
+ code: "TAP_NO_OP",
3286
+ message: "No app-server instances found to start.",
3287
+ warnings: [],
3288
+ data: {}
3289
+ };
3290
+ }
3291
+ logHeader("@hua-labs/tap bridge start --all");
3292
+ log(
3293
+ `Found ${appServerInstances.length} app-server instance(s): ${appServerInstances.join(", ")}`
3294
+ );
3295
+ log("");
3296
+ const started = [];
3297
+ const failed = [];
3298
+ const warnings = [];
3299
+ for (const instanceId of appServerInstances) {
3300
+ const inst = state.instances[instanceId];
3301
+ const storedName = inst?.agentName ?? void 0;
3302
+ if (!storedName) {
3303
+ const msg = `${instanceId}: skipped \u2014 no stored agent-name. Set it first: tap bridge start ${instanceId} --agent-name <name>`;
3304
+ log(msg);
3305
+ warnings.push(msg);
3306
+ continue;
3307
+ }
3308
+ log(`Starting ${instanceId} (agent: ${storedName})...`);
3309
+ const result = await bridgeStart(instanceId, storedName, flags);
3310
+ if (result.ok) {
3311
+ started.push(instanceId);
3312
+ logSuccess(`${instanceId} started`);
3313
+ } else {
3314
+ failed.push(instanceId);
3315
+ logError(`${instanceId}: ${result.message}`);
3316
+ }
3317
+ log("");
3318
+ }
3319
+ 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(", ")}`;
3320
+ return {
3321
+ ok: failed.length === 0 && started.length > 0,
3322
+ command: "bridge",
3323
+ code: started.length > 0 ? "TAP_BRIDGE_START_OK" : "TAP_BRIDGE_START_FAILED",
3324
+ message,
3325
+ warnings,
3326
+ data: { started, failed }
3327
+ };
3328
+ }
3329
+ async function bridgeStopOne(identifier) {
3330
+ const repoRoot = findRepoRoot();
3331
+ const state = loadState(repoRoot);
3332
+ if (!state) {
3333
+ return {
3334
+ ok: false,
3335
+ command: "bridge",
3336
+ code: "TAP_NOT_INITIALIZED",
3337
+ message: "Not initialized. Run: npx @hua-labs/tap init",
3338
+ warnings: [],
3339
+ data: {}
3340
+ };
3341
+ }
3342
+ const resolved = resolveInstanceId(identifier, state);
3343
+ if (!resolved.ok) {
3344
+ return {
3345
+ ok: false,
3346
+ command: "bridge",
3347
+ code: resolved.code,
3348
+ message: resolved.message,
3349
+ warnings: [],
3350
+ data: {}
3351
+ };
3352
+ }
3353
+ const instanceId = resolved.instanceId;
3354
+ const ctx = createAdapterContext(state.commsDir, repoRoot);
3355
+ const instance = state.instances[instanceId];
3356
+ const bridgeState = loadCurrentBridgeState(
3357
+ ctx.stateDir,
3358
+ instanceId,
3359
+ instance?.bridge
3360
+ );
3361
+ const appServer = bridgeState?.appServer ?? null;
3362
+ logHeader(`@hua-labs/tap bridge stop ${instanceId}`);
3363
+ const stopped = await stopBridge({
3364
+ instanceId,
3365
+ stateDir: ctx.stateDir,
3366
+ platform: ctx.platform
3367
+ });
3368
+ let appServerStopped = false;
3369
+ let appServerTransferredTo = null;
3370
+ if (stopped) {
3371
+ logSuccess(`Bridge for ${instanceId} stopped`);
3372
+ } else {
3373
+ log(`No running bridge for ${instanceId}`);
3374
+ }
3375
+ if (appServer?.managed) {
3376
+ const sharedUsers = getSharedAppServerUsers(
3377
+ state,
3378
+ ctx.stateDir,
3379
+ instanceId,
3380
+ appServer.url
3381
+ );
3382
+ if (sharedUsers.length > 0) {
3383
+ const recipient = sharedUsers[0];
3384
+ if (transferManagedAppServerOwnership(
3385
+ state,
3386
+ ctx.stateDir,
3387
+ recipient,
3388
+ appServer
3389
+ )) {
3390
+ appServerTransferredTo = recipient;
3391
+ log(`Managed app-server ownership moved to ${recipient}`);
3392
+ } else {
3393
+ log(
3394
+ `Managed app-server left running at ${appServer.url} because ownership transfer failed`
3395
+ );
3396
+ }
3397
+ } else {
3398
+ appServerStopped = await stopManagedAppServer(appServer, ctx.platform);
3399
+ if (appServerStopped) {
3400
+ const gatewayNote = appServer.auth?.gatewayPid != null ? `, gateway PID: ${appServer.auth.gatewayPid}` : "";
3401
+ logSuccess(
3402
+ `Managed app-server stopped (PID: ${appServer.pid ?? "-"}${gatewayNote})`
3403
+ );
3404
+ }
3405
+ }
3406
+ }
3407
+ if (instance) {
3408
+ const updated = { ...instance, bridge: null };
3409
+ const newState = updateInstanceState(state, instanceId, updated);
3410
+ saveState(repoRoot, newState);
3411
+ }
3412
+ if (stopped) {
3413
+ return {
3414
+ ok: true,
3415
+ command: "bridge",
3416
+ instanceId,
3417
+ code: "TAP_BRIDGE_STOP_OK",
3418
+ message: `Bridge for ${instanceId} stopped`,
3419
+ warnings: [],
3420
+ data: {
3421
+ appServerStopped,
3422
+ appServerTransferredTo
3423
+ }
3424
+ };
3425
+ }
3426
+ return {
3427
+ ok: true,
3428
+ command: "bridge",
3429
+ instanceId,
3430
+ code: "TAP_BRIDGE_NOT_RUNNING",
3431
+ message: `No running bridge for ${instanceId}`,
3432
+ warnings: [],
3433
+ data: {
3434
+ appServerStopped,
3435
+ appServerTransferredTo
3436
+ }
3437
+ };
3438
+ }
3439
+ async function bridgeStopAll() {
3440
+ const repoRoot = findRepoRoot();
3441
+ const state = loadState(repoRoot);
3442
+ if (!state) {
3443
+ return {
3444
+ ok: false,
3445
+ command: "bridge",
3446
+ code: "TAP_NOT_INITIALIZED",
3447
+ message: "Not initialized. Run: npx @hua-labs/tap init",
3448
+ warnings: [],
3449
+ data: {}
3450
+ };
3451
+ }
3452
+ const ctx = createAdapterContext(state.commsDir, repoRoot);
3453
+ const instanceIds = Object.keys(state.instances);
3454
+ const stopped = [];
3455
+ const managedAppServers = /* @__PURE__ */ new Map();
3456
+ logHeader("@hua-labs/tap bridge stop (all)");
3457
+ let stateChanged = false;
3458
+ for (const instanceId of instanceIds) {
3459
+ const bridgeState = loadCurrentBridgeState(
3460
+ ctx.stateDir,
3461
+ instanceId,
3462
+ state.instances[instanceId]?.bridge
3463
+ );
3464
+ const appServer = bridgeState?.appServer;
3465
+ if (appServer?.managed && appServer.pid != null) {
3466
+ managedAppServers.set(
3467
+ `${appServer.url}:${appServer.pid}:${appServer.auth?.gatewayPid ?? "-"}`,
3468
+ appServer
3469
+ );
3470
+ }
3471
+ const didStop = await stopBridge({
3472
+ instanceId,
3473
+ stateDir: ctx.stateDir,
3474
+ platform: ctx.platform
3475
+ });
3476
+ if (didStop) {
3477
+ logSuccess(`Stopped bridge for ${instanceId}`);
3478
+ stopped.push(instanceId);
3479
+ }
3480
+ const instance = state.instances[instanceId];
3481
+ if (instance?.bridge) {
3482
+ state.instances[instanceId] = { ...instance, bridge: null };
3483
+ stateChanged = true;
3484
+ }
3485
+ }
3486
+ const stoppedAppServers = [];
3487
+ for (const appServer of managedAppServers.values()) {
3488
+ if (await stopManagedAppServer(appServer, ctx.platform)) {
3489
+ stoppedAppServers.push(appServer.pid);
3490
+ const gatewayNote = appServer.auth?.gatewayPid != null ? `, gateway PID ${appServer.auth.gatewayPid}` : "";
3491
+ logSuccess(
3492
+ `Stopped app-server PID ${appServer.pid} (${appServer.url}${gatewayNote})`
3493
+ );
3494
+ }
3495
+ }
3496
+ if (stateChanged) {
3497
+ state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3498
+ saveState(repoRoot, state);
3499
+ }
3500
+ const message = stopped.length > 0 ? `Stopped ${stopped.length} bridge(s): ${stopped.join(", ")}` : "No running bridges found";
3501
+ log(message);
3502
+ return {
3503
+ ok: true,
3504
+ command: "bridge",
3505
+ code: stopped.length > 0 ? "TAP_BRIDGE_STOP_OK" : "TAP_BRIDGE_NOT_RUNNING",
3506
+ message,
3507
+ warnings: [],
3508
+ data: { stopped, stoppedAppServers }
3509
+ };
3510
+ }
3511
+ function bridgeStatusAll() {
3512
+ const repoRoot = findRepoRoot();
3513
+ const state = loadState(repoRoot);
3514
+ if (!state) {
3515
+ return {
3516
+ ok: false,
3517
+ command: "bridge",
3518
+ code: "TAP_NOT_INITIALIZED",
3519
+ message: "Not initialized. Run: npx @hua-labs/tap init",
3520
+ warnings: [],
3521
+ data: {}
3522
+ };
3523
+ }
3524
+ const { config: resolvedCfg } = resolveConfig({}, repoRoot);
3525
+ const stateDir = resolvedCfg.stateDir;
3526
+ const instanceIds = Object.keys(state.instances);
3527
+ const bridges = {};
3528
+ logHeader("@hua-labs/tap bridge status");
3529
+ log(
3530
+ `${"Instance".padEnd(20)} ${"Runtime".padEnd(8)} ${"Status".padEnd(10)} ${"PID".padEnd(8)} ${"Port".padEnd(6)} ${"Last Heartbeat"}`
3531
+ );
3532
+ log(
3533
+ `${"\u2500".repeat(20)} ${"\u2500".repeat(8)} ${"\u2500".repeat(10)} ${"\u2500".repeat(8)} ${"\u2500".repeat(6)} ${"\u2500".repeat(20)}`
3534
+ );
3535
+ for (const instanceId of instanceIds) {
3536
+ const inst = state.instances[instanceId];
3537
+ if (!inst?.installed) continue;
3538
+ if (inst.bridgeMode !== "app-server") {
3539
+ log(
3540
+ `${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${"n/a".padEnd(10)} ${"-".padEnd(8)} ${"-".padEnd(6)} ${inst.bridgeMode} mode`
3541
+ );
3542
+ bridges[instanceId] = {
3543
+ status: "n/a",
3544
+ runtime: inst.runtime,
3545
+ pid: null,
3546
+ port: inst.port,
3547
+ lastHeartbeat: null,
3548
+ appServer: null
3549
+ };
3550
+ continue;
3551
+ }
3552
+ const status = getBridgeStatus(stateDir, instanceId);
3553
+ const bridgeState = loadBridgeState(stateDir, instanceId);
3554
+ const age = getHeartbeatAge(stateDir, instanceId);
3555
+ const pid = bridgeState?.pid ?? null;
3556
+ const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
3557
+ const pidStr = pid ? String(pid) : "-";
3558
+ const portStr = inst.port ? String(inst.port) : "-";
3559
+ const ageStr = age !== null ? formatAge(age) : "-";
3560
+ const statusColor = status === "running" ? "running" : status === "stale" ? "stale!" : "stopped";
3561
+ log(
3562
+ `${instanceId.padEnd(20)} ${inst.runtime.padEnd(8)} ${statusColor.padEnd(10)} ${pidStr.padEnd(8)} ${portStr.padEnd(6)} ${ageStr}`
3563
+ );
3564
+ if (bridgeState?.appServer) {
3565
+ log(` App server: ${formatAppServerState(bridgeState.appServer)}`);
3566
+ if (bridgeState.appServer.logPath) {
3567
+ log(` Server log: ${bridgeState.appServer.logPath}`);
3568
+ }
3569
+ if (bridgeState.appServer.auth) {
3570
+ log(
3571
+ ` Protected: ${redactProtectedUrl(bridgeState.appServer.auth.protectedUrl)}`
3572
+ );
3573
+ }
3574
+ }
3575
+ bridges[instanceId] = {
3576
+ status,
3577
+ runtime: inst.runtime,
3578
+ pid,
3579
+ port: inst.port,
3580
+ lastHeartbeat: heartbeat,
3581
+ appServer: bridgeState?.appServer ?? null
3582
+ };
3583
+ }
3584
+ if (instanceIds.length === 0) {
3585
+ log("No instances installed.");
3586
+ }
3587
+ log("");
3588
+ return {
3589
+ ok: true,
3590
+ command: "bridge",
3591
+ code: "TAP_BRIDGE_STATUS_OK",
3592
+ message: `${instanceIds.length} instance(s) checked`,
3593
+ warnings: [],
3594
+ data: { bridges }
3595
+ };
3596
+ }
3597
+ function bridgeStatusOne(identifier) {
3598
+ const repoRoot = findRepoRoot();
3599
+ const state = loadState(repoRoot);
3600
+ if (!state) {
3601
+ return {
3602
+ ok: false,
3603
+ command: "bridge",
3604
+ code: "TAP_NOT_INITIALIZED",
3605
+ message: "Not initialized. Run: npx @hua-labs/tap init",
3606
+ warnings: [],
3607
+ data: {}
3608
+ };
3609
+ }
3610
+ const resolved = resolveInstanceId(identifier, state);
3611
+ if (!resolved.ok) {
3612
+ return {
3613
+ ok: false,
3614
+ command: "bridge",
3615
+ code: resolved.code,
3616
+ message: resolved.message,
3617
+ warnings: [],
3618
+ data: {}
3619
+ };
3620
+ }
3621
+ const instanceId = resolved.instanceId;
3622
+ const inst = state.instances[instanceId];
3623
+ if (!inst?.installed) {
3624
+ return {
3625
+ ok: false,
3626
+ command: "bridge",
3627
+ instanceId,
3628
+ code: "TAP_INSTANCE_NOT_FOUND",
3629
+ message: `${instanceId} is not installed.`,
3630
+ warnings: [],
3631
+ data: {}
3632
+ };
3633
+ }
3634
+ logHeader(`@hua-labs/tap bridge status ${instanceId}`);
3635
+ log(`Instance: ${instanceId}`);
3636
+ log(`Runtime: ${inst.runtime}`);
3637
+ log(`Bridge mode: ${inst.bridgeMode}`);
3638
+ if (inst.port) log(`Port: ${inst.port}`);
3639
+ if (inst.bridgeMode !== "app-server") {
3640
+ log(`Status: n/a (${inst.bridgeMode} mode)`);
3641
+ log("");
3642
+ return {
3643
+ ok: true,
3644
+ command: "bridge",
3645
+ instanceId,
3646
+ runtime: inst.runtime,
3647
+ code: "TAP_BRIDGE_STATUS_OK",
3648
+ message: `${instanceId} bridge: n/a (${inst.bridgeMode} mode)`,
3649
+ warnings: [],
3650
+ data: {
3651
+ status: "n/a",
3652
+ bridgeMode: inst.bridgeMode,
3653
+ pid: null,
3654
+ port: inst.port,
3655
+ lastHeartbeat: null,
3656
+ appServer: null
3657
+ }
3658
+ };
3659
+ }
3660
+ const { config: resolvedCfg2 } = resolveConfig({}, repoRoot);
3661
+ const stateDir = resolvedCfg2.stateDir;
3662
+ const status = getBridgeStatus(stateDir, instanceId);
3663
+ const bridgeState = loadBridgeState(stateDir, instanceId);
3664
+ const age = getHeartbeatAge(stateDir, instanceId);
3665
+ const heartbeat = getBridgeHeartbeatTimestamp(stateDir, instanceId);
3666
+ log(`Status: ${status}`);
3667
+ if (bridgeState) {
3668
+ log(`PID: ${bridgeState.pid}`);
3669
+ log(
3670
+ `Heartbeat: ${heartbeat ?? "-"}${age !== null ? ` (${formatAge(age)})` : ""}`
3671
+ );
3672
+ log(
3673
+ `Log: ${path13.join(stateDir, "logs", `bridge-${instanceId}.log`)}`
3674
+ );
3675
+ if (bridgeState.appServer) {
3676
+ log(`App server: ${bridgeState.appServer.url}`);
3677
+ log(`Server PID: ${bridgeState.appServer.pid ?? "-"}`);
3678
+ log(
3679
+ `Server mode: ${bridgeState.appServer.managed ? "managed" : "external"}`
3680
+ );
3681
+ log(
3682
+ `Health: ${bridgeState.appServer.healthy ? "healthy" : "unhealthy"}`
3683
+ );
3684
+ log(`Checked: ${bridgeState.appServer.lastCheckedAt}`);
3685
+ if (bridgeState.appServer.logPath) {
3686
+ log(`Server log: ${bridgeState.appServer.logPath}`);
3687
+ }
3688
+ if (bridgeState.appServer.auth) {
3689
+ log(`Auth: ${bridgeState.appServer.auth.mode}`);
3690
+ log(
3691
+ `Protected: ${redactProtectedUrl(bridgeState.appServer.auth.protectedUrl)}`
3692
+ );
3693
+ log(`Upstream: ${bridgeState.appServer.auth.upstreamUrl}`);
3694
+ log(`TUI connect: ${bridgeState.appServer.auth.upstreamUrl}`);
3695
+ log(`Gateway PID: ${bridgeState.appServer.auth.gatewayPid ?? "-"}`);
3696
+ if (bridgeState.appServer.auth.gatewayLogPath) {
3697
+ log(`Gateway log: ${bridgeState.appServer.auth.gatewayLogPath}`);
3698
+ }
3699
+ } else if (bridgeState.appServer.managed) {
3700
+ log(`Auth: none (--no-auth)`);
3701
+ log(`TUI connect: ${bridgeState.appServer.url}`);
3702
+ }
3703
+ }
3704
+ }
3705
+ log("");
3706
+ return {
3707
+ ok: true,
3708
+ command: "bridge",
3709
+ instanceId,
3710
+ runtime: inst.runtime,
3711
+ code: "TAP_BRIDGE_STATUS_OK",
3712
+ message: `${instanceId} bridge: ${status}`,
3713
+ warnings: [],
3714
+ data: {
3715
+ status,
3716
+ bridgeMode: inst.bridgeMode,
3717
+ pid: bridgeState?.pid ?? null,
3718
+ port: inst.port,
3719
+ lastHeartbeat: heartbeat,
3720
+ appServer: bridgeState?.appServer ?? null
3721
+ }
3722
+ };
3723
+ }
3724
+ async function bridgeCommand(args) {
3725
+ const { positional, flags } = parseArgs(args);
3726
+ const subcommand = positional[0];
3727
+ const identifierArg = positional[1];
3728
+ const agentName = typeof flags["agent-name"] === "string" ? flags["agent-name"] : void 0;
3729
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
3730
+ log(BRIDGE_HELP);
3731
+ return {
3732
+ ok: true,
3733
+ command: "bridge",
3734
+ code: "TAP_NO_OP",
3735
+ message: BRIDGE_HELP,
3736
+ warnings: [],
3737
+ data: {}
3738
+ };
3739
+ }
3740
+ switch (subcommand) {
3741
+ case "start": {
3742
+ const wantsAll = flags["all"] === true || identifierArg === "--all";
3743
+ const hasInstance = identifierArg && identifierArg !== "--all";
3744
+ if (wantsAll && hasInstance) {
3745
+ return {
3746
+ ok: false,
3747
+ command: "bridge",
3748
+ code: "TAP_INVALID_ARGUMENT",
3749
+ message: `Cannot combine <instance> with --all. Use either:
3750
+ tap bridge start ${identifierArg}
3751
+ tap bridge start --all`,
3752
+ warnings: [],
3753
+ data: {}
3754
+ };
3755
+ }
3756
+ if (wantsAll) {
3757
+ return bridgeStartAll(flags);
3758
+ }
3759
+ if (!identifierArg) {
3760
+ return {
3761
+ ok: false,
3762
+ command: "bridge",
3763
+ code: "TAP_INVALID_ARGUMENT",
3764
+ message: "Missing instance. Usage: npx @hua-labs/tap bridge start <instance> or --all",
3765
+ warnings: [],
3766
+ data: {}
3767
+ };
3768
+ }
3769
+ return bridgeStart(identifierArg, agentName, flags);
3770
+ }
3771
+ case "stop": {
3772
+ if (!identifierArg) {
3773
+ return bridgeStopAll();
3774
+ }
3775
+ return bridgeStopOne(identifierArg);
3776
+ }
3777
+ case "status": {
3778
+ if (identifierArg) {
3779
+ return bridgeStatusOne(identifierArg);
3780
+ }
3781
+ return bridgeStatusAll();
3782
+ }
3783
+ default:
3784
+ return {
3785
+ ok: false,
3786
+ command: "bridge",
3787
+ code: "TAP_INVALID_ARGUMENT",
3788
+ message: `Unknown bridge subcommand: ${subcommand}. Use: start, stop, status`,
3789
+ warnings: [],
3790
+ data: {}
3791
+ };
3792
+ }
3793
+ }
3794
+ var BRIDGE_HELP;
3795
+ var init_bridge2 = __esm({
3796
+ "src/commands/bridge.ts"() {
3797
+ "use strict";
3798
+ init_state();
3799
+ init_bridge();
3800
+ init_config();
3801
+ init_adapters();
3802
+ init_utils();
3803
+ BRIDGE_HELP = `
3804
+ Usage:
3805
+ tap-comms bridge <subcommand> [instance] [options]
3806
+
3807
+ Subcommands:
3808
+ start <instance> Start bridge for an instance (e.g. codex, codex-reviewer)
3809
+ start --all Start all registered app-server instances
3810
+ stop <instance> Stop bridge for an instance
3811
+ stop Stop all running bridges
3812
+ status Show bridge status for all instances
3813
+ status <instance> Show bridge status for a specific instance
3814
+
3815
+ Options:
3816
+ --agent-name <name> Agent identity for bridge (or set TAP_AGENT_NAME env)
3817
+ Saved to state \u2014 only needed on first start
3818
+ --all Start all registered app-server instances
3819
+ --busy-mode <steer|wait> How to handle active turns (default: steer)
3820
+ --poll-seconds <n> Inbox poll interval (default: 5)
3821
+ --reconnect-seconds <n> Reconnect delay after disconnect (default: 5)
3822
+ --message-lookback-minutes <n> Process messages from last N minutes (default: 10)
3823
+ --thread-id <id> Resume specific thread
3824
+ --ephemeral Use ephemeral thread (no persistence)
3825
+ --process-existing-messages Process all existing inbox messages
3826
+ --no-server Skip app-server auto-start and connect only
3827
+ --no-auth Skip auth gateway (app-server listens directly, localhost only)
3828
+
3829
+ Port Assignment:
3830
+ Ports are auto-assigned from 4501 on first bridge start if not set via --port
3831
+ during 'tap add'. Auto-assigned ports are saved to state for future starts.
3832
+
3833
+ Examples:
3834
+ npx @hua-labs/tap bridge start codex --agent-name myAgent
3835
+ npx @hua-labs/tap bridge start --all
3836
+ npx @hua-labs/tap bridge start codex --agent-name myAgent --no-server
3837
+ npx @hua-labs/tap bridge start codex-reviewer --agent-name reviewer --busy-mode steer
3838
+ npx @hua-labs/tap bridge stop codex
3839
+ npx @hua-labs/tap bridge stop
3840
+ npx @hua-labs/tap bridge status
3841
+ `.trim();
3842
+ }
3843
+ });
3844
+
3845
+ // src/commands/up.ts
3846
+ var up_exports = {};
3847
+ __export(up_exports, {
3848
+ upCommand: () => upCommand
3849
+ });
3850
+ async function upCommand(args) {
3851
+ if (args.includes("--help") || args.includes("-h")) {
3852
+ log(UP_HELP);
3853
+ return {
3854
+ ok: true,
3855
+ command: "up",
3856
+ code: "TAP_NO_OP",
3857
+ message: UP_HELP,
3858
+ warnings: [],
3859
+ data: {}
3860
+ };
3861
+ }
3862
+ const repoRoot = findRepoRoot();
3863
+ const result = await bridgeCommand(["start", "--all", ...args]);
3864
+ const snapshot = collectDashboardSnapshot(repoRoot);
3865
+ const activeBridges = snapshot.bridges.filter(
3866
+ (bridge) => bridge.status === "running"
3867
+ ).length;
3868
+ if (!result.ok) {
3869
+ return {
3870
+ ...result,
3871
+ command: "up",
3872
+ data: {
3873
+ ...result.data,
3874
+ snapshot
3875
+ }
3876
+ };
3877
+ }
3878
+ return {
3879
+ ok: true,
3880
+ command: "up",
3881
+ code: "TAP_UP_OK",
3882
+ message: `tap up: ${activeBridges} bridge(s) running`,
3883
+ warnings: result.warnings,
3884
+ data: {
3885
+ ...result.data,
3886
+ snapshot
3887
+ }
3888
+ };
3889
+ }
3890
+ var UP_HELP;
3891
+ var init_up = __esm({
3892
+ "src/commands/up.ts"() {
3893
+ "use strict";
3894
+ init_bridge2();
3895
+ init_dashboard();
3896
+ init_utils();
3897
+ UP_HELP = `
3898
+ Usage:
3899
+ tap-comms up [bridge-start options]
3900
+
3901
+ Description:
3902
+ Start all registered app-server bridge daemons with one command.
3903
+ This is the orchestration entrypoint for headless/background TAP operation.
3904
+
3905
+ Examples:
3906
+ npx @hua-labs/tap up
3907
+ npx @hua-labs/tap up --no-auth
3908
+ npx @hua-labs/tap up --busy-mode wait
3909
+ `.trim();
3910
+ }
3911
+ });
3912
+
3913
+ // src/commands/down.ts
3914
+ var down_exports = {};
3915
+ __export(down_exports, {
3916
+ downCommand: () => downCommand
3917
+ });
3918
+ async function downCommand(args) {
3919
+ if (args.includes("--help") || args.includes("-h")) {
3920
+ log(DOWN_HELP);
3921
+ return {
3922
+ ok: true,
3923
+ command: "down",
3924
+ code: "TAP_NO_OP",
3925
+ message: DOWN_HELP,
3926
+ warnings: [],
3927
+ data: {}
3928
+ };
3929
+ }
3930
+ const repoRoot = findRepoRoot();
3931
+ const result = await bridgeCommand(["stop"]);
3932
+ const snapshot = collectDashboardSnapshot(repoRoot);
3933
+ if (!result.ok) {
3934
+ return {
3935
+ ...result,
3936
+ command: "down",
3937
+ data: {
3938
+ ...result.data,
3939
+ snapshot
3940
+ }
3941
+ };
3942
+ }
3943
+ return {
3944
+ ok: true,
3945
+ command: "down",
3946
+ code: "TAP_DOWN_OK",
3947
+ message: `tap down: ${snapshot.bridges.filter((bridge) => bridge.status === "running").length} bridge(s) still running`,
3948
+ warnings: result.warnings,
3949
+ data: {
3950
+ ...result.data,
3951
+ snapshot
3952
+ }
3953
+ };
3954
+ }
3955
+ var DOWN_HELP;
3956
+ var init_down = __esm({
3957
+ "src/commands/down.ts"() {
3958
+ "use strict";
3959
+ init_bridge2();
3960
+ init_dashboard();
3961
+ init_utils();
3962
+ DOWN_HELP = `
3963
+ Usage:
3964
+ tap-comms down
3965
+
3966
+ Description:
3967
+ Stop all running bridge daemons and managed app-servers.
3968
+
3969
+ Examples:
3970
+ npx @hua-labs/tap down
3971
+ `.trim();
3972
+ }
3973
+ });
3974
+
3975
+ // src/index.ts
3976
+ init_state();
3977
+
3978
+ // src/version.ts
3979
+ import * as fs4 from "fs";
3980
+ import * as path4 from "path";
3981
+ import { fileURLToPath } from "url";
3982
+ var FALLBACK_VERSION = "0.0.0";
3983
+ function resolvePackageVersion(metaUrl = import.meta.url) {
3984
+ const moduleDir = path4.dirname(fileURLToPath(metaUrl));
3985
+ const packageJsonPath = path4.join(moduleDir, "..", "package.json");
3986
+ try {
3987
+ const parsed = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
3988
+ if (typeof parsed.version === "string" && parsed.version.trim()) {
3989
+ return parsed.version;
3990
+ }
3991
+ } catch {
3992
+ }
3993
+ return FALLBACK_VERSION;
3994
+ }
3995
+ var version = resolvePackageVersion();
3996
+
3997
+ // src/index.ts
3998
+ init_config();
3999
+ init_bridge();
4000
+ init_dashboard();
4001
+
4002
+ // src/api/state.ts
4003
+ init_dashboard();
4004
+ init_utils();
4005
+ init_config();
4006
+ function getDashboardSnapshot(options) {
4007
+ const repoRoot = options?.repoRoot ?? findRepoRoot();
4008
+ return collectDashboardSnapshot(repoRoot, options?.commsDir);
4009
+ }
4010
+ async function* streamEvents(options) {
4011
+ const intervalMs = options?.intervalMs ?? 2e3;
4012
+ const repoRoot = options?.repoRoot ?? findRepoRoot();
4013
+ while (!options?.signal?.aborted) {
4014
+ yield collectDashboardSnapshot(repoRoot, options?.commsDir);
4015
+ await new Promise((resolve8) => {
4016
+ const onAbort = () => {
4017
+ clearTimeout(timer);
4018
+ resolve8();
4019
+ };
4020
+ const timer = setTimeout(() => {
4021
+ options?.signal?.removeEventListener("abort", onAbort);
4022
+ resolve8();
4023
+ }, intervalMs);
4024
+ options?.signal?.addEventListener("abort", onAbort, { once: true });
4025
+ });
4026
+ }
4027
+ }
4028
+ async function startAgents(options) {
4029
+ const { upCommand: upCommand2 } = await Promise.resolve().then(() => (init_up(), up_exports));
4030
+ const result = await upCommand2(options?.args ?? []);
4031
+ const repoRoot = findRepoRoot();
4032
+ const snapshot = collectDashboardSnapshot(repoRoot);
4033
+ return {
4034
+ ok: result.ok,
4035
+ message: result.message,
4036
+ snapshot,
4037
+ commandResult: result
4038
+ };
4039
+ }
4040
+ async function stopAgents() {
4041
+ const { downCommand: downCommand2 } = await Promise.resolve().then(() => (init_down(), down_exports));
4042
+ const result = await downCommand2([]);
4043
+ const repoRoot = findRepoRoot();
4044
+ const snapshot = collectDashboardSnapshot(repoRoot);
4045
+ return {
4046
+ ok: result.ok,
4047
+ message: result.message,
4048
+ snapshot,
4049
+ commandResult: result
4050
+ };
4051
+ }
4052
+ function getConfig(options) {
4053
+ const repoRoot = options?.repoRoot ?? findRepoRoot();
4054
+ const { config } = resolveConfig({}, repoRoot);
4055
+ return {
4056
+ repoRoot,
4057
+ commsDir: options?.commsDir ?? config.commsDir,
4058
+ stateDir: config.stateDir,
4059
+ appServerUrl: config.appServerUrl
4060
+ };
4061
+ }
4062
+
4063
+ // src/api/http.ts
4064
+ import {
4065
+ createServer as createServer2
4066
+ } from "http";
4067
+ var CORS_HEADERS = {
4068
+ "Access-Control-Allow-Origin": "http://localhost:3000",
4069
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
4070
+ "Access-Control-Allow-Headers": "Content-Type"
4071
+ };
4072
+ function jsonResponse(res, data, status = 200) {
4073
+ res.writeHead(status, {
4074
+ "Content-Type": "application/json",
4075
+ ...CORS_HEADERS
4076
+ });
4077
+ res.end(JSON.stringify(data));
4078
+ }
4079
+ function handleSnapshot(res, apiOptions) {
4080
+ const snapshot = getDashboardSnapshot(apiOptions);
4081
+ jsonResponse(res, snapshot);
4082
+ }
4083
+ function handleConfig(res, apiOptions) {
4084
+ const config = getConfig(apiOptions);
4085
+ jsonResponse(res, config);
4086
+ }
4087
+ async function handleEvents(req, res, apiOptions) {
4088
+ res.writeHead(200, {
4089
+ "Content-Type": "text/event-stream",
4090
+ "Cache-Control": "no-cache",
4091
+ Connection: "keep-alive",
4092
+ ...CORS_HEADERS
4093
+ });
4094
+ const controller = new AbortController();
4095
+ req.on("close", () => controller.abort());
4096
+ for await (const snapshot of streamEvents({
4097
+ ...apiOptions,
4098
+ signal: controller.signal
4099
+ })) {
4100
+ if (controller.signal.aborted) break;
4101
+ res.write(`data: ${JSON.stringify(snapshot)}
4102
+
4103
+ `);
4104
+ }
4105
+ res.end();
4106
+ }
4107
+ function handleHealth(res) {
4108
+ jsonResponse(res, { ok: true, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
4109
+ }
4110
+ async function startHttpServer(options) {
4111
+ const port = options?.port ?? 4580;
4112
+ const host = "127.0.0.1";
4113
+ const apiOptions = {
4114
+ repoRoot: options?.repoRoot,
4115
+ commsDir: options?.commsDir
4116
+ };
4117
+ const server = createServer2(
4118
+ async (req, res) => {
4119
+ const url = new URL(req.url ?? "/", `http://${host}:${port}`);
4120
+ const pathname = url.pathname;
4121
+ if (req.method === "OPTIONS") {
4122
+ res.writeHead(204, CORS_HEADERS);
4123
+ res.end();
4124
+ return;
4125
+ }
4126
+ try {
4127
+ if (req.method === "GET") {
4128
+ switch (pathname) {
4129
+ case "/api/snapshot":
4130
+ handleSnapshot(res, apiOptions);
4131
+ return;
4132
+ case "/api/events":
4133
+ await handleEvents(req, res, apiOptions);
4134
+ return;
4135
+ case "/api/config":
4136
+ handleConfig(res, apiOptions);
4137
+ return;
4138
+ case "/health":
4139
+ handleHealth(res);
4140
+ return;
4141
+ }
4142
+ }
4143
+ if (req.method === "POST") {
4144
+ const contentType = req.headers["content-type"] ?? "";
4145
+ if (!contentType.includes("application/json")) {
4146
+ jsonResponse(
4147
+ res,
4148
+ { error: "Content-Type must be application/json" },
4149
+ 415
4150
+ );
4151
+ return;
4152
+ }
4153
+ switch (pathname) {
4154
+ case "/api/start":
4155
+ jsonResponse(res, await startAgents());
4156
+ return;
4157
+ case "/api/stop":
4158
+ jsonResponse(res, await stopAgents());
4159
+ return;
4160
+ }
4161
+ }
4162
+ jsonResponse(res, { error: "Not found" }, 404);
4163
+ } catch (err) {
4164
+ const message = err instanceof Error ? err.message : String(err);
4165
+ jsonResponse(res, { error: message }, 500);
4166
+ }
4167
+ }
4168
+ );
4169
+ await new Promise((resolve8, reject) => {
4170
+ server.once("error", reject);
4171
+ server.listen(port, host, () => {
4172
+ server.removeListener("error", reject);
4173
+ resolve8();
4174
+ });
4175
+ });
4176
+ return {
4177
+ port,
4178
+ close: () => new Promise((resolve8, reject) => {
4179
+ server.close((err) => err ? reject(err) : resolve8());
4180
+ })
4181
+ };
4182
+ }
4183
+
4184
+ // src/index.ts
4185
+ init_runtime();
417
4186
  export {
418
4187
  LOCAL_CONFIG_FILE,
419
4188
  SHARED_CONFIG_FILE,
420
4189
  buildRuntimeEnv,
4190
+ collectDashboardSnapshot,
421
4191
  createInitialState,
4192
+ getConfig,
4193
+ getDashboardSnapshot,
422
4194
  getFnmBinDir,
423
4195
  getHeartbeatAge,
424
4196
  loadLocalConfig,
@@ -432,7 +4204,11 @@ export {
432
4204
  saveLocalConfig,
433
4205
  saveSharedConfig,
434
4206
  saveState,
4207
+ startAgents,
4208
+ startHttpServer,
435
4209
  stateExists,
4210
+ stopAgents,
4211
+ streamEvents,
436
4212
  updateBridgeHeartbeat,
437
4213
  version
438
4214
  };