@ait-co/devtools 0.1.79 → 0.1.80

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 (43) hide show
  1. package/dist/devtools-opener-BbUXBzgA.js.map +1 -1
  2. package/dist/devtools-opener-Bp671YXu.cjs.map +1 -1
  3. package/dist/devtools-opener-D84kZFtR.js.map +1 -1
  4. package/dist/devtools-opener-h6A-UjzC.cjs.map +1 -1
  5. package/dist/mcp/cli.js +214 -55
  6. package/dist/mcp/cli.js.map +1 -1
  7. package/dist/mcp/server.js +1 -1
  8. package/dist/panel/index.js +6 -2
  9. package/dist/panel/index.js.map +1 -1
  10. package/dist/{qr-http-server-D2d44bv7.js → qr-http-server-DJ5K3Odk.js} +34 -1
  11. package/dist/qr-http-server-DJ5K3Odk.js.map +1 -0
  12. package/dist/{qr-http-server-Dx7KnQtg.js → qr-http-server-DkOFfZsR.js} +34 -1
  13. package/dist/qr-http-server-DkOFfZsR.js.map +1 -0
  14. package/dist/{qr-http-server-DOOLghY0.cjs → qr-http-server-Dkx2-pKF.cjs} +34 -1
  15. package/dist/qr-http-server-Dkx2-pKF.cjs.map +1 -0
  16. package/dist/{qr-http-server-oENyLvn9.cjs → qr-http-server-MIUHaiYw.cjs} +34 -1
  17. package/dist/qr-http-server-MIUHaiYw.cjs.map +1 -0
  18. package/dist/{relay-url-store-Cx_SqWtl.cjs → relay-url-store-BiEK9BN1.cjs} +3 -1
  19. package/dist/relay-url-store-BiEK9BN1.cjs.map +1 -0
  20. package/dist/{relay-url-store-CV8nScsn.js → relay-url-store-DH8-VUFc.js} +3 -1
  21. package/dist/relay-url-store-DH8-VUFc.js.map +1 -0
  22. package/dist/{relay-url-store-B_wrNe5A.js → relay-url-store-RKcao_yG.js} +6 -1
  23. package/dist/relay-url-store-RKcao_yG.js.map +1 -0
  24. package/dist/{tunnel-8h2r-ouK.js → tunnel-BTlq1mmH.js} +2 -2
  25. package/dist/{tunnel-8h2r-ouK.js.map → tunnel-BTlq1mmH.js.map} +1 -1
  26. package/dist/{tunnel-CInRDnKE.cjs → tunnel-C-AFdAVL.cjs} +2 -2
  27. package/dist/{tunnel-CInRDnKE.cjs.map → tunnel-C-AFdAVL.cjs.map} +1 -1
  28. package/dist/unplugin/index.cjs +6 -3
  29. package/dist/unplugin/index.cjs.map +1 -1
  30. package/dist/unplugin/index.d.cts.map +1 -1
  31. package/dist/unplugin/index.d.ts.map +1 -1
  32. package/dist/unplugin/index.js +6 -3
  33. package/dist/unplugin/index.js.map +1 -1
  34. package/dist/unplugin/tunnel.cjs +1 -1
  35. package/dist/unplugin/tunnel.js +1 -1
  36. package/package.json +1 -1
  37. package/dist/qr-http-server-D2d44bv7.js.map +0 -1
  38. package/dist/qr-http-server-DOOLghY0.cjs.map +0 -1
  39. package/dist/qr-http-server-Dx7KnQtg.js.map +0 -1
  40. package/dist/qr-http-server-oENyLvn9.cjs.map +0 -1
  41. package/dist/relay-url-store-B_wrNe5A.js.map +0 -1
  42. package/dist/relay-url-store-CV8nScsn.js.map +0 -1
  43. package/dist/relay-url-store-Cx_SqWtl.cjs.map +0 -1
package/dist/mcp/cli.js CHANGED
@@ -1289,51 +1289,67 @@ function openUrlInBrowser(url) {
1289
1289
  return false;
1290
1290
  }
1291
1291
  /**
1292
- * Manages auto-opening Chrome DevTools exactly once per relay attach session.
1292
+ * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).
1293
1293
  *
1294
1294
  * Create one instance per `runDebugServer` call and pass its `open()` method
1295
- * as the `onFirstAttach` callback to `startAttachWatcher`.
1295
+ * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).
1296
+ *
1297
+ * The open fires for each NEW `targetId` — subsequent notifications for the
1298
+ * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after
1299
+ * page reload on the phone) fires a new open. The URL opened is the stable
1300
+ * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —
1301
+ * it mints a fresh TOTP at click time so there is no expiry race. Falls back to
1302
+ * building a direct `front_end/chii_app.html?wss=…` URL when
1303
+ * `inspectorStableUrl` is absent.
1296
1304
  *
1297
- * The open fires at most once. Subsequent `open()` calls are no-ops.
1298
1305
  * Opt-out and mock-environment guard are checked at call time.
1299
1306
  */
1300
1307
  var AutoDevtoolsOpener = class {
1301
- _opened = false;
1308
+ /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */
1309
+ _openedTargets = /* @__PURE__ */ new Set();
1302
1310
  /**
1303
1311
  * Attempts to auto-open Chii DevTools in the developer's browser.
1304
1312
  *
1305
- * Builds a `<relay-base>/front_end/chii_app.html?wss=…` URL pointing at the
1306
- * attached target. A fresh TOTP `at=` code is minted at call time so the
1307
- * relay's WebSocket upgrade gate accepts the connection.
1313
+ * Opens when:
1314
+ * - `options.targetId` is a NEW target (not yet in `_openedTargets`).
1308
1315
  *
1309
1316
  * No-op when any of the following conditions hold:
1310
- * 1. Already opened this session (`_opened` is true).
1317
+ * 1. `targetId` has already been opened (`_openedTargets` has it).
1311
1318
  * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.
1312
1319
  * 3. `options.env` is `mock` (env 1 — F12 is already available).
1313
- * 4. `options.relayHttpBaseUrl` is null/undefined/empty (relay not up yet).
1314
- * 5. `options.targetId` is null/undefined/empty (no page attached yet).
1320
+ * 4. `options.targetId` is null/undefined/empty (no page attached yet).
1321
+ * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.
1315
1322
  *
1316
- * Always writes the DevTools URL to stderr so the developer can copy it
1317
- * if the browser open fails or the popup is blocked.
1323
+ * When `inspectorStableUrl` is provided (issue #530 stable URL): opens
1324
+ * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.
1325
+ * The URL contains no tunnel host or TOTP code — safe to log anywhere.
1318
1326
  *
1319
- * TOTP expiry caveat: the `at=` code embedded in the URL is valid for ~3
1320
- * minutes (relay gate ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). The
1321
- * developer must open the URL within that window; if they miss it, reload
1322
- * the page or re-run `open()` (though the once-per-session guard prevents
1323
- * that — restart the MCP server if needed).
1327
+ * Legacy path (no `inspectorStableUrl`): builds a direct
1328
+ * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`
1329
+ * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).
1324
1330
  *
1325
- * SECRET-HANDLING: the inspector URL (written to stderr) contains the relay
1326
- * host and a short-lived TOTP code. Do NOT write it to stdout or any
1327
- * persistent log.
1331
+ * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay
1332
+ * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or
1333
+ * persistent logs.
1328
1334
  */
1329
1335
  open(options) {
1330
- if (this._opened) return;
1331
1336
  if (isAutoDevtoolsDisabled()) return;
1332
1337
  if (options.env === "mock") return;
1333
- if (!options.relayHttpBaseUrl) return;
1334
1338
  if (!options.targetId) return;
1335
- this._opened = true;
1336
- const inspectorUrl = buildChiiInspectorUrl(options.relayHttpBaseUrl, options.targetId, options.mintTotp);
1339
+ const targetId = options.targetId;
1340
+ if (this._openedTargets.has(targetId)) return;
1341
+ if (options.inspectorStableUrl) {
1342
+ this._openedTargets.add(targetId);
1343
+ const stableUrl = options.inspectorStableUrl;
1344
+ process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chii DevTools를 자동으로 엽니다.
1345
+ [ait-debug] 인스펙터 URL: ${stableUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)
1346
+ `);
1347
+ if (!openUrlInBrowser(stableUrl)) process.stderr.write(`[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\n`);
1348
+ return;
1349
+ }
1350
+ if (!options.relayHttpBaseUrl) return;
1351
+ this._openedTargets.add(targetId);
1352
+ const inspectorUrl = buildChiiInspectorUrl(options.relayHttpBaseUrl, targetId, options.mintTotp);
1337
1353
  if (inspectorUrl === null) {
1338
1354
  process.stderr.write("[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\n[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\n");
1339
1355
  return;
@@ -1344,9 +1360,17 @@ var AutoDevtoolsOpener = class {
1344
1360
  `);
1345
1361
  if (!openUrlInBrowser(inspectorUrl)) process.stderr.write("[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\n");
1346
1362
  }
1347
- /** Returns `true` if `open()` has passed all guards and fired once. */
1363
+ /**
1364
+ * Returns `true` if `open()` has been called for at least one target.
1365
+ * (Replaces the old once-per-session `_opened` flag; kept for interface
1366
+ * compatibility with tests that read `opener.opened`.)
1367
+ */
1348
1368
  get opened() {
1349
- return this._opened;
1369
+ return this._openedTargets.size > 0;
1370
+ }
1371
+ /** Returns the set of target IDs that have already been auto-opened. */
1372
+ get openedTargets() {
1373
+ return this._openedTargets;
1350
1374
  }
1351
1375
  };
1352
1376
  //#endregion
@@ -2112,6 +2136,8 @@ const en = {
2112
2136
  "dashboard.inspector.section": "Inspector",
2113
2137
  "dashboard.inspector.open": "Open inspector",
2114
2138
  "dashboard.inspector.waiting": "Inspector URL pending — appears after a page attaches",
2139
+ "inspector.error.noTarget": "No page attached. Attach a device and try again.",
2140
+ "inspector.error.relayDown": "Relay is not active. Start a relay session first.",
2115
2141
  "attach.title": "AIT Debug Session — QR Scan",
2116
2142
  "attach.deployment": "deployment: {label}",
2117
2143
  "attach.steps.section": "How to scan",
@@ -2364,6 +2390,8 @@ const tables = {
2364
2390
  "dashboard.inspector.section": "인스펙터",
2365
2391
  "dashboard.inspector.open": "인스펙터 열기",
2366
2392
  "dashboard.inspector.waiting": "인스펙터 URL 대기 중 (페이지 attach 후 표시됩니다)",
2393
+ "inspector.error.noTarget": "연결된 페이지가 없습니다. 기기를 attach한 후 다시 시도하세요.",
2394
+ "inspector.error.relayDown": "relay가 활성화되지 않았습니다. start_debug로 relay를 기동하세요.",
2367
2395
  "attach.title": "AIT 디버그 세션 — QR 스캔",
2368
2396
  "attach.deployment": "deployment: {label}",
2369
2397
  "attach.steps.section": "스캔 절차",
@@ -3116,6 +3144,7 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/a
3116
3144
  * @param getDashboardState - dashboard 상태를 반환하는 클로저. 주입 시 `GET /` dashboard와
3117
3145
  * `GET /events` SSE 스트림이 활성화된다. 미주입 시 두 라우트는 204/서비스 없음으로 응답.
3118
3146
  * @param options - 서버 옵션. `sseRefreshIntervalMs`로 idle 탭 TOTP 만료 방지 주기를 조정.
3147
+ * `getDirectInspectorUrl`로 /inspector 라우트에서 직접 조립 URL을 제공해 redirect 루프를 방지.
3119
3148
  */
3120
3149
  async function startQrHttpServer(getDashboardState, options) {
3121
3150
  const { default: QRCode } = await import("qrcode");
@@ -3213,6 +3242,31 @@ async function startQrHttpServer(getDashboardState, options) {
3213
3242
  });
3214
3243
  return;
3215
3244
  }
3245
+ if (path === "/inspector") {
3246
+ const getDirectInspectorUrl = options?.getDirectInspectorUrl;
3247
+ if (!getDirectInspectorUrl) {
3248
+ res.writeHead(503, { "Content-Type": "text/plain; charset=utf-8" });
3249
+ res.end("Inspector endpoint is not available in this server mode.");
3250
+ return;
3251
+ }
3252
+ const result = getDirectInspectorUrl();
3253
+ const s = resolveLocaleStrings(locale);
3254
+ if (!result.ok) {
3255
+ const body = `<!DOCTYPE html><html lang="${locale}"><head><meta charset="utf-8"><title>Inspector</title></head><body><p>${escapeHtml(s(result.reason === "noTarget" ? "inspector.error.noTarget" : "inspector.error.relayDown"))}</p><p style="font-size:0.9em;color:#666">` + (locale === "ko" ? "(<a href=\"/\">대시보드로 돌아가기</a>)" : "(<a href=\"/\">Back to dashboard</a>)") + `</p></body></html>`;
3256
+ res.writeHead(502, {
3257
+ "Content-Type": "text/html; charset=utf-8",
3258
+ "Cache-Control": "no-store"
3259
+ });
3260
+ res.end(body);
3261
+ return;
3262
+ }
3263
+ res.writeHead(302, {
3264
+ Location: result.url,
3265
+ "Cache-Control": "no-store"
3266
+ });
3267
+ res.end();
3268
+ return;
3269
+ }
3216
3270
  if (path === "/qr.png") {
3217
3271
  const encodedU = params.get("u") ?? "";
3218
3272
  let attachUrl;
@@ -3267,6 +3321,9 @@ async function startQrHttpServer(getDashboardState, options) {
3267
3321
  buildAttachPageUrl(attachUrl) {
3268
3322
  return `http://127.0.0.1:${port}/attach?u=${encodeURIComponent(attachUrl)}`;
3269
3323
  },
3324
+ get inspectorStableUrl() {
3325
+ return `http://127.0.0.1:${port}/inspector`;
3326
+ },
3270
3327
  notifyStateChange() {
3271
3328
  notifyStateChangeInternal();
3272
3329
  },
@@ -4660,7 +4717,7 @@ async function readMcpSdkVersion() {
4660
4717
  * some test environments that skip the build step).
4661
4718
  */
4662
4719
  function readDevtoolsVersion() {
4663
- return "0.1.79";
4720
+ return "0.1.80";
4664
4721
  }
4665
4722
  /**
4666
4723
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -5164,7 +5221,7 @@ function createDebugServer(deps) {
5164
5221
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
5165
5222
  const server = new Server({
5166
5223
  name: "ait-debug",
5167
- version: "0.1.79"
5224
+ version: "0.1.80"
5168
5225
  }, { capabilities: { tools: { listChanged: true } } });
5169
5226
  server.setRequestHandler(ListToolsRequestSchema, () => {
5170
5227
  const conn = router.active;
@@ -5243,7 +5300,7 @@ function createDebugServer(deps) {
5243
5300
  const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
5244
5301
  let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
5245
5302
  if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
5246
- const { readRelayUrls } = await import("../relay-url-store-B_wrNe5A.js");
5303
+ const { readRelayUrls } = await import("../relay-url-store-RKcao_yG.js");
5247
5304
  tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
5248
5305
  }
5249
5306
  if (tunnelHttpUrl === "") return mcpError("build_attach_url(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. 자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.");
@@ -5974,7 +6031,26 @@ async function bootRelayFamily(options = {}) {
5974
6031
  * wss URL) — it is NEVER logged here. The caller validates presence and passes
5975
6032
  * the value straight to the CDP client.
5976
6033
  */
5977
- async function bootExternalRelayFamily(relayBaseUrl) {
6034
+ /**
6035
+ * Attempts to read the local loopback HTTP base URL of the env-2 Chii relay
6036
+ * (issue #530). Resolution order:
6037
+ * 1. `AIT_RELAY_LOCAL_URL` env var, if set and non-empty.
6038
+ * 2. `relayLocalUrl` from the `.ait_urls` file, if `projectRoot` is given.
6039
+ * 3. `undefined` — caller falls back to the tunnel base (existing behavior).
6040
+ *
6041
+ * This is a best-effort read — never throws. The returned value is a plain
6042
+ * `http://127.0.0.1:<port>` loopback URL; no secret exposure.
6043
+ */
6044
+ async function readRelayLocalUrl(env = process.env, projectRoot) {
6045
+ const envValue = (env.AIT_RELAY_LOCAL_URL ?? "").trim();
6046
+ if (envValue !== "") return envValue;
6047
+ if (projectRoot !== void 0) try {
6048
+ const { readRelayUrls } = await import("../relay-url-store-RKcao_yG.js");
6049
+ const stored = await readRelayUrls({ projectRoot });
6050
+ if (stored?.relayLocalUrl) return stored.relayLocalUrl;
6051
+ } catch {}
6052
+ }
6053
+ async function bootExternalRelayFamily(relayBaseUrl, relayLocalUrl) {
5978
6054
  assertRelayAuthConfigured();
5979
6055
  const connection = createRelayConnection(relayBaseUrl);
5980
6056
  const tunnelStatus = makeTunnelStatus(true, relayBaseUrl.replace(/^http/, "ws"));
@@ -5982,6 +6058,7 @@ async function bootExternalRelayFamily(relayBaseUrl) {
5982
6058
  connection,
5983
6059
  relayOrigin: "external-pwa",
5984
6060
  relayHttpUrl: relayBaseUrl,
6061
+ relayLocalHttpUrl: relayLocalUrl,
5985
6062
  getTunnelStatus: () => tunnelStatus,
5986
6063
  stop() {
5987
6064
  connection.close();
@@ -6022,7 +6099,7 @@ async function readMobileRelayBaseUrl(env = process.env, projectRoot) {
6022
6099
  const envValue = typeof raw === "string" ? raw.trim() : "";
6023
6100
  if (envValue !== "") return envValue;
6024
6101
  if (projectRoot !== void 0) {
6025
- const { readRelayUrls } = await import("../relay-url-store-B_wrNe5A.js");
6102
+ const { readRelayUrls } = await import("../relay-url-store-RKcao_yG.js");
6026
6103
  const stored = await readRelayUrls({ projectRoot });
6027
6104
  if (stored?.relayBaseUrl !== void 0) return stored.relayBaseUrl;
6028
6105
  }
@@ -6106,13 +6183,18 @@ var DualConnectionRouter = class {
6106
6183
  return this.activeFamily?.relayOrigin;
6107
6184
  }
6108
6185
  /**
6109
- * Local HTTP base URL of the Chii relay for the currently-active family (#503).
6110
- * Used by `getDashboardState` to build the inspector URL via `buildChiiInspectorUrl`.
6111
- * Returns `undefined` when no relay family is active (local/mock mode).
6112
- * SECRET-HANDLING: not logged callers must not write this to stdout/logs.
6186
+ * HTTP base URL of the Chii relay to use for inspector URL assembly (#503,
6187
+ * #530). Prefers the LOCAL loopback base (`relayLocalHttpUrl`) when available
6188
+ * so front_end page load + client WS do not traverse a cloudflare tunnel —
6189
+ * falls back to `relayHttpUrl` (the tunnel base for env-2, loopback for env-3/4)
6190
+ * when not set. Returns `undefined` when no relay family is active.
6191
+ *
6192
+ * SECRET-HANDLING: when relayLocalHttpUrl is absent this falls back to
6193
+ * relayHttpUrl which may carry the tunnel host — callers must not log it.
6113
6194
  */
6114
6195
  get activeRelayHttpUrl() {
6115
- return this.activeFamily?.relayHttpUrl;
6196
+ if (!this.activeFamily) return void 0;
6197
+ return this.activeFamily.relayLocalHttpUrl ?? this.activeFamily.relayHttpUrl;
6116
6198
  }
6117
6199
  /** Every booted family (for unified shutdown). All families are lazy (#396). */
6118
6200
  bootedFamilies() {
@@ -6160,7 +6242,9 @@ var DualConnectionRouter = class {
6160
6242
  if (activeFamily.connection.kind === "relay") {
6161
6243
  const firstTarget = activeFamily.connection.listTargets()[0];
6162
6244
  const env = deriveEnvironment(activeFamily.connection.kind, getLiveIntent(), activeFamily.relayOrigin);
6245
+ const inspectorStableUrl = this.deps.getInspectorStableUrl?.() ?? null;
6163
6246
  this.deps.devtoolsOpener.open({
6247
+ inspectorStableUrl,
6164
6248
  relayHttpBaseUrl: activeFamily.relayHttpUrl,
6165
6249
  targetId: firstTarget?.id,
6166
6250
  mintTotp: process.env.AIT_DEBUG_TOTP_SECRET ? () => generateTotp(process.env.AIT_DEBUG_TOTP_SECRET) : void 0,
@@ -6224,7 +6308,7 @@ async function runDebugServer(options = {}) {
6224
6308
  const devtoolsOpener = new AutoDevtoolsOpener();
6225
6309
  const diagnosticsCollector = new InMemoryDiagnosticsCollector();
6226
6310
  const router = new DualConnectionRouter({
6227
- bootLazyFor: async (key, projectRoot) => key === "relay-sandbox" ? bootExternalRelayFamily(await readMobileRelayBaseUrl(process.env, projectRoot)) : key === "local-browser" ? bootLocalFamily() : bootRelayFamily({
6311
+ bootLazyFor: async (key, projectRoot) => key === "relay-sandbox" ? bootExternalRelayFamily(await readMobileRelayBaseUrl(process.env, projectRoot), await readRelayLocalUrl(process.env, projectRoot)) : key === "local-browser" ? bootLocalFamily() : bootRelayFamily({
6228
6312
  relayPort: options.relayPort,
6229
6313
  verifyAuth: buildRelayVerifyAuth(),
6230
6314
  onWssUrl: (wssUrl) => {
@@ -6235,7 +6319,8 @@ async function runDebugServer(options = {}) {
6235
6319
  }),
6236
6320
  diagnosticsCollector,
6237
6321
  devtoolsOpener,
6238
- onPageAttach: () => qrServer?.notifyStateChange()
6322
+ onPageAttach: () => qrServer?.notifyStateChange(),
6323
+ getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null
6239
6324
  });
6240
6325
  const aitSource = new RoutingAitSource(() => {
6241
6326
  return router.active;
@@ -6243,9 +6328,7 @@ async function runDebugServer(options = {}) {
6243
6328
  let lastAttachParts = null;
6244
6329
  const getDashboardState = () => {
6245
6330
  const targets = router.active.listTargets();
6246
- const relayHttpUrl = router.activeRelayHttpUrl;
6247
- const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;
6248
- const inspectorUrl = relayHttpUrl && targets.length > 0 ? buildChiiInspectorUrl(relayHttpUrl, targets[0].id, totpSecret ? () => generateTotp(totpSecret, Date.now()) : void 0) : null;
6331
+ const inspectorUrl = qrServer?.inspectorStableUrl ?? null;
6249
6332
  return {
6250
6333
  tunnel: {
6251
6334
  up: router.relayTunnelStatus().up,
@@ -6260,9 +6343,35 @@ async function runDebugServer(options = {}) {
6260
6343
  mode: deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin)
6261
6344
  };
6262
6345
  };
6346
+ const getDirectInspectorUrl = () => {
6347
+ const relayHttpUrl = router.activeRelayHttpUrl;
6348
+ if (!relayHttpUrl) return {
6349
+ ok: false,
6350
+ reason: "relayDown"
6351
+ };
6352
+ const targets = router.active.listTargets();
6353
+ if (targets.length === 0) return {
6354
+ ok: false,
6355
+ reason: "noTarget"
6356
+ };
6357
+ const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;
6358
+ if (!totpSecret) return {
6359
+ ok: false,
6360
+ reason: "totpUnavailable"
6361
+ };
6362
+ const url = buildChiiInspectorUrl(relayHttpUrl, targets[0].id, () => generateTotp(totpSecret, Date.now()));
6363
+ if (url === null) return {
6364
+ ok: false,
6365
+ reason: "totpUnavailable"
6366
+ };
6367
+ return {
6368
+ ok: true,
6369
+ url
6370
+ };
6371
+ };
6263
6372
  let qrServer;
6264
6373
  try {
6265
- qrServer = await startQrHttpServer(getDashboardState);
6374
+ qrServer = await startQrHttpServer(getDashboardState, { getDirectInspectorUrl });
6266
6375
  } catch (err) {
6267
6376
  logWarn("server.start", { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${err instanceof Error ? err.message : String(err)}` });
6268
6377
  }
@@ -6398,7 +6507,7 @@ async function runLocalDebugServer(options = {}) {
6398
6507
  const devtoolsOpener = new AutoDevtoolsOpener();
6399
6508
  const diagnosticsCollector = new InMemoryDiagnosticsCollector();
6400
6509
  const router = new DualConnectionRouter({
6401
- bootLazyFor: async (key, projectRoot) => key === "relay-sandbox" ? bootExternalRelayFamily(await readMobileRelayBaseUrl(process.env, projectRoot)) : key === "local-browser" ? bootLocalFamilyForEntry() : bootRelayFamily({
6510
+ bootLazyFor: async (key, projectRoot) => key === "relay-sandbox" ? bootExternalRelayFamily(await readMobileRelayBaseUrl(process.env, projectRoot), await readRelayLocalUrl(process.env, projectRoot)) : key === "local-browser" ? bootLocalFamilyForEntry() : bootRelayFamily({
6402
6511
  verifyAuth: buildRelayVerifyAuth(),
6403
6512
  onWssUrl: (wssUrl) => {
6404
6513
  lockHandle.updateWssUrl(wssUrl);
@@ -6408,7 +6517,8 @@ async function runLocalDebugServer(options = {}) {
6408
6517
  }),
6409
6518
  diagnosticsCollector,
6410
6519
  devtoolsOpener,
6411
- onPageAttach: () => qrServer?.notifyStateChange()
6520
+ onPageAttach: () => qrServer?.notifyStateChange(),
6521
+ getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null
6412
6522
  });
6413
6523
  const aitSource = new RoutingAitSource(() => {
6414
6524
  return router.active;
@@ -6416,9 +6526,7 @@ async function runLocalDebugServer(options = {}) {
6416
6526
  let lastAttachParts = null;
6417
6527
  const getDashboardState = () => {
6418
6528
  const targets = router.active.listTargets();
6419
- const relayHttpUrl = router.activeRelayHttpUrl;
6420
- const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;
6421
- const inspectorUrl = relayHttpUrl && targets.length > 0 ? buildChiiInspectorUrl(relayHttpUrl, targets[0].id, totpSecret ? () => generateTotp(totpSecret, Date.now()) : void 0) : null;
6529
+ const inspectorUrl = qrServer?.inspectorStableUrl ?? null;
6422
6530
  return {
6423
6531
  tunnel: {
6424
6532
  up: router.relayTunnelStatus().up,
@@ -6432,9 +6540,35 @@ async function runLocalDebugServer(options = {}) {
6432
6540
  inspectorUrl
6433
6541
  };
6434
6542
  };
6543
+ const getDirectInspectorUrl = () => {
6544
+ const relayHttpUrl = router.activeRelayHttpUrl;
6545
+ if (!relayHttpUrl) return {
6546
+ ok: false,
6547
+ reason: "relayDown"
6548
+ };
6549
+ const targets = router.active.listTargets();
6550
+ if (targets.length === 0) return {
6551
+ ok: false,
6552
+ reason: "noTarget"
6553
+ };
6554
+ const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;
6555
+ if (!totpSecret) return {
6556
+ ok: false,
6557
+ reason: "totpUnavailable"
6558
+ };
6559
+ const url = buildChiiInspectorUrl(relayHttpUrl, targets[0].id, () => generateTotp(totpSecret, Date.now()));
6560
+ if (url === null) return {
6561
+ ok: false,
6562
+ reason: "totpUnavailable"
6563
+ };
6564
+ return {
6565
+ ok: true,
6566
+ url
6567
+ };
6568
+ };
6435
6569
  let qrServer;
6436
6570
  try {
6437
- qrServer = await startQrHttpServer(getDashboardState);
6571
+ qrServer = await startQrHttpServer(getDashboardState, { getDirectInspectorUrl });
6438
6572
  } catch (err) {
6439
6573
  logWarn("server.start", { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${err instanceof Error ? err.message : String(err)}` });
6440
6574
  }
@@ -6554,7 +6688,7 @@ async function runMobileDebugServer(options = {}) {
6554
6688
  const devtoolsOpener = new AutoDevtoolsOpener();
6555
6689
  const diagnosticsCollector = new InMemoryDiagnosticsCollector();
6556
6690
  const router = new DualConnectionRouter({
6557
- bootLazyFor: async (key) => key === "relay-sandbox" ? bootExternalRelayFamily(relayBaseUrl) : key === "local-browser" ? bootLocalFamily() : bootRelayFamily({
6691
+ bootLazyFor: async (key) => key === "relay-sandbox" ? bootExternalRelayFamily(relayBaseUrl, await readRelayLocalUrl(process.env, options.projectRoot ?? process.cwd())) : key === "local-browser" ? bootLocalFamily() : bootRelayFamily({
6558
6692
  verifyAuth: buildRelayVerifyAuth(),
6559
6693
  onWssUrl: (wssUrl) => {
6560
6694
  lockHandle.updateWssUrl(wssUrl);
@@ -6564,7 +6698,8 @@ async function runMobileDebugServer(options = {}) {
6564
6698
  }),
6565
6699
  diagnosticsCollector,
6566
6700
  devtoolsOpener,
6567
- onPageAttach: () => qrServer?.notifyStateChange()
6701
+ onPageAttach: () => qrServer?.notifyStateChange(),
6702
+ getInspectorStableUrl: () => qrServer?.inspectorStableUrl ?? null
6568
6703
  });
6569
6704
  const aitSource = new RoutingAitSource(() => {
6570
6705
  return router.active;
@@ -6572,9 +6707,7 @@ async function runMobileDebugServer(options = {}) {
6572
6707
  let lastAttachParts = null;
6573
6708
  const getDashboardState = () => {
6574
6709
  const targets = router.active.listTargets();
6575
- const relayHttpUrl = router.activeRelayHttpUrl;
6576
- const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;
6577
- const inspectorUrl = relayHttpUrl && targets.length > 0 ? buildChiiInspectorUrl(relayHttpUrl, targets[0].id, totpSecret ? () => generateTotp(totpSecret, Date.now()) : void 0) : null;
6710
+ const inspectorUrl = qrServer?.inspectorStableUrl ?? null;
6578
6711
  return {
6579
6712
  tunnel: {
6580
6713
  up: router.relayTunnelStatus().up,
@@ -6588,9 +6721,35 @@ async function runMobileDebugServer(options = {}) {
6588
6721
  inspectorUrl
6589
6722
  };
6590
6723
  };
6724
+ const getDirectInspectorUrl = () => {
6725
+ const relayHttpUrl = router.activeRelayHttpUrl;
6726
+ if (!relayHttpUrl) return {
6727
+ ok: false,
6728
+ reason: "relayDown"
6729
+ };
6730
+ const targets = router.active.listTargets();
6731
+ if (targets.length === 0) return {
6732
+ ok: false,
6733
+ reason: "noTarget"
6734
+ };
6735
+ const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;
6736
+ if (!totpSecret) return {
6737
+ ok: false,
6738
+ reason: "totpUnavailable"
6739
+ };
6740
+ const url = buildChiiInspectorUrl(relayHttpUrl, targets[0].id, () => generateTotp(totpSecret, Date.now()));
6741
+ if (url === null) return {
6742
+ ok: false,
6743
+ reason: "totpUnavailable"
6744
+ };
6745
+ return {
6746
+ ok: true,
6747
+ url
6748
+ };
6749
+ };
6591
6750
  let qrServer;
6592
6751
  try {
6593
- qrServer = await startQrHttpServer(getDashboardState);
6752
+ qrServer = await startQrHttpServer(getDashboardState, { getDirectInspectorUrl });
6594
6753
  } catch (err) {
6595
6754
  logWarn("server.start", { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${err instanceof Error ? err.message : String(err)}` });
6596
6755
  }
@@ -7105,7 +7264,7 @@ function createDevServer(deps = {}) {
7105
7264
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
7106
7265
  const server = new Server({
7107
7266
  name: "ait-devtools",
7108
- version: "0.1.79"
7267
+ version: "0.1.80"
7109
7268
  }, { capabilities: { tools: {} } });
7110
7269
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
7111
7270
  server.setRequestHandler(CallToolRequestSchema, async (request) => {