@ait-co/devtools 0.1.81 → 0.1.85

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 (76) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/{chii-relay-DSVG4Ui1.js → chii-relay-BASitNMw.js} +1 -1
  4. package/dist/{chii-relay-DSVG4Ui1.js.map → chii-relay-BASitNMw.js.map} +1 -1
  5. package/dist/{chii-relay-BcnVJBqm.cjs → chii-relay-BzUf0LH3.cjs} +1 -1
  6. package/dist/{chii-relay-BcnVJBqm.cjs.map → chii-relay-BzUf0LH3.cjs.map} +1 -1
  7. package/dist/{deeplink-B-94XmWA.js → deeplink-BpO9qc-D.js} +5 -3
  8. package/dist/{deeplink-BLU2_hg6.cjs.map → deeplink-BpO9qc-D.js.map} +1 -1
  9. package/dist/{deeplink-CYqDwVYs.js → deeplink-D1HXJ2YG.js} +5 -3
  10. package/dist/{deeplink-CU6opogq.cjs.map → deeplink-D1HXJ2YG.js.map} +1 -1
  11. package/dist/{deeplink-BLU2_hg6.cjs → deeplink-DCScMYcp.cjs} +5 -3
  12. package/dist/deeplink-DCScMYcp.cjs.map +1 -0
  13. package/dist/{deeplink-CU6opogq.cjs → deeplink-DDOe0FQl.cjs} +5 -3
  14. package/dist/deeplink-DDOe0FQl.cjs.map +1 -0
  15. package/dist/{devtools-opener-Bp671YXu.cjs → devtools-opener-BDY0w3_0.cjs} +11 -5
  16. package/dist/devtools-opener-BDY0w3_0.cjs.map +1 -0
  17. package/dist/{devtools-opener-D84kZFtR.js → devtools-opener-BTl5A6Cd.js} +11 -5
  18. package/dist/devtools-opener-BTl5A6Cd.js.map +1 -0
  19. package/dist/{devtools-opener-BbUXBzgA.js → devtools-opener-XpwL3fZ9.js} +22 -6
  20. package/dist/devtools-opener-XpwL3fZ9.js.map +1 -0
  21. package/dist/{devtools-opener-h6A-UjzC.cjs → devtools-opener-mDgeg_MX.cjs} +11 -5
  22. package/dist/devtools-opener-mDgeg_MX.cjs.map +1 -0
  23. package/dist/machine-state-Chg_6SPq.js +188 -0
  24. package/dist/machine-state-Chg_6SPq.js.map +1 -0
  25. package/dist/machine-state-DOUweFsJ.cjs +216 -0
  26. package/dist/machine-state-DOUweFsJ.cjs.map +1 -0
  27. package/dist/mcp/cli.js +112 -54
  28. package/dist/mcp/cli.js.map +1 -1
  29. package/dist/mcp/server.d.ts.map +1 -1
  30. package/dist/mcp/server.js +6 -10
  31. package/dist/mcp/server.js.map +1 -1
  32. package/dist/panel/index.js +109 -8
  33. package/dist/panel/index.js.map +1 -1
  34. package/dist/{qr-http-server-DJ5K3Odk.js → qr-http-server-Buorblrx.js} +73 -23
  35. package/dist/qr-http-server-Buorblrx.js.map +1 -0
  36. package/dist/{qr-http-server-Dkx2-pKF.cjs → qr-http-server-BvAtX9Lc.cjs} +73 -23
  37. package/dist/qr-http-server-BvAtX9Lc.cjs.map +1 -0
  38. package/dist/{qr-http-server-DkOFfZsR.js → qr-http-server-BxjrJr9t.js} +73 -23
  39. package/dist/qr-http-server-BxjrJr9t.js.map +1 -0
  40. package/dist/{qr-http-server-MIUHaiYw.cjs → qr-http-server-CAUyOrCm.cjs} +73 -23
  41. package/dist/qr-http-server-CAUyOrCm.cjs.map +1 -0
  42. package/dist/{relay-secret-store-CsCOfpWt.cjs → relay-secret-store-B5WAozDv.cjs} +2 -2
  43. package/dist/{relay-secret-store-CsCOfpWt.cjs.map → relay-secret-store-B5WAozDv.cjs.map} +1 -1
  44. package/dist/{relay-secret-store-6pPzLkUO.js → relay-secret-store-BvNWdSjV.js} +2 -2
  45. package/dist/{relay-secret-store-6pPzLkUO.js.map → relay-secret-store-BvNWdSjV.js.map} +1 -1
  46. package/dist/{relay-url-store-DH8-VUFc.js → relay-url-store-1CXVqNDL.js} +2 -2
  47. package/dist/{relay-url-store-DH8-VUFc.js.map → relay-url-store-1CXVqNDL.js.map} +1 -1
  48. package/dist/{relay-url-store-BiEK9BN1.cjs → relay-url-store-D2lX9POP.cjs} +2 -2
  49. package/dist/{relay-url-store-BiEK9BN1.cjs.map → relay-url-store-D2lX9POP.cjs.map} +1 -1
  50. package/dist/{totp-DYdP9N3o.js → totp-CauHjkdE.js} +1 -1
  51. package/dist/{totp-DYdP9N3o.js.map → totp-CauHjkdE.js.map} +1 -1
  52. package/dist/{totp-CNw0w89F.cjs → totp-D9fjaVak.cjs} +1 -1
  53. package/dist/{totp-CNw0w89F.cjs.map → totp-D9fjaVak.cjs.map} +1 -1
  54. package/dist/{tunnel-C-AFdAVL.cjs → tunnel-CfT31xho.cjs} +6 -6
  55. package/dist/{tunnel-C-AFdAVL.cjs.map → tunnel-CfT31xho.cjs.map} +1 -1
  56. package/dist/{tunnel-BTlq1mmH.js → tunnel-DPwJBn1u.js} +6 -6
  57. package/dist/{tunnel-BTlq1mmH.js.map → tunnel-DPwJBn1u.js.map} +1 -1
  58. package/dist/unplugin/index.cjs +90 -5
  59. package/dist/unplugin/index.cjs.map +1 -1
  60. package/dist/unplugin/index.d.cts.map +1 -1
  61. package/dist/unplugin/index.d.ts.map +1 -1
  62. package/dist/unplugin/index.js +90 -5
  63. package/dist/unplugin/index.js.map +1 -1
  64. package/dist/unplugin/tunnel.cjs +4 -4
  65. package/dist/unplugin/tunnel.js +4 -4
  66. package/package.json +1 -1
  67. package/dist/deeplink-B-94XmWA.js.map +0 -1
  68. package/dist/deeplink-CYqDwVYs.js.map +0 -1
  69. package/dist/devtools-opener-BbUXBzgA.js.map +0 -1
  70. package/dist/devtools-opener-Bp671YXu.cjs.map +0 -1
  71. package/dist/devtools-opener-D84kZFtR.js.map +0 -1
  72. package/dist/devtools-opener-h6A-UjzC.cjs.map +0 -1
  73. package/dist/qr-http-server-DJ5K3Odk.js.map +0 -1
  74. package/dist/qr-http-server-DkOFfZsR.js.map +0 -1
  75. package/dist/qr-http-server-Dkx2-pKF.cjs.map +0 -1
  76. package/dist/qr-http-server-MIUHaiYw.cjs.map +0 -1
package/dist/mcp/cli.js CHANGED
@@ -1031,9 +1031,10 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
1031
1031
  * @param totpCode - Optional current TOTP code (6 digits). When provided, it
1032
1032
  * is appended as `at=<totpCode>`. Must be computed at call time — it rotates
1033
1033
  * every 30 s. Omit when TOTP is disabled.
1034
- * @param opts - Optional app identity hints: `name` and `icon` (#498).
1034
+ * @param opts - Optional app identity hints: `name`, `icon`, and `selfdebug`
1035
+ * (#498, #543).
1035
1036
  * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>
1036
- * [&at=<code>][&name=<enc>][&icon=<enc>]` params.
1037
+ * [&at=<code>][&name=<enc>][&icon=<enc>][&selfdebug=1]` params.
1037
1038
  */
1038
1039
  function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode, opts) {
1039
1040
  let url = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}&debug=1&relay=${encodeURIComponent(wssUrl)}`;
@@ -1048,6 +1049,7 @@ function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode, opts) {
1048
1049
  }
1049
1050
  if (iconParsed?.protocol === "https:") url += `&icon=${encodeURIComponent(opts.icon)}`;
1050
1051
  }
1052
+ if (opts?.selfdebug === true) url += "&selfdebug=1";
1051
1053
  return url;
1052
1054
  }
1053
1055
  /**
@@ -1233,12 +1235,18 @@ function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "co
1233
1235
  return `${relayHttpBaseUrl.replace(/\/$/, "")}/front_end/chii_app.html?${params.toString()}`;
1234
1236
  }
1235
1237
  /**
1236
- * Returns `true` when auto-open is **disabled** via the `AIT_AUTO_DEVTOOLS`
1237
- * env var. Only the explicit `"0"` value disables it; anything else (including
1238
- * absent) leaves auto-open enabled.
1238
+ * Returns `true` when auto-open is **disabled**.
1239
+ *
1240
+ * Default (env var absent or any value other than `"1"`) is **disabled**
1241
+ * the developer uses the "디버그 툴 열기" button on the /attach or dashboard
1242
+ * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic
1243
+ * browser-open behaviour on device attach.
1244
+ *
1245
+ * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward
1246
+ * compatibility (same effect as absent).
1239
1247
  */
1240
1248
  function isAutoDevtoolsDisabled() {
1241
- return process.env.AIT_AUTO_DEVTOOLS === "0";
1249
+ return process.env.AIT_AUTO_DEVTOOLS !== "1";
1242
1250
  }
1243
1251
  /**
1244
1252
  * Opens the given URL in the OS default browser using a platform-appropriate
@@ -1341,8 +1349,8 @@ var AutoDevtoolsOpener = class {
1341
1349
  if (options.inspectorStableUrl) {
1342
1350
  this._openedTargets.add(targetId);
1343
1351
  const stableUrl = options.inspectorStableUrl;
1344
- process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chii DevTools를 자동으로 엽니다.
1345
- [ait-debug] 인스펙터 URL: ${stableUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 있습니다)
1352
+ process.stderr.write(`[ait-debug] 기기가 연결됐습니다.
1353
+ [ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace("/inspector", "")})의 "디버그 툴 열기" 버튼을 눌러 DevTools를 여세요.\n[ait-debug] (AIT_AUTO_DEVTOOLS=1 설정하면 연결 자동으로 열립니다)
1346
1354
  `);
1347
1355
  if (!openUrlInBrowser(stableUrl)) process.stderr.write(`[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\n`);
1348
1356
  return;
@@ -1354,8 +1362,8 @@ var AutoDevtoolsOpener = class {
1354
1362
  process.stderr.write("[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\n[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\n");
1355
1363
  return;
1356
1364
  }
1357
- process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chii DevTools를 자동으로 엽니다.
1358
- [ait-debug] DevTools URL: ${inspectorUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 있습니다)
1365
+ process.stderr.write(`[ait-debug] 기기가 연결됐습니다.
1366
+ [ait-debug] DevTools URL: ${inspectorUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=1 설정하면 연결 자동으로 열립니다)
1359
1367
  [ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.
1360
1368
  `);
1361
1369
  if (!openUrlInBrowser(inspectorUrl)) process.stderr.write("[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\n");
@@ -2134,8 +2142,8 @@ const en = {
2134
2142
  "dashboard.url.copy": "Copy",
2135
2143
  "dashboard.url.copied": "Copied",
2136
2144
  "dashboard.inspector.section": "Inspector",
2137
- "dashboard.inspector.open": "Open inspector",
2138
- "dashboard.inspector.waiting": "Inspector URL pending appears after a page attaches",
2145
+ "dashboard.inspector.open": "Open DevTools",
2146
+ "dashboard.inspector.waiting": "Attach a page to enable the \"Open DevTools\" button",
2139
2147
  "inspector.error.noTarget": "No page attached. Attach a device and try again.",
2140
2148
  "inspector.error.relayDown": "Relay is not active. Start a relay session first.",
2141
2149
  "attach.title": "AIT Debug Session — QR Scan",
@@ -2388,8 +2396,8 @@ const tables = {
2388
2396
  "dashboard.url.copy": "복사",
2389
2397
  "dashboard.url.copied": "복사됨",
2390
2398
  "dashboard.inspector.section": "인스펙터",
2391
- "dashboard.inspector.open": "인스펙터 열기",
2392
- "dashboard.inspector.waiting": "인스펙터 URL 대기 (페이지 attach 표시됩니다)",
2399
+ "dashboard.inspector.open": "디버그 열기",
2400
+ "dashboard.inspector.waiting": "페이지를 attach하면 \"디버그 열기\" 버튼이 표시됩니다",
2393
2401
  "inspector.error.noTarget": "연결된 페이지가 없습니다. 기기를 attach한 후 다시 시도하세요.",
2394
2402
  "inspector.error.relayDown": "relay가 활성화되지 않았습니다. start_debug로 relay를 기동하세요.",
2395
2403
  "attach.title": "AIT 디버그 세션 — QR 스캔",
@@ -2595,7 +2603,15 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
2595
2603
  .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2596
2604
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2597
2605
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2598
- </style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).</li><li>launcher 안의 <strong>"QR 카메라로 스캔"</strong>으로 이 QR 코드를 스캔하세요.</li><li>미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요</li><li><strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요</li><li><strong>QR이 만료된 경우 (TOTP — 코드 1개는 30초 창, 만료 후 ~3분(±6 step) 이내 소급 허용)</strong> — 새 QR을 다시 스캔하세요</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section></body></html>`;
2606
+ .inspector-link {
2607
+ display: inline-block; margin-top: 0.5rem;
2608
+ padding: 0.45rem 1rem; border-radius: 6px;
2609
+ background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;
2610
+ text-decoration: none; text-align: center;
2611
+ }
2612
+ .inspector-link:hover { background: #388bfd; }
2613
+ .inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
2614
+ </style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).</li><li>launcher 안의 <strong>"QR 카메라로 스캔"</strong>으로 이 QR 코드를 스캔하세요.</li><li>미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요</li><li><strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요</li><li><strong>QR이 만료된 경우 (TOTP — 코드 1개는 30초 창, 만료 후 ~3분(±6 step) 이내 소급 허용)</strong> — 새 QR을 다시 스캔하세요</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section><hr/><section id="inspector-section"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section></body></html>`;
2599
2615
  const attachChromeHtmlKoIntoss = `<!DOCTYPE html>
2600
2616
  <html lang="ko"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="__QR_DATA_URL__"/><title>AIT 디버그 세션 — QR 스캔</title><style>
2601
2617
  *, *::before, *::after { box-sizing: border-box; }
@@ -2645,7 +2661,15 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
2645
2661
  .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2646
2662
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2647
2663
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2648
- </style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>토스 앱을 실행하세요.</li><li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li><li>팝업이 뜨면 <strong>"토스로 열기"</strong>를 탭하세요.</li><li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li><li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link <code>_deploymentId</code> 파라미터가 있는지 확인</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li><li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li>__LIVE_FAQ__</ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section></body></html>`;
2664
+ .inspector-link {
2665
+ display: inline-block; margin-top: 0.5rem;
2666
+ padding: 0.45rem 1rem; border-radius: 6px;
2667
+ background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;
2668
+ text-decoration: none; text-align: center;
2669
+ }
2670
+ .inspector-link:hover { background: #388bfd; }
2671
+ .inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
2672
+ </style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>토스 앱을 실행하세요.</li><li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li><li>팝업이 뜨면 <strong>"토스로 열기"</strong>를 탭하세요.</li><li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li><li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li><li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li>__LIVE_FAQ__</ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section><hr/><section id="inspector-section"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section></body></html>`;
2649
2673
  const dashboardChromeHtmlEn = `<!DOCTYPE html>
2650
2674
  <html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>AIT Debug Dashboard</title><style>
2651
2675
  *, *::before, *::after { box-sizing: border-box; }
@@ -2755,7 +2779,15 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
2755
2779
  .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2756
2780
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2757
2781
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2758
- </style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).</li><li>Scan this QR code with <strong>"Scan QR with camera"</strong> inside the launcher.</li><li>The mini-app opens fullscreen and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen</li><li><strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner</li><li><strong>QR expired (TOTP — 30-second step, ±6 steps (~3 min) accepted)</strong> — scan a fresh QR code</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section></body></html>`;
2782
+ .inspector-link {
2783
+ display: inline-block; margin-top: 0.5rem;
2784
+ padding: 0.45rem 1rem; border-radius: 6px;
2785
+ background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;
2786
+ text-decoration: none; text-align: center;
2787
+ }
2788
+ .inspector-link:hover { background: #388bfd; }
2789
+ .inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
2790
+ </style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).</li><li>Scan this QR code with <strong>"Scan QR with camera"</strong> inside the launcher.</li><li>The mini-app opens fullscreen and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen</li><li><strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner</li><li><strong>QR expired (TOTP — 30-second step, ±6 steps (~3 min) accepted)</strong> — scan a fresh QR code</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section><hr/><section id="inspector-section"><h2>Inspector</h2>__INSPECTOR_SECTION__</section></body></html>`;
2759
2791
  const attachChromeHtmlEnIntoss = `<!DOCTYPE html>
2760
2792
  <html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="__QR_DATA_URL__"/><title>AIT Debug Session — QR Scan</title><style>
2761
2793
  *, *::before, *::after { box-sizing: border-box; }
@@ -2805,7 +2837,15 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
2805
2837
  .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2806
2838
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2807
2839
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2808
- </style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Open the Toss app.</li><li>Scan the QR code with your phone camera app.</li><li>Tap <strong>"Open in Toss"</strong> when the popup appears.</li><li>The mini-app opens and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)</li><li><strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li><li><strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server</li>__LIVE_FAQ__</ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section></body></html>`;
2840
+ .inspector-link {
2841
+ display: inline-block; margin-top: 0.5rem;
2842
+ padding: 0.45rem 1rem; border-radius: 6px;
2843
+ background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;
2844
+ text-decoration: none; text-align: center;
2845
+ }
2846
+ .inspector-link:hover { background: #388bfd; }
2847
+ .inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
2848
+ </style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Open the Toss app.</li><li>Scan the QR code with your phone camera app.</li><li>Tap <strong>"Open in Toss"</strong> when the popup appears.</li><li>The mini-app opens and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)</li><li><strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li><li><strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server</li>__LIVE_FAQ__</ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section><hr/><section id="inspector-section"><h2>Inspector</h2>__INSPECTOR_SECTION__</section></body></html>`;
2809
2849
  /** Map from Locale to the precompiled dashboard chrome string. */
2810
2850
  const dashboardChromeByLocale = {
2811
2851
  ko: dashboardChromeHtmlKo,
@@ -2905,8 +2945,9 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
2905
2945
  const copyLabel = escapeHtml(s("dashboard.url.copy"));
2906
2946
  attachSection = `<img class="qr" src="${qrDataUrl}" alt="attach QR" /><div class="url-row"><p class="url-box" id="url-box">${safeAttachUrl}</p><button class="copy-btn" id="copy-btn" type="button" aria-label="${copyLabel}">${copyLabel}</button></div>`;
2907
2947
  } else attachSection = `<p class="hint">${escapeHtml(s("dashboard.attach.hint"))}</p>`;
2948
+ const pagesAttached = Array.isArray(state.pages) && state.pages.length > 0;
2908
2949
  let inspectorSection;
2909
- if (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>`;
2950
+ 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>`;
2910
2951
  else inspectorSection = `<span class="inspector-hint" id="inspector-link">${escapeHtml(s("dashboard.inspector.waiting"))}</span>`;
2911
2952
  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) => {
2912
2953
  return `<li><span class="page-id">${escapeHtml(p.id)}</span> <span class="page-url">${escapeHtml(p.url.slice(0, 120))}</span></li>`;
@@ -3076,11 +3117,15 @@ function buildSseScript(strings) {
3076
3117
  sec.innerHTML = '<p class=\\"hint\\">' + ATTACH_HINT + '</p>';`}
3077
3118
  }
3078
3119
  }
3079
- // 인스펙터 링크 갱신 — #inspector-link (#503).
3120
+ // 인스펙터 링크 갱신 — #inspector-link (#503, gate 보정 #544).
3121
+ // 게이트: pages.length > 0 (페이지 attach 여부) — inspectorUrl 존재 여부가 아님.
3122
+ // #530 이후 inspectorUrl은 항상 안정 URL이므로 null 게이트는 사실상 항상 활성이었다.
3123
+ // pages.length > 0 으로 바꿔 미attach 시 대기 힌트를 보여주도록 수정.
3080
3124
  // SECRET-HANDLING: inspectorUrl을 console.log 등으로 출력하지 않는다.
3081
3125
  var insp = document.getElementById('inspector-link');
3082
3126
  if (insp) {
3083
- if (s.inspectorUrl) {
3127
+ var pagesAttachedSse = Array.isArray(s.pages) && s.pages.length > 0;
3128
+ if (pagesAttachedSse && s.inspectorUrl) {
3084
3129
  var safeInspUrl = String(s.inspectorUrl).slice(0, 2000).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
3085
3130
  insp.outerHTML = '<a class=\\"inspector-link\\" id=\\"inspector-link\\" href=\\"' + safeInspUrl + '\\" target=\\"_blank\\" rel=\\"noopener noreferrer\\">' + INSPECTOR_OPEN_LABEL + '</a>';
3086
3131
  } else {
@@ -3102,28 +3147,33 @@ function buildSseScript(strings) {
3102
3147
  * Attach 페이지 HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.
3103
3148
  *
3104
3149
  * 동적 파트:
3105
- * - __QR_DATA_URL__ : base64 data URL (QR 이미지)
3106
- * - __SAFE_LABEL__ : HTML-escaped deploymentId label (intoss family에만 존재)
3107
- * - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)
3108
- * - __MODE_LABEL__ : 환경 배지 (`<p class="mode-label">…</p>` 또는 빈 문자열, #468)
3109
- * - __LIVE_FAQ__ : 환경 4 LIVE read-only `<li>` 또는 빈 문자열 (intoss family에만 존재)
3150
+ * - __QR_DATA_URL__ : base64 data URL (QR 이미지)
3151
+ * - __SAFE_LABEL__ : HTML-escaped deploymentId label (intoss family에만 존재)
3152
+ * - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)
3153
+ * - __MODE_LABEL__ : 환경 배지 (`<p class="mode-label">…</p>` 또는 빈 문자열, #468)
3154
+ * - __LIVE_FAQ__ : 환경 4 LIVE read-only `<li>` 또는 빈 문자열 (intoss family에만 존재)
3155
+ * - __INSPECTOR_SECTION__ : "디버그 툴 열기" 버튼 또는 대기 힌트 (#544)
3110
3156
  *
3111
3157
  * mode-aware 분기 (#468): mode가 `relay-mobile`이면 sandbox family chrome(launcher
3112
3158
  * PWA 절차), 그 외는 intoss family chrome(토스 앱 절차)을 선택한다. `relay-live`는
3113
3159
  * intoss chrome에 LIVE read-only 라인을 추가한다.
3114
3160
  *
3115
3161
  * SSE 스크립트도 주입 — `#attach-section` hook이 있으면 `/events` push 때 QR이
3116
- * `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#tunnel-status`·`#pages-list`
3117
- * 나머지 selector는 /attach 페이지에 없으므로 null-guard로 no-op.
3162
+ * `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#inspector-link`도 SSE push로
3163
+ * pages.length > 0 게이트에 따라 활성/비활성 전환된다 (#544).
3118
3164
  *
3119
3165
  * SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
3166
+ * inspectorStableUrl은 /inspector 안정 URL (127.0.0.1, 시크릿 없음) — 노출 가능.
3120
3167
  */
3121
- function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams(), mode) {
3168
+ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams(), mode, pagesAttached = false, inspectorStableUrl = null) {
3122
3169
  const s = resolveLocaleStrings(locale);
3123
3170
  const langSwitcher = buildLangSwitcher(path, params, locale, s);
3124
3171
  const family = attachFamilyForMode(mode);
3125
3172
  const liveFaq = mode === "relay-live" ? `<li>${s("attach.intoss.faq.liveReadOnly")}</li>` : "";
3126
- const filled = attachChromeByLocale[locale][family].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__MODE_LABEL__", buildModeLabel(mode, s)).replaceAll("__LIVE_FAQ__", liveFaq).replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl);
3173
+ let inspectorSection;
3174
+ if (pagesAttached && inspectorStableUrl) inspectorSection = `<a class="inspector-link" id="inspector-link" href="${escapeHtml(inspectorStableUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(s("dashboard.inspector.open"))}</a>`;
3175
+ else inspectorSection = `<span class="inspector-hint" id="inspector-link">${escapeHtml(s("dashboard.inspector.waiting"))}</span>`;
3176
+ const filled = attachChromeByLocale[locale][family].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__MODE_LABEL__", buildModeLabel(mode, s)).replaceAll("__LIVE_FAQ__", liveFaq).replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl).replaceAll("__INSPECTOR_SECTION__", inspectorSection);
3127
3177
  const sseScript = buildSseScript({
3128
3178
  tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
3129
3179
  tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
@@ -3225,12 +3275,20 @@ async function startQrHttpServer(getDashboardState, options) {
3225
3275
  const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);
3226
3276
  if (dpMatch?.[1]) deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);
3227
3277
  } catch {}
3228
- const mode = getDashboardState?.().mode;
3278
+ const currentState = getDashboardState?.();
3279
+ const mode = currentState?.mode;
3280
+ const pagesAttached = Array.isArray(currentState?.pages) && (currentState?.pages.length ?? 0) > 0;
3281
+ const inspectorStableUrlForAttach = (() => {
3282
+ if (!options?.getDirectInspectorUrl) return null;
3283
+ const addr = server.address();
3284
+ if (!addr || typeof addr === "string") return null;
3285
+ return `http://127.0.0.1:${addr.port}/inspector`;
3286
+ })();
3229
3287
  QRCode.toDataURL(attachUrl, {
3230
3288
  type: "image/png",
3231
3289
  errorCorrectionLevel: "M"
3232
3290
  }).then((dataUrl) => {
3233
- const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params, mode);
3291
+ const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params, mode, pagesAttached, inspectorStableUrlForAttach);
3234
3292
  res.writeHead(200, {
3235
3293
  "Content-Type": "text/html; charset=utf-8",
3236
3294
  "Cache-Control": "no-store"
@@ -3703,7 +3761,7 @@ const DEBUG_TOOL_DEFINITIONS = [
3703
3761
  },
3704
3762
  {
3705
3763
  name: "build_attach_url",
3706
- description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep-link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep-link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). When projectRoot is given, the app name from <projectRoot>/package.json is automatically added as name= so the launcher partner bar shows it. Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (polls up to 30 s). On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). \n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>). The code is valid for ~3 minutes (the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.",
3764
+ description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep-link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep-link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). When projectRoot is given, the app name from <projectRoot>/package.json is automatically added as name= so the launcher partner bar shows it. Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (polls up to 30 s). On timeout, call build_attach_url again to resume polling. The server automatically opens the QR dashboard in the OS default browser when running on a local GUI machine headless/remote environments fall back to the text QR in the tool output.\n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>). The code is valid for ~3 minutes (the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.\n\nselfdebug (env 2 / relay-sandbox only): pass selfdebug=true to add &selfdebug=1 to the launcher deep-link. The launcher PWA then registers its own document as the CDP target instead of the framed mini-app. SINGLE-ATTACH MODEL: attaching the launcher self-target evicts any currently-attached mini-app target — use this mode exclusively for diagnosing the launcher document itself (DOM, safe-area, console). Not applicable in env 3/4 (relay-staging/relay-live) — passing selfdebug=true there returns an error.",
3707
3765
  inputSchema: {
3708
3766
  type: "object",
3709
3767
  properties: {
@@ -3715,13 +3773,13 @@ const DEBUG_TOOL_DEFINITIONS = [
3715
3773
  type: "boolean",
3716
3774
  description: "If true, block after returning the QR until a page attaches to the relay (polls listTargets ~1 s interval, timeout 30 s). On attach, the response includes the attached page list. On timeout, call build_attach_url again to resume polling."
3717
3775
  },
3718
- open_in_browser: {
3719
- type: "boolean",
3720
- description: "If true (default), render the QR as a PNG and open it in the OS default browser. Only works when the MCP server is running on a local GUI machine — headless or remote container environments should set this to false to use the text QR fallback."
3721
- },
3722
3776
  projectRoot: {
3723
3777
  type: "string",
3724
3778
  description: "Absolute path to the mini-app project root (the directory containing its package.json and .ait_urls). When AIT_TUNNEL_BASE_URL is unset (env 2 / relay-mobile only), the daemon reads the app tunnel URL from <projectRoot>/.ait_urls written by the dev server (tunnel:{cdp:true}). Pass this because the daemon's own cwd is fixed at launch. Omit when AIT_TUNNEL_BASE_URL is set explicitly."
3779
+ },
3780
+ selfdebug: {
3781
+ type: "boolean",
3782
+ description: "Env 2 / relay-sandbox only. When true, adds &selfdebug=1 to the launcher deep-link so the launcher PWA registers its own document as the CDP target (launcher diagnostics mode). SINGLE-ATTACH MODEL: self-target attach evicts any currently-attached mini-app target. Use only when you need to inspect the launcher itself (DOM, safe-area, console). Passing selfdebug=true in env 3/4 (relay-staging/relay-live) returns an error. Default: false (omitted — output is byte-identical to previous behaviour)."
3725
3783
  }
3726
3784
  },
3727
3785
  required: []
@@ -4717,7 +4775,7 @@ async function readMcpSdkVersion() {
4717
4775
  * some test environments that skip the build step).
4718
4776
  */
4719
4777
  function readDevtoolsVersion() {
4720
- return "0.1.81";
4778
+ return "0.1.85";
4721
4779
  }
4722
4780
  /**
4723
4781
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -5221,7 +5279,7 @@ function createDebugServer(deps) {
5221
5279
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
5222
5280
  const server = new Server({
5223
5281
  name: "ait-debug",
5224
- version: "0.1.81"
5282
+ version: "0.1.85"
5225
5283
  }, { capabilities: { tools: { listChanged: true } } });
5226
5284
  server.setRequestHandler(ListToolsRequestSchema, () => {
5227
5285
  const conn = router.active;
@@ -5294,7 +5352,8 @@ function createDebugServer(deps) {
5294
5352
  }
5295
5353
  if (name === "build_attach_url") {
5296
5354
  const waitForAttach = request.params.arguments?.wait_for_attach === true;
5297
- const openInBrowser = request.params.arguments?.open_in_browser !== false;
5355
+ const selfdebug = request.params.arguments?.selfdebug === true;
5356
+ if (selfdebug && env !== "relay-mobile") return mcpError("build_attach_url: selfdebug=true는 env 2 / relay-sandbox 전용 기능입니다. 현재 환경(env 3/4)에서는 launcher가 없어 self-target 모드를 지원하지 않습니다. launcher self-target이 필요하다면 relay-sandbox 모드로 재시작하세요.");
5298
5357
  if (env === "relay-mobile") {
5299
5358
  const rawBuildProjectRoot = request.params.arguments?.projectRoot;
5300
5359
  const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
@@ -5329,7 +5388,10 @@ function createDebugServer(deps) {
5329
5388
  const rawName = typeof pkg.name === "string" ? pkg.name : "";
5330
5389
  launcherAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
5331
5390
  } catch {}
5332
- const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode, { name: launcherAppName });
5391
+ const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode, {
5392
+ name: launcherAppName,
5393
+ ...selfdebug ? { selfdebug: true } : {}
5394
+ });
5333
5395
  onAttachUrlBuilt?.({
5334
5396
  kind: "launcher",
5335
5397
  tunnelHttpUrl,
@@ -5347,8 +5409,8 @@ function createDebugServer(deps) {
5347
5409
  const header = "This tool result is shown to the user directly — do NOT re-print the QR below in your reply (it wastes output tokens). Just tell the user to scan the QR in this output (Ctrl+O to expand if collapsed).";
5348
5410
  const warningPrefix = "";
5349
5411
  const guiAvailable = canOpenBrowser();
5350
- if (openInBrowser && !guiAvailable) {
5351
- const headlessNote = "[open_in_browser] GUI 환경이 감지되지 않았습니다 (headless/remote 환경). open_in_browser=false로 자동 폴백합니다. 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
5412
+ if (!guiAvailable) {
5413
+ const headlessNote = "GUI 환경이 감지되지 않았습니다 (headless/remote 환경). 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
5352
5414
  const qrHeadless = await renderQr(attachUrl);
5353
5415
  const headlessText = `${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
5354
5416
  attachUrl,
@@ -5378,7 +5440,7 @@ function createDebugServer(deps) {
5378
5440
  text: `${headlessText}\n\n${JSON.stringify(pagesResultHl, null, 2)}`
5379
5441
  }] };
5380
5442
  }
5381
- if (openInBrowser && guiAvailable && qrHttpServer) {
5443
+ if (guiAvailable && qrHttpServer) {
5382
5444
  const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
5383
5445
  if (browserResult.opened) {
5384
5446
  const retriedNote = browserResult.retried ? " (1회 retry 후 성공)" : "";
@@ -5423,7 +5485,7 @@ function createDebugServer(deps) {
5423
5485
  ...browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}
5424
5486
  };
5425
5487
  const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
5426
- const fallbackNote = `[open_in_browser] 브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
5488
+ const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
5427
5489
  const qr = await renderQr(attachUrl);
5428
5490
  const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
5429
5491
  attachUrl,
@@ -5518,8 +5580,8 @@ function createDebugServer(deps) {
5518
5580
  const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
5519
5581
  const header = "This tool result is shown to the user directly — do NOT re-print the QR below in your reply (it wastes output tokens). Just tell the user to scan the QR in this output (Ctrl+O to expand if collapsed).";
5520
5582
  const guiAvailable = canOpenBrowser();
5521
- if (openInBrowser && !guiAvailable) {
5522
- const headlessNote = "[open_in_browser] GUI 환경이 감지되지 않았습니다 (headless/remote 환경). open_in_browser=false로 자동 폴백합니다. 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
5583
+ if (!guiAvailable) {
5584
+ const headlessNote = "GUI 환경이 감지되지 않았습니다 (headless/remote 환경). 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
5523
5585
  const qrHeadless = await renderQr(attachUrl);
5524
5586
  const headlessText = `${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
5525
5587
  attachUrl,
@@ -5549,7 +5611,7 @@ function createDebugServer(deps) {
5549
5611
  text: `${headlessText}\n\n${JSON.stringify(pagesResultHl, null, 2)}`
5550
5612
  }] };
5551
5613
  }
5552
- if (openInBrowser && guiAvailable && qrHttpServer) {
5614
+ if (guiAvailable && qrHttpServer) {
5553
5615
  const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
5554
5616
  if (browserResult.opened) {
5555
5617
  const retriedNote = browserResult.retried ? " (1회 retry 후 성공)" : "";
@@ -5594,7 +5656,7 @@ function createDebugServer(deps) {
5594
5656
  ...browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}
5595
5657
  };
5596
5658
  const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
5597
- const fallbackNote = `[open_in_browser] 브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:
5659
+ const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:
5598
5660
  ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
5599
5661
  const qr = await renderQr(attachUrl);
5600
5662
  const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
@@ -7050,10 +7112,6 @@ const DEV_TOOL_DEFINITIONS = [
7050
7112
  wait_for_attach: {
7051
7113
  type: "boolean",
7052
7114
  description: "If true, block until a page attaches."
7053
- },
7054
- open_in_browser: {
7055
- type: "boolean",
7056
- description: "If true (default), open the QR PNG in the OS browser."
7057
7115
  }
7058
7116
  },
7059
7117
  required: ["scheme_url"]
@@ -7264,7 +7322,7 @@ function createDevServer(deps = {}) {
7264
7322
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
7265
7323
  const server = new Server({
7266
7324
  name: "ait-devtools",
7267
- version: "0.1.81"
7325
+ version: "0.1.85"
7268
7326
  }, { capabilities: { tools: {} } });
7269
7327
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
7270
7328
  server.setRequestHandler(CallToolRequestSchema, async (request) => {