@ait-co/devtools 0.1.88 → 0.1.90

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/mcp/cli.js CHANGED
@@ -84,6 +84,40 @@ function startParentWatcher(onOrphaned, opts) {
84
84
  clearInterval(handle);
85
85
  } };
86
86
  }
87
+ /**
88
+ * Starts a periodic watchdog that calls `onExpired` once after `maxAgeMs`
89
+ * milliseconds have elapsed since the watchdog was created.
90
+ *
91
+ * Motivation (issue #571): cloudflared quick-tunnel lifetimes are finite (a
92
+ * few hours). A daemon that has been running for days will have outlived its
93
+ * tunnel regardless of whether the tunnel process exited cleanly. This watchdog
94
+ * caps the daemon's maximum age and forces a fresh start so the tunnel is
95
+ * replaced before it silently expires.
96
+ *
97
+ * @param onExpired - Called once when the maximum age is reached. The caller
98
+ * should call `shutdown()` then `process.exit(0)`.
99
+ * @param opts.maxAgeMs - Maximum daemon lifetime in ms. Default 6 h.
100
+ * @param opts.intervalMs - Check interval in ms. Default 60 000 (1 min).
101
+ * @param opts.now - Time source (injectable for tests). Default `Date.now`.
102
+ *
103
+ * @returns `stop` — call during shutdown to clear the interval.
104
+ */
105
+ function startMaxAgeWatchdog(onExpired, opts = {}) {
106
+ const { maxAgeMs = 360 * 60 * 1e3, intervalMs = 6e4, now = () => Date.now() } = opts;
107
+ const startedAt = now();
108
+ let fired = false;
109
+ const handle = setInterval(() => {
110
+ if (fired) return;
111
+ if (now() - startedAt >= maxAgeMs) {
112
+ fired = true;
113
+ clearInterval(handle);
114
+ onExpired();
115
+ }
116
+ }, intervalMs);
117
+ return { stop() {
118
+ clearInterval(handle);
119
+ } };
120
+ }
87
121
  //#endregion
88
122
  //#region src/mcp/ait-chii-source.ts
89
123
  function isObject$4(value) {
@@ -369,7 +403,12 @@ var ChiiCdpConnection = class {
369
403
  }
370
404
  /** Refresh the attached-target list from the relay's `GET /targets`. */
371
405
  async refreshTargets() {
372
- const res = await fetch(`${this.relayBaseUrl}/targets`);
406
+ let targetsUrl = `${this.relayBaseUrl}/targets`;
407
+ if (this.totpSecret) {
408
+ const code = generateTotp(this.totpSecret);
409
+ targetsUrl += `?at=${encodeURIComponent(code)}`;
410
+ }
411
+ const res = await fetch(targetsUrl);
373
412
  if (!res.ok) throw new Error(`Chii relay /targets returned HTTP ${res.status} ${res.statusText}`);
374
413
  const body = await res.json();
375
414
  const list = isObject$3(body) && Array.isArray(body.targets) ? body.targets : [];
@@ -934,14 +973,28 @@ async function startChiiRelay(options = {}) {
934
973
  };
935
974
  if (verifyAuth) httpServer.on("request", (req, res) => {
936
975
  const rewritten = rewriteAtPathPrefix(req.url ?? "");
937
- if (rewritten === null) return;
938
- req.url = rewritten;
939
- if (!verifyAuth(req)) {
940
- res.statusCode = 401;
941
- res.setHeader("Access-Control-Allow-Origin", "*");
942
- res.setHeader("Content-Type", "application/json");
943
- res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
944
- notifyAuthReject("http-request");
976
+ if (rewritten !== null) {
977
+ req.url = rewritten;
978
+ if (!verifyAuth(req)) {
979
+ res.statusCode = 401;
980
+ res.setHeader("Access-Control-Allow-Origin", "*");
981
+ res.setHeader("Content-Type", "application/json");
982
+ res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
983
+ notifyAuthReject("http-request");
984
+ }
985
+ return;
986
+ }
987
+ const pathname = (req.url ?? "").split("?")[0];
988
+ if (pathname === "/targets" || pathname === "/targets/") {
989
+ if (!verifyAuth(req)) {
990
+ res.statusCode = 401;
991
+ res.setHeader("Access-Control-Allow-Origin", "*");
992
+ res.setHeader("Content-Type", "application/json");
993
+ res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
994
+ notifyAuthReject("http-request");
995
+ return;
996
+ }
997
+ return;
945
998
  }
946
999
  });
947
1000
  const chiiWssClass = keepaliveIntervalMs > 0 ? tryLoadChiiWssClass() : null;
@@ -3472,10 +3525,12 @@ function readLock(lockPath) {
3472
3525
  const parsed = JSON.parse(raw);
3473
3526
  if (typeof parsed === "object" && parsed !== null && "pid" in parsed && typeof parsed.pid === "number" && "startedAt" in parsed && typeof parsed.startedAt === "string") {
3474
3527
  const p = parsed;
3528
+ const tunnelChildPid = typeof p.tunnelChildPid === "number" ? p.tunnelChildPid : null;
3475
3529
  return {
3476
3530
  pid: p.pid,
3477
3531
  wssUrl: typeof p.wssUrl === "string" ? p.wssUrl : null,
3478
- startedAt: p.startedAt
3532
+ startedAt: p.startedAt,
3533
+ tunnelChildPid
3479
3534
  };
3480
3535
  }
3481
3536
  return null;
@@ -3541,16 +3596,19 @@ function acquireLock(options = {}) {
3541
3596
  const { force = false } = options;
3542
3597
  const lockPath = lockFilePath();
3543
3598
  const existing = readLock(lockPath);
3544
- if (existing !== null) if (isPidAlive(existing.pid)) if (force) {
3545
- process.stderr.write(`[ait-debug] --force: terminating existing session PID=${existing.pid} …\n`);
3546
- killAndWait(existing.pid);
3547
- process.stderr.write(`[ait-debug] --force: PID=${existing.pid} stopped, taking over.\n`);
3548
- } else {
3549
- const urlPart = existing.wssUrl != null ? `wssUrl=${existing.wssUrl}` : "wssUrl=(tunnel starting)";
3550
- process.stderr.write(`[ait-debug] 기존 debug-mode 세션이 이미 실행 중 — PID=${existing.pid}, started ${existing.startedAt}, ${urlPart}\n[ait-debug] 회복: \`kill ${existing.pid}\` 또는 \`npx @ait-co/devtools devtools-mcp --force\`\n`);
3551
- throw new ServerLockConflictError(existing.pid, existing.wssUrl, existing.startedAt);
3552
- }
3553
- else process.stderr.write(`[ait-debug] stale lock from PID ${existing.pid} recovered starting fresh.\n`);
3599
+ if (existing !== null) if (isPidAlive(existing.pid)) {
3600
+ const tunnelChildPid = existing.tunnelChildPid;
3601
+ if (typeof tunnelChildPid === "number" && !isPidAlive(tunnelChildPid)) process.stderr.write(`[ait-debug] stale lock: holder PID=${existing.pid} alive but tunnel child PID=${tunnelChildPid} is dead — reclaiming lock.\n`);
3602
+ else if (force) {
3603
+ process.stderr.write(`[ait-debug] --force: terminating existing session PID=${existing.pid} …\n`);
3604
+ killAndWait(existing.pid);
3605
+ process.stderr.write(`[ait-debug] --force: PID=${existing.pid} stopped, taking over.\n`);
3606
+ } else {
3607
+ const urlPart = existing.wssUrl != null ? `wssUrl=${existing.wssUrl}` : "wssUrl=(tunnel starting)";
3608
+ process.stderr.write(`[ait-debug] 기존 debug-mode 세션이 이미 실행 중 — PID=${existing.pid}, started ${existing.startedAt}, ${urlPart}\n[ait-debug] 회복: \`kill ${existing.pid}\` 또는 \`npx @ait-co/devtools devtools-mcp --force\`\n`);
3609
+ throw new ServerLockConflictError(existing.pid, existing.wssUrl, existing.startedAt);
3610
+ }
3611
+ } else process.stderr.write(`[ait-debug] stale lock from PID ${existing.pid} recovered — starting fresh.\n`);
3554
3612
  const data = {
3555
3613
  pid: process.pid,
3556
3614
  wssUrl: null,
@@ -3564,6 +3622,11 @@ function acquireLock(options = {}) {
3564
3622
  data.wssUrl = wssUrl;
3565
3623
  writeLock(lockPath, data);
3566
3624
  },
3625
+ updateTunnelChildPid(pid) {
3626
+ if (released) return;
3627
+ data.tunnelChildPid = pid;
3628
+ writeLock(lockPath, data);
3629
+ },
3567
3630
  release() {
3568
3631
  if (released) return;
3569
3632
  released = true;
@@ -4781,7 +4844,7 @@ async function readMcpSdkVersion() {
4781
4844
  * some test environments that skip the build step).
4782
4845
  */
4783
4846
  function readDevtoolsVersion() {
4784
- return "0.1.88";
4847
+ return "0.1.90";
4785
4848
  }
4786
4849
  /**
4787
4850
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -4840,7 +4903,7 @@ function computeNextRecommendedAction(tunnel, pages, env, authRejects = null) {
4840
4903
  * - Lock file data contains only pid + startedAt + wssUrl — no secrets.
4841
4904
  */
4842
4905
  async function getDiagnostics(input) {
4843
- const { tunnel, connection, env, envReason, collector, readLock: readLockFn, recentErrorsLimit = 10, getMcpVersion = readMcpSdkVersion, checkParentAlive = () => isPidAlive(process.ppid) } = input;
4906
+ const { tunnel, connection, env, envReason, collector, readLock: readLockFn, recentErrorsLimit = 10, getMcpVersion = readMcpSdkVersion, checkParentAlive = () => isPidAlive(process.ppid), tunnelChildPid } = input;
4844
4907
  const [mcpVersion, devtoolsVersion] = await Promise.all([getMcpVersion(), Promise.resolve(readDevtoolsVersion())]);
4845
4908
  const lockData = readLockFn();
4846
4909
  const serverLockHolder = lockData ? {
@@ -4848,8 +4911,11 @@ async function getDiagnostics(input) {
4848
4911
  startedAt: lockData.startedAt,
4849
4912
  wssUrl: lockData.wssUrl
4850
4913
  } : null;
4914
+ const effectiveTunnelChildPid = tunnelChildPid ?? lockData?.tunnelChildPid ?? null;
4915
+ let effectiveUp = tunnel.up;
4916
+ if (tunnel.up && typeof effectiveTunnelChildPid === "number" && effectiveTunnelChildPid !== null && !isPidAlive(effectiveTunnelChildPid)) effectiveUp = false;
4851
4917
  const tunnelInfo = {
4852
- up: tunnel.up,
4918
+ up: effectiveUp,
4853
4919
  wssUrl: tunnel.wssUrl,
4854
4920
  pid: lockData?.pid ?? null,
4855
4921
  startedAt: lockData?.startedAt ?? null,
@@ -4937,6 +5003,11 @@ async function ensureCloudflaredBin() {
4937
5003
  /**
4938
5004
  * Opens a cloudflared quick tunnel to the local relay port and resolves once
4939
5005
  * the public URL is assigned.
5006
+ *
5007
+ * FIX 1 (issue #571): after URL resolution the returned `QuickTunnel` object
5008
+ * watches the cloudflared child process for unexpected exits and calls any
5009
+ * registered `onUnexpectedExit` callback so the health probe can immediately
5010
+ * trigger reissue instead of waiting for the next poll interval.
4940
5011
  */
4941
5012
  async function startQuickTunnel(localPort) {
4942
5013
  await ensureCloudflaredBin();
@@ -4963,10 +5034,20 @@ async function startQuickTunnel(localPort) {
4963
5034
  tunnel.once("error", onError);
4964
5035
  tunnel.once("exit", onExit);
4965
5036
  });
5037
+ let intentionalStop = false;
5038
+ let unexpectedExitCb = null;
5039
+ tunnel.once("exit", (code) => {
5040
+ if (!intentionalStop && unexpectedExitCb !== null) unexpectedExitCb(code);
5041
+ });
4966
5042
  return {
4967
5043
  url,
4968
5044
  wssUrl: url.replace(/^https/, "wss"),
4969
- stop: () => {
5045
+ childPid: tunnel.process?.pid,
5046
+ onUnexpectedExit(cb) {
5047
+ unexpectedExitCb = cb;
5048
+ },
5049
+ stop() {
5050
+ intentionalStop = true;
4970
5051
  tunnel.stop();
4971
5052
  }
4972
5053
  };
@@ -5088,6 +5169,10 @@ async function probeTunnel(httpsUrl, timeoutMs = 1e4) {
5088
5169
  * times). On success the caller is notified via `onReissue`; on permanent
5089
5170
  * failure via `onPermanentDrop`.
5090
5171
  *
5172
+ * FIX 1 (issue #571): the probe also subscribes to each tunnel's
5173
+ * `onUnexpectedExit` callback to detect child death *immediately* instead of
5174
+ * waiting for the next probe interval (which could be 60 s away).
5175
+ *
5091
5176
  * @returns `stop` — call during server shutdown to clear the probe interval.
5092
5177
  */
5093
5178
  function startTunnelHealthProbe(initialTunnel, localPort, options) {
@@ -5096,6 +5181,43 @@ function startTunnelHealthProbe(initialTunnel, localPort, options) {
5096
5181
  let consecutiveFailures = 0;
5097
5182
  let reissueAttempts = 0;
5098
5183
  let stopped = false;
5184
+ const doReissueOrDrop = async () => {
5185
+ if (stopped) return;
5186
+ reissueAttempts += 1;
5187
+ if (reissueAttempts > 3) return;
5188
+ log(`[ait-debug] tunnel drop detected — reissuing (attempt ${reissueAttempts}/3)\n`);
5189
+ try {
5190
+ const newTunnel = await spawnTunnel(localPort);
5191
+ try {
5192
+ currentTunnel.stop();
5193
+ } catch {}
5194
+ currentTunnel = newTunnel;
5195
+ consecutiveFailures = 0;
5196
+ armChildExitWatch(newTunnel);
5197
+ log(`[ait-debug] tunnel reissued — new relay: ${newTunnel.wssUrl}\n`);
5198
+ onReissue(newTunnel);
5199
+ } catch (err) {
5200
+ const message = err instanceof Error ? err.message : String(err);
5201
+ log(`[ait-debug] tunnel reissue attempt ${reissueAttempts} failed: ${message}\n`);
5202
+ if (reissueAttempts >= 3) {
5203
+ clearInterval(handle);
5204
+ stopped = true;
5205
+ const droppedAt = (/* @__PURE__ */ new Date()).toISOString();
5206
+ log(`[ait-debug] tunnel permanently dropped after 3 reissue attempts — restart the debug server to continue (npx @ait-co/devtools devtools-mcp).
5207
+ `);
5208
+ onPermanentDrop(droppedAt);
5209
+ }
5210
+ }
5211
+ };
5212
+ const armChildExitWatch = (t) => {
5213
+ t.onUnexpectedExit((code) => {
5214
+ if (stopped) return;
5215
+ log(`[ait-debug] cloudflared child exited unexpectedly (code=${code}) — triggering immediate reissue\n`);
5216
+ consecutiveFailures = failuresBeforeReissue;
5217
+ doReissueOrDrop();
5218
+ });
5219
+ };
5220
+ armChildExitWatch(initialTunnel);
5099
5221
  const handle = setInterval(() => {
5100
5222
  (async () => {
5101
5223
  if (stopped) return;
@@ -5109,30 +5231,7 @@ function startTunnelHealthProbe(initialTunnel, localPort, options) {
5109
5231
  consecutiveFailures += 1;
5110
5232
  log(`[ait-debug] tunnel health probe: failure ${consecutiveFailures}/${failuresBeforeReissue} (url=${httpsUrl})\n`);
5111
5233
  if (consecutiveFailures < failuresBeforeReissue) return;
5112
- reissueAttempts += 1;
5113
- if (reissueAttempts > 3) return;
5114
- log(`[ait-debug] tunnel drop detected — reissuing (attempt ${reissueAttempts}/3)\n`);
5115
- try {
5116
- const newTunnel = await spawnTunnel(localPort);
5117
- try {
5118
- currentTunnel.stop();
5119
- } catch {}
5120
- currentTunnel = newTunnel;
5121
- consecutiveFailures = 0;
5122
- log(`[ait-debug] tunnel reissued — new relay: ${newTunnel.wssUrl}\n`);
5123
- onReissue(newTunnel);
5124
- } catch (err) {
5125
- const message = err instanceof Error ? err.message : String(err);
5126
- log(`[ait-debug] tunnel reissue attempt ${reissueAttempts} failed: ${message}\n`);
5127
- if (reissueAttempts >= 3) {
5128
- clearInterval(handle);
5129
- stopped = true;
5130
- const droppedAt = (/* @__PURE__ */ new Date()).toISOString();
5131
- log(`[ait-debug] tunnel permanently dropped after 3 reissue attempts — restart the debug server to continue (npx @ait-co/devtools devtools-mcp).
5132
- `);
5133
- onPermanentDrop(droppedAt);
5134
- }
5135
- }
5234
+ await doReissueOrDrop();
5136
5235
  })();
5137
5236
  }, probeIntervalMs);
5138
5237
  return { stop() {
@@ -5282,15 +5381,16 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
5282
5381
  * naturally via `enableDomains`). The tier only controls visibility.
5283
5382
  */
5284
5383
  function createDebugServer(deps) {
5285
- const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 6e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret, onAttachUrlBuilt } = deps;
5384
+ const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 6e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret, onAttachUrlBuilt, getTunnelChildPid, readLock: readLockDep } = deps;
5286
5385
  const getTotpSecret = deps.getTotpSecret ?? (() => totpSecret);
5386
+ const readLockFn = readLockDep ?? readServerLock;
5287
5387
  const router = routerDep ?? makeSingleConnectionRouter(connection);
5288
5388
  const resolveEnvironment = getEnvDep ?? (() => deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin));
5289
5389
  const resolveEnvironmentReason = getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},liveIntent=${getLiveIntent()}`);
5290
5390
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
5291
5391
  const server = new Server({
5292
5392
  name: "ait-debug",
5293
- version: "0.1.88"
5393
+ version: "0.1.90"
5294
5394
  }, { capabilities: { tools: { listChanged: true } } });
5295
5395
  server.setRequestHandler(ListToolsRequestSchema, () => {
5296
5396
  const conn = router.active;
@@ -5355,8 +5455,9 @@ function createDebugServer(deps) {
5355
5455
  env,
5356
5456
  envReason,
5357
5457
  collector,
5358
- readLock: readServerLock,
5359
- recentErrorsLimit
5458
+ readLock: readLockFn,
5459
+ recentErrorsLimit,
5460
+ tunnelChildPid: getTunnelChildPid?.() ?? void 0
5360
5461
  }), name, env, conn.listTargets().length > 0);
5361
5462
  } catch (err) {
5362
5463
  return errorResult(err, name);
@@ -6049,12 +6150,14 @@ async function bootRelayFamily(options = {}) {
6049
6150
  tunnel = t;
6050
6151
  tunnelStatus = makeTunnelStatus(true, t.wssUrl);
6051
6152
  options.onWssUrl?.(t.wssUrl);
6153
+ if (t.childPid !== void 0) options.onTunnelChildPid?.(t.childPid);
6052
6154
  logInfo("tunnel.up", { totpEnabled });
6053
6155
  tunnelProbe = startTunnelHealthProbe(t, relay.port, {
6054
6156
  onReissue: (newTunnel) => {
6055
6157
  tunnel = newTunnel;
6056
6158
  tunnelStatus = makeTunnelStatus(true, newTunnel.wssUrl, null, 0);
6057
6159
  options.onWssUrl?.(newTunnel.wssUrl);
6160
+ if (newTunnel.childPid !== void 0) options.onTunnelChildPid?.(newTunnel.childPid);
6058
6161
  printAttachBanner({
6059
6162
  wssUrl: newTunnel.wssUrl,
6060
6163
  totpEnabled
@@ -6387,6 +6490,7 @@ async function runDebugServer(options = {}) {
6387
6490
  const lockHandle = acquireLock({ force: options.force ?? false });
6388
6491
  const devtoolsOpener = new AutoDevtoolsOpener();
6389
6492
  const diagnosticsCollector = new InMemoryDiagnosticsCollector();
6493
+ let activeTunnelChildPid = null;
6390
6494
  const router = new DualConnectionRouter({
6391
6495
  bootLazyFor: async (key, projectRoot) => key === "relay-sandbox" ? bootExternalRelayFamily(await readMobileRelayBaseUrl(process.env, projectRoot), await readRelayLocalUrl(process.env, projectRoot)) : key === "local-browser" ? bootLocalFamily() : bootRelayFamily({
6392
6496
  relayPort: options.relayPort,
@@ -6395,6 +6499,10 @@ async function runDebugServer(options = {}) {
6395
6499
  lockHandle.updateWssUrl(wssUrl);
6396
6500
  qrServer?.notifyStateChange();
6397
6501
  },
6502
+ onTunnelChildPid: (pid) => {
6503
+ activeTunnelChildPid = pid;
6504
+ lockHandle.updateTunnelChildPid(pid);
6505
+ },
6398
6506
  onAuthReject: () => diagnosticsCollector.recordAuthReject()
6399
6507
  }),
6400
6508
  diagnosticsCollector,
@@ -6466,6 +6574,7 @@ async function runDebugServer(options = {}) {
6466
6574
  router,
6467
6575
  aitSource,
6468
6576
  getTunnelStatus: () => router.relayTunnelStatus(),
6577
+ getTunnelChildPid: () => activeTunnelChildPid,
6469
6578
  get qrHttpServer() {
6470
6579
  return qrServer;
6471
6580
  },
@@ -6479,10 +6588,12 @@ async function runDebugServer(options = {}) {
6479
6588
  const transport = new StdioServerTransport();
6480
6589
  let closed = false;
6481
6590
  let parentWatcher = null;
6591
+ let maxAgeWatchdog = null;
6482
6592
  const shutdown = () => {
6483
6593
  if (closed) return;
6484
6594
  closed = true;
6485
6595
  parentWatcher?.stop();
6596
+ maxAgeWatchdog?.stop();
6486
6597
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
6487
6598
  router.stopWatcher();
6488
6599
  for (const family of router.bootedFamilies()) family.stop();
@@ -6497,6 +6608,7 @@ async function runDebugServer(options = {}) {
6497
6608
  if (!closed) {
6498
6609
  closed = true;
6499
6610
  parentWatcher?.stop();
6611
+ maxAgeWatchdog?.stop();
6500
6612
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
6501
6613
  router.stopWatcher();
6502
6614
  for (const family of router.bootedFamilies()) family.stop();
@@ -6535,6 +6647,11 @@ async function runDebugServer(options = {}) {
6535
6647
  process.exit(0);
6536
6648
  });
6537
6649
  }
6650
+ if (process.env.AIT_DEBUG_NO_MAX_AGE !== "1") maxAgeWatchdog = startMaxAgeWatchdog(() => {
6651
+ process.stderr.write("[ait-debug] max-age watchdog: daemon lifetime exceeded — shutting down for a fresh start.\n");
6652
+ shutdown();
6653
+ process.exit(0);
6654
+ }, { maxAgeMs: process.env.AIT_DEBUG_MAX_AGE_MS ? Number.parseInt(process.env.AIT_DEBUG_MAX_AGE_MS, 10) || void 0 : void 0 });
6538
6655
  }
6539
6656
  /**
6540
6657
  * Serves the debug stack over stdio with the local browser as the default
@@ -6586,6 +6703,7 @@ async function runLocalDebugServer(options = {}) {
6586
6703
  };
6587
6704
  const devtoolsOpener = new AutoDevtoolsOpener();
6588
6705
  const diagnosticsCollector = new InMemoryDiagnosticsCollector();
6706
+ let activeTunnelChildPid = null;
6589
6707
  const router = new DualConnectionRouter({
6590
6708
  bootLazyFor: async (key, projectRoot) => key === "relay-sandbox" ? bootExternalRelayFamily(await readMobileRelayBaseUrl(process.env, projectRoot), await readRelayLocalUrl(process.env, projectRoot)) : key === "local-browser" ? bootLocalFamilyForEntry() : bootRelayFamily({
6591
6709
  verifyAuth: buildRelayVerifyAuth(),
@@ -6593,6 +6711,10 @@ async function runLocalDebugServer(options = {}) {
6593
6711
  lockHandle.updateWssUrl(wssUrl);
6594
6712
  qrServer?.notifyStateChange();
6595
6713
  },
6714
+ onTunnelChildPid: (pid) => {
6715
+ activeTunnelChildPid = pid;
6716
+ lockHandle.updateTunnelChildPid(pid);
6717
+ },
6596
6718
  onAuthReject: () => diagnosticsCollector.recordAuthReject()
6597
6719
  }),
6598
6720
  diagnosticsCollector,
@@ -6663,6 +6785,7 @@ async function runLocalDebugServer(options = {}) {
6663
6785
  router,
6664
6786
  aitSource,
6665
6787
  getTunnelStatus: () => router.relayTunnelStatus(),
6788
+ getTunnelChildPid: () => activeTunnelChildPid,
6666
6789
  get qrHttpServer() {
6667
6790
  return qrServer;
6668
6791
  },
@@ -6676,10 +6799,12 @@ async function runLocalDebugServer(options = {}) {
6676
6799
  const transport = new StdioServerTransport();
6677
6800
  let closed = false;
6678
6801
  let parentWatcher = null;
6802
+ let maxAgeWatchdog = null;
6679
6803
  const shutdown = () => {
6680
6804
  if (closed) return;
6681
6805
  closed = true;
6682
6806
  parentWatcher?.stop();
6807
+ maxAgeWatchdog?.stop();
6683
6808
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
6684
6809
  router.stopWatcher();
6685
6810
  for (const family of router.bootedFamilies()) family.stop();
@@ -6694,6 +6819,7 @@ async function runLocalDebugServer(options = {}) {
6694
6819
  if (!closed) {
6695
6820
  closed = true;
6696
6821
  parentWatcher?.stop();
6822
+ maxAgeWatchdog?.stop();
6697
6823
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
6698
6824
  router.stopWatcher();
6699
6825
  for (const family of router.bootedFamilies()) family.stop();
@@ -6734,6 +6860,11 @@ async function runLocalDebugServer(options = {}) {
6734
6860
  process.exit(0);
6735
6861
  });
6736
6862
  }
6863
+ if (process.env.AIT_DEBUG_NO_MAX_AGE !== "1") maxAgeWatchdog = startMaxAgeWatchdog(() => {
6864
+ process.stderr.write("[ait-debug] max-age watchdog: daemon lifetime exceeded — shutting down for a fresh start.\n");
6865
+ shutdown();
6866
+ process.exit(0);
6867
+ }, { maxAgeMs: process.env.AIT_DEBUG_MAX_AGE_MS ? Number.parseInt(process.env.AIT_DEBUG_MAX_AGE_MS, 10) || void 0 : void 0 });
6737
6868
  }
6738
6869
  /**
6739
6870
  * Serves the env-2 (real-device PWA) debug stack over stdio with the external
@@ -6767,6 +6898,7 @@ async function runMobileDebugServer(options = {}) {
6767
6898
  const lockHandle = acquireLock({ force: options.force ?? false });
6768
6899
  const devtoolsOpener = new AutoDevtoolsOpener();
6769
6900
  const diagnosticsCollector = new InMemoryDiagnosticsCollector();
6901
+ let activeTunnelChildPid = null;
6770
6902
  const router = new DualConnectionRouter({
6771
6903
  bootLazyFor: async (key) => key === "relay-sandbox" ? bootExternalRelayFamily(relayBaseUrl, await readRelayLocalUrl(process.env, options.projectRoot ?? process.cwd())) : key === "local-browser" ? bootLocalFamily() : bootRelayFamily({
6772
6904
  verifyAuth: buildRelayVerifyAuth(),
@@ -6774,6 +6906,10 @@ async function runMobileDebugServer(options = {}) {
6774
6906
  lockHandle.updateWssUrl(wssUrl);
6775
6907
  qrServer?.notifyStateChange();
6776
6908
  },
6909
+ onTunnelChildPid: (pid) => {
6910
+ activeTunnelChildPid = pid;
6911
+ lockHandle.updateTunnelChildPid(pid);
6912
+ },
6777
6913
  onAuthReject: () => diagnosticsCollector.recordAuthReject()
6778
6914
  }),
6779
6915
  diagnosticsCollector,
@@ -6844,6 +6980,7 @@ async function runMobileDebugServer(options = {}) {
6844
6980
  router,
6845
6981
  aitSource,
6846
6982
  getTunnelStatus: () => router.relayTunnelStatus(),
6983
+ getTunnelChildPid: () => activeTunnelChildPid,
6847
6984
  get qrHttpServer() {
6848
6985
  return qrServer;
6849
6986
  },
@@ -6857,10 +6994,12 @@ async function runMobileDebugServer(options = {}) {
6857
6994
  const transport = new StdioServerTransport();
6858
6995
  let closed = false;
6859
6996
  let parentWatcher = null;
6997
+ let maxAgeWatchdog = null;
6860
6998
  const shutdown = () => {
6861
6999
  if (closed) return;
6862
7000
  closed = true;
6863
7001
  parentWatcher?.stop();
7002
+ maxAgeWatchdog?.stop();
6864
7003
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
6865
7004
  router.stopWatcher();
6866
7005
  for (const family of router.bootedFamilies()) family.stop();
@@ -6875,6 +7014,7 @@ async function runMobileDebugServer(options = {}) {
6875
7014
  if (!closed) {
6876
7015
  closed = true;
6877
7016
  parentWatcher?.stop();
7017
+ maxAgeWatchdog?.stop();
6878
7018
  if (totpRefreshHandle) clearInterval(totpRefreshHandle);
6879
7019
  router.stopWatcher();
6880
7020
  for (const family of router.bootedFamilies()) family.stop();
@@ -6915,6 +7055,11 @@ async function runMobileDebugServer(options = {}) {
6915
7055
  process.exit(0);
6916
7056
  });
6917
7057
  }
7058
+ if (process.env.AIT_DEBUG_NO_MAX_AGE !== "1") maxAgeWatchdog = startMaxAgeWatchdog(() => {
7059
+ process.stderr.write("[ait-debug] max-age watchdog: daemon lifetime exceeded — shutting down for a fresh start.\n");
7060
+ shutdown();
7061
+ process.exit(0);
7062
+ }, { maxAgeMs: process.env.AIT_DEBUG_MAX_AGE_MS ? Number.parseInt(process.env.AIT_DEBUG_MAX_AGE_MS, 10) || void 0 : void 0 });
6918
7063
  }
6919
7064
  //#endregion
6920
7065
  //#region src/mcp/ait-http-source.ts
@@ -7344,7 +7489,7 @@ function createDevServer(deps = {}) {
7344
7489
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
7345
7490
  const server = new Server({
7346
7491
  name: "ait-devtools",
7347
- version: "0.1.88"
7492
+ version: "0.1.90"
7348
7493
  }, { capabilities: { tools: {} } });
7349
7494
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
7350
7495
  server.setRequestHandler(CallToolRequestSchema, async (request) => {