@ait-co/devtools 0.1.100 → 0.1.102

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.
Files changed (36) hide show
  1. package/dist/mcp/cli.js +107 -19
  2. package/dist/mcp/cli.js.map +1 -1
  3. package/dist/mcp/server.js +1 -1
  4. package/dist/panel/index.js +2 -2
  5. package/dist/{qr-http-server-Clvk1weS.cjs → qr-http-server-B7DsRdN1.cjs} +13 -6
  6. package/dist/{qr-http-server-Clvk1weS.cjs.map → qr-http-server-B7DsRdN1.cjs.map} +1 -1
  7. package/dist/{qr-http-server-B1fmICC4.js → qr-http-server-CK-ZT_pC.js} +13 -6
  8. package/dist/{qr-http-server-B1fmICC4.js.map → qr-http-server-CK-ZT_pC.js.map} +1 -1
  9. package/dist/{qr-http-server-ofopTUL-.js → qr-http-server-DI3A6f5L.js} +13 -6
  10. package/dist/{qr-http-server-ofopTUL-.js.map → qr-http-server-DI3A6f5L.js.map} +1 -1
  11. package/dist/{qr-http-server-C9NUBysQ.cjs → qr-http-server-Dqb3GQju.cjs} +13 -6
  12. package/dist/{qr-http-server-C9NUBysQ.cjs.map → qr-http-server-Dqb3GQju.cjs.map} +1 -1
  13. package/dist/{relay-secret-store-J0SUUXjH.js → relay-secret-store-B0DH-8Qb.js} +46 -3
  14. package/dist/relay-secret-store-B0DH-8Qb.js.map +1 -0
  15. package/dist/{relay-secret-store-B5WAozDv.cjs → relay-secret-store-CqDaaFW1.cjs} +43 -2
  16. package/dist/relay-secret-store-CqDaaFW1.cjs.map +1 -0
  17. package/dist/{relay-secret-store-BvNWdSjV.js → relay-secret-store-DKuoAJmA.js} +43 -2
  18. package/dist/relay-secret-store-DKuoAJmA.js.map +1 -0
  19. package/dist/{relay-url-store-RKcao_yG.js → relay-url-store-BPeUZsiY.js} +2 -2
  20. package/dist/{relay-url-store-RKcao_yG.js.map → relay-url-store-BPeUZsiY.js.map} +1 -1
  21. package/dist/{relay-url-store-D2lX9POP.cjs → relay-url-store-CIZlFBkR.cjs} +2 -2
  22. package/dist/{relay-url-store-D2lX9POP.cjs.map → relay-url-store-CIZlFBkR.cjs.map} +1 -1
  23. package/dist/{relay-url-store-1CXVqNDL.js → relay-url-store-DASEZiT9.js} +2 -2
  24. package/dist/{relay-url-store-1CXVqNDL.js.map → relay-url-store-DASEZiT9.js.map} +1 -1
  25. package/dist/{tunnel-C_qpse3-.js → tunnel-CepDBgEc.js} +2 -2
  26. package/dist/{tunnel-C_qpse3-.js.map → tunnel-CepDBgEc.js.map} +1 -1
  27. package/dist/{tunnel-BmDfjkQI.cjs → tunnel-D0QnxKsF.cjs} +2 -2
  28. package/dist/{tunnel-BmDfjkQI.cjs.map → tunnel-D0QnxKsF.cjs.map} +1 -1
  29. package/dist/unplugin/index.cjs +3 -3
  30. package/dist/unplugin/index.js +3 -3
  31. package/dist/unplugin/tunnel.cjs +1 -1
  32. package/dist/unplugin/tunnel.js +1 -1
  33. package/package.json +1 -1
  34. package/dist/relay-secret-store-B5WAozDv.cjs.map +0 -1
  35. package/dist/relay-secret-store-BvNWdSjV.js.map +0 -1
  36. package/dist/relay-secret-store-J0SUUXjH.js.map +0 -1
package/dist/mcp/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-Xq3ACwkm.js";
3
- import { t as loadRelaySecretReadOnly } from "../relay-secret-store-J0SUUXjH.js";
3
+ import { t as loadRelaySecretReadOnly } from "../relay-secret-store-B0DH-8Qb.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { argv } from "node:process";
@@ -1933,7 +1933,7 @@ function findFreePort() {
1933
1933
  else resolve(port);
1934
1934
  });
1935
1935
  });
1936
- server.once("error", reject);
1936
+ server.on("error", reject);
1937
1937
  });
1938
1938
  }
1939
1939
  /**
@@ -3003,7 +3003,7 @@ function buildLangSwitcher(path, existingParams, locale, s) {
3003
3003
  * - tunnel wssUrl은 "터널 연결됨" 상태 표시에서 UP/DOWN만 노출.
3004
3004
  * wssUrl 값 자체는 dashboard HTML에 넣지 않는다.
3005
3005
  */
3006
- function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new URLSearchParams()) {
3006
+ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new URLSearchParams(), devtoolsEntryUrl = null) {
3007
3007
  const s = resolveLocaleStrings(locale);
3008
3008
  const now = (/* @__PURE__ */ new Date()).toISOString();
3009
3009
  const tunnelStatus = state.tunnel.up ? s("dashboard.tunnel.up") : s("dashboard.tunnel.down");
@@ -3016,7 +3016,7 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
3016
3016
  } else attachSection = `<p class="hint">${escapeHtml(s("dashboard.attach.hint"))}</p>`;
3017
3017
  const pagesAttached = Array.isArray(state.pages) && state.pages.length > 0;
3018
3018
  let inspectorSection;
3019
- if (pagesAttached && state.inspectorUrl) inspectorSection = `<a class="inspector-link" id="inspector-link" href="${escapeHtml(state.inspectorUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(s("dashboard.inspector.open"))}</a>`;
3019
+ if (pagesAttached && devtoolsEntryUrl) inspectorSection = `<a class="inspector-link" id="inspector-link" href="${escapeHtml(devtoolsEntryUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(s("dashboard.inspector.open"))}</a>`;
3020
3020
  else inspectorSection = `<span class="inspector-hint" id="inspector-link">${escapeHtml(s("dashboard.inspector.waiting"))}</span>`;
3021
3021
  const pagesSection = state.pages === null ? "" : `<hr /><section id="pages-section"><h2>${escapeHtml(s("dashboard.pages.section"))}</h2><ul id="pages-list">${state.pages.length > 0 ? state.pages.map((p) => {
3022
3022
  return `<li><span class="page-id">${escapeHtml(p.id)}</span> <span class="page-url">${escapeHtml(p.url.slice(0, 120))}</span></li>`;
@@ -3301,7 +3301,13 @@ async function startQrHttpServer(getDashboardState, options) {
3301
3301
  errorCorrectionLevel: "M"
3302
3302
  });
3303
3303
  } catch {}
3304
- const html = buildDashboardHtml(state, qrDataUrl, locale, path, params);
3304
+ const devtoolsEntryUrl = (() => {
3305
+ if (!options?.getDirectInspectorUrl) return null;
3306
+ const addr = server.address();
3307
+ if (!addr || typeof addr === "string") return null;
3308
+ return `http://127.0.0.1:${addr.port}/devtools/`;
3309
+ })();
3310
+ const html = buildDashboardHtml(state, qrDataUrl, locale, path, params, devtoolsEntryUrl);
3305
3311
  res.writeHead(200, {
3306
3312
  "Content-Type": "text/html; charset=utf-8",
3307
3313
  "Cache-Control": "no-store"
@@ -3369,11 +3375,12 @@ async function startQrHttpServer(getDashboardState, options) {
3369
3375
  });
3370
3376
  return;
3371
3377
  }
3372
- if (path === "/inspector") {
3378
+ if (path === "/inspector" || path === "/devtools" || path === "/devtools/") {
3373
3379
  const getDirectInspectorUrl = options?.getDirectInspectorUrl;
3374
3380
  if (!getDirectInspectorUrl) {
3381
+ const body = path === "/inspector" ? "Inspector endpoint is not available in this server mode." : "relay 연결 세션에서만 DevTools UI를 열 수 있습니다.";
3375
3382
  res.writeHead(503, { "Content-Type": "text/plain; charset=utf-8" });
3376
- res.end("Inspector endpoint is not available in this server mode.");
3383
+ res.end(body);
3377
3384
  return;
3378
3385
  }
3379
3386
  const result = getDirectInspectorUrl();
@@ -4858,7 +4865,7 @@ async function readMcpSdkVersion() {
4858
4865
  * some test environments that skip the build step).
4859
4866
  */
4860
4867
  function readDevtoolsVersion() {
4861
- return "0.1.100";
4868
+ return "0.1.102";
4862
4869
  }
4863
4870
  /**
4864
4871
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -5313,6 +5320,52 @@ function makeTunnelStatus(up, wssUrl, droppedAt = null, reissueAttempts = 0) {
5313
5320
  * Node-only.
5314
5321
  */
5315
5322
  /**
5323
+ * Maximum age (ms) of a page's `lastSeenAt` before it is treated as a ghost
5324
+ * and excluded from the `wait_for_attach` short-circuit in `build_attach_url`
5325
+ * (issue #610).
5326
+ *
5327
+ * Rationale: the env-2 relay is owned by the dev server (unplugin), so every
5328
+ * `dev:phone:cdp` restart produces a new quick-tunnel. The old relay goes
5329
+ * offline immediately, but the daemon's warm `ChiiCdpConnection` still lists
5330
+ * the last-seen target — its `lastSeenAt` freezes at the moment the old relay
5331
+ * died. A 5-minute threshold is large enough to be invisible in normal usage
5332
+ * (active CDP sessions see a message every few seconds) while being small
5333
+ * enough to catch a relay that went down before the daemon was re-entered.
5334
+ *
5335
+ * Injectable for tests via {@link DebugServerDeps.stalePageThresholdMs}.
5336
+ */
5337
+ const RELAY_SANDBOX_STALE_PAGE_MS = 300 * 1e3;
5338
+ /**
5339
+ * Predicate used by `build_attach_url`'s `wait_for_attach` loop to decide
5340
+ * whether the relay-sandbox connection has a genuinely fresh page attached.
5341
+ *
5342
+ * Stale-ghost gating (issue #610): when the dev server restarts with a new
5343
+ * quick-tunnel, the warm `ChiiCdpConnection` still lists the last-seen target
5344
+ * but its `lastSeenAt` is frozen. A page whose `lastSeenAt` exceeds
5345
+ * `stalePageThresholdMs` is a ghost from the dead relay — it must NOT
5346
+ * short-circuit `wait_for_attach`.
5347
+ *
5348
+ * Rules:
5349
+ * - `pages.length === 0` → false (nothing attached).
5350
+ * - Connection has no `getLastSeenAt` (test fakes, local-browser) → falls back
5351
+ * to `pages.length > 0` (regression-safe).
5352
+ * - `seenMs === null` → treat as fresh (no CDP message received yet, first
5353
+ * message pending — the connection is alive).
5354
+ * - Otherwise: at least one page must satisfy `nowMs - seenMs <=
5355
+ * stalePageThresholdMs`.
5356
+ *
5357
+ * Exported for unit testing.
5358
+ */
5359
+ function isSandboxPageFresh(pages, getLastSeenAt, nowMs, stalePageThresholdMs) {
5360
+ if (pages.length === 0) return false;
5361
+ if (getLastSeenAt === null) return true;
5362
+ return pages.some((p) => {
5363
+ const seenMs = getLastSeenAt(p.id);
5364
+ if (seenMs === null) return true;
5365
+ return nowMs - seenMs <= stalePageThresholdMs;
5366
+ });
5367
+ }
5368
+ /**
5316
5369
  * Parses `_deploymentId` from the query string of a scheme URL.
5317
5370
  *
5318
5371
  * Returns `null` when the param is absent or empty — callers treat that as
@@ -5395,7 +5448,7 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
5395
5448
  * naturally via `enableDomains`). The tier only controls visibility.
5396
5449
  */
5397
5450
  function createDebugServer(deps) {
5398
- const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 6e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret, onAttachUrlBuilt, getTunnelChildPid, readLock: readLockDep } = deps;
5451
+ const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 6e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret, onAttachUrlBuilt, getTunnelChildPid, readLock: readLockDep, stalePageThresholdMs = RELAY_SANDBOX_STALE_PAGE_MS, nowMs = () => Date.now() } = deps;
5399
5452
  const getTotpSecret = deps.getTotpSecret ?? (() => totpSecret);
5400
5453
  const readLockFn = readLockDep ?? readServerLock;
5401
5454
  const router = routerDep ?? makeSingleConnectionRouter(connection);
@@ -5404,7 +5457,7 @@ function createDebugServer(deps) {
5404
5457
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
5405
5458
  const server = new Server({
5406
5459
  name: "ait-debug",
5407
- version: "0.1.100"
5460
+ version: "0.1.102"
5408
5461
  }, { capabilities: { tools: { listChanged: true } } });
5409
5462
  server.setRequestHandler(ListToolsRequestSchema, () => {
5410
5463
  const conn = router.active;
@@ -5492,7 +5545,7 @@ function createDebugServer(deps) {
5492
5545
  const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
5493
5546
  let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
5494
5547
  if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
5495
- const { readRelayUrls } = await import("../relay-url-store-RKcao_yG.js");
5548
+ const { readRelayUrls } = await import("../relay-url-store-BPeUZsiY.js");
5496
5549
  tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
5497
5550
  }
5498
5551
  if (tunnelHttpUrl === "") return mcpError("build_attach_url(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. 자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.");
@@ -5533,7 +5586,10 @@ function createDebugServer(deps) {
5533
5586
  });
5534
5587
  const relayUrl = tunnelStatus.wssUrl;
5535
5588
  const totp = totpMeta;
5536
- const isMatchingPage = (pages) => pages.length > 0;
5589
+ const connAsAny = conn;
5590
+ const getLastSeenAt = typeof connAsAny.getTargetLastSeenAt === "function" ? (id) => connAsAny.getTargetLastSeenAt(id) : null;
5591
+ const callNow = nowMs();
5592
+ const isMatchingPage = (pages) => isSandboxPageFresh(pages, getLastSeenAt, callNow, stalePageThresholdMs);
5537
5593
  const buildTimeoutError = (baseText, timeoutSec, observed) => {
5538
5594
  const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
5539
5595
  return `${baseText}\n\nNo page attached within ${timeoutSec}s${observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : ""} — launcher QR을 폰 카메라로 스캔한 뒤 call list_pages를 다시 호출하세요.`;
@@ -6242,7 +6298,7 @@ async function readRelayLocalUrl(env = process.env, projectRoot) {
6242
6298
  const envValue = (env.AIT_RELAY_LOCAL_URL ?? "").trim();
6243
6299
  if (envValue !== "") return envValue;
6244
6300
  if (projectRoot !== void 0) try {
6245
- const { readRelayUrls } = await import("../relay-url-store-RKcao_yG.js");
6301
+ const { readRelayUrls } = await import("../relay-url-store-BPeUZsiY.js");
6246
6302
  const stored = await readRelayUrls({ projectRoot });
6247
6303
  if (stored?.relayLocalUrl) return stored.relayLocalUrl;
6248
6304
  } catch {}
@@ -6296,7 +6352,7 @@ async function readMobileRelayBaseUrl(env = process.env, projectRoot) {
6296
6352
  const envValue = typeof raw === "string" ? raw.trim() : "";
6297
6353
  if (envValue !== "") return envValue;
6298
6354
  if (projectRoot !== void 0) {
6299
- const { readRelayUrls } = await import("../relay-url-store-RKcao_yG.js");
6355
+ const { readRelayUrls } = await import("../relay-url-store-BPeUZsiY.js");
6300
6356
  const stored = await readRelayUrls({ projectRoot });
6301
6357
  if (stored?.relayBaseUrl !== void 0) return stored.relayBaseUrl;
6302
6358
  }
@@ -6459,10 +6515,39 @@ var DualConnectionRouter = class {
6459
6515
  * `projectRoot` is forwarded to `bootLazyFor` so `relay-sandbox` boot can
6460
6516
  * fall back to `.ait_urls` file discovery (#424) when `AIT_RELAY_BASE_URL` is
6461
6517
  * not set in the environment.
6518
+ *
6519
+ * **Relay-sandbox stale-URL rebuild (issue #610):** when the `relay-sandbox`
6520
+ * family is already warm, reads the current relay URL via
6521
+ * `deps.readSandboxRelayUrl` and compares it against the cached
6522
+ * `relayHttpUrl`. If they differ (dev server was restarted → new tunnel),
6523
+ * the stale family is torn down, evicted from the map, and a fresh one is
6524
+ * booted. If they match, or if the URL cannot be read, the warm family is
6525
+ * reused (fail-open — no unnecessary teardown on transient read errors).
6526
+ *
6527
+ * SECRET-HANDLING: fresh and cached relay URLs carry the tunnel host. The
6528
+ * comparison result (same/different) is the only thing surfaced — URLs are
6529
+ * never logged.
6462
6530
  */
6463
6531
  async familyFor(key, projectRoot) {
6464
6532
  const warm = this.lazyFamilies.get(key);
6465
- if (warm) return warm;
6533
+ if (warm) {
6534
+ if (key === "relay-sandbox" && this.deps.readSandboxRelayUrl !== void 0) {
6535
+ let freshUrl = null;
6536
+ try {
6537
+ freshUrl = await this.deps.readSandboxRelayUrl(projectRoot);
6538
+ } catch {
6539
+ freshUrl = null;
6540
+ }
6541
+ if (freshUrl !== null && freshUrl !== warm.relayHttpUrl) {
6542
+ warm.stop();
6543
+ this.lazyFamilies.delete(key);
6544
+ const booted = await this.deps.bootLazyFor(key, projectRoot);
6545
+ this.lazyFamilies.set(key, booted);
6546
+ return booted;
6547
+ }
6548
+ }
6549
+ return warm;
6550
+ }
6466
6551
  const booted = await this.deps.bootLazyFor(key, projectRoot);
6467
6552
  this.lazyFamilies.set(key, booted);
6468
6553
  return booted;
@@ -6522,7 +6607,8 @@ async function runDebugServer(options = {}) {
6522
6607
  diagnosticsCollector,
6523
6608
  devtoolsOpener,
6524
6609
  onPageAttach: () => qrServer?.notifyStateChange(),
6525
- getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null
6610
+ getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,
6611
+ readSandboxRelayUrl: (pr) => readMobileRelayBaseUrl(process.env, pr).catch(() => null)
6526
6612
  });
6527
6613
  const aitSource = new RoutingAitSource(() => {
6528
6614
  return router.active;
@@ -6734,7 +6820,8 @@ async function runLocalDebugServer(options = {}) {
6734
6820
  diagnosticsCollector,
6735
6821
  devtoolsOpener,
6736
6822
  onPageAttach: () => qrServer?.notifyStateChange(),
6737
- getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null
6823
+ getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,
6824
+ readSandboxRelayUrl: (pr) => readMobileRelayBaseUrl(process.env, pr).catch(() => null)
6738
6825
  });
6739
6826
  const aitSource = new RoutingAitSource(() => {
6740
6827
  return router.active;
@@ -6929,7 +7016,8 @@ async function runMobileDebugServer(options = {}) {
6929
7016
  diagnosticsCollector,
6930
7017
  devtoolsOpener,
6931
7018
  onPageAttach: () => qrServer?.notifyStateChange(),
6932
- getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null
7019
+ getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null,
7020
+ readSandboxRelayUrl: (pr) => readMobileRelayBaseUrl(process.env, pr ?? options.projectRoot ?? process.cwd()).catch(() => null)
6933
7021
  });
6934
7022
  const aitSource = new RoutingAitSource(() => {
6935
7023
  return router.active;
@@ -7503,7 +7591,7 @@ function createDevServer(deps = {}) {
7503
7591
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
7504
7592
  const server = new Server({
7505
7593
  name: "ait-devtools",
7506
- version: "0.1.100"
7594
+ version: "0.1.102"
7507
7595
  }, { capabilities: { tools: {} } });
7508
7596
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
7509
7597
  server.setRequestHandler(CallToolRequestSchema, async (request) => {