@ait-co/devtools 0.1.72 → 0.1.74

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 (51) hide show
  1. package/dist/{deeplink-CaO6hZVG.js → deeplink-B-94XmWA.js} +19 -3
  2. package/dist/deeplink-B-94XmWA.js.map +1 -0
  3. package/dist/{deeplink-BONXxWEO.cjs → deeplink-BLU2_hg6.cjs} +19 -3
  4. package/dist/deeplink-BLU2_hg6.cjs.map +1 -0
  5. package/dist/{deeplink-CCGiyoHq.cjs → deeplink-CU6opogq.cjs} +19 -3
  6. package/dist/deeplink-CU6opogq.cjs.map +1 -0
  7. package/dist/{deeplink-Cqli4qzm.js → deeplink-CYqDwVYs.js} +19 -3
  8. package/dist/deeplink-CYqDwVYs.js.map +1 -0
  9. package/dist/mcp/cli.js +162 -54
  10. package/dist/mcp/cli.js.map +1 -1
  11. package/dist/mcp/server.js +2 -2
  12. package/dist/mcp/server.js.map +1 -1
  13. package/dist/panel/index.js +22 -8
  14. package/dist/panel/index.js.map +1 -1
  15. package/dist/{qr-http-server-Bh9qkiRm.js → qr-http-server-Bn2ciFuC.js} +65 -12
  16. package/dist/qr-http-server-Bn2ciFuC.js.map +1 -0
  17. package/dist/{qr-http-server-DPOOrh6y.cjs → qr-http-server-BqZ8c0Bp.cjs} +65 -12
  18. package/dist/qr-http-server-BqZ8c0Bp.cjs.map +1 -0
  19. package/dist/{qr-http-server-CwkTFbMV.js → qr-http-server-Cpc4jfTA.js} +65 -12
  20. package/dist/qr-http-server-Cpc4jfTA.js.map +1 -0
  21. package/dist/{qr-http-server-CG198dpc.cjs → qr-http-server-DNGVwI0P.cjs} +65 -12
  22. package/dist/qr-http-server-DNGVwI0P.cjs.map +1 -0
  23. package/dist/{tunnel-CQdoKopR.js → tunnel-BuymAS3O.js} +22 -10
  24. package/dist/tunnel-BuymAS3O.js.map +1 -0
  25. package/dist/{tunnel-BS6Td6f4.cjs → tunnel-CAxygywQ.cjs} +22 -10
  26. package/dist/tunnel-CAxygywQ.cjs.map +1 -0
  27. package/dist/unplugin/index.cjs +13 -3
  28. package/dist/unplugin/index.cjs.map +1 -1
  29. package/dist/unplugin/index.d.cts.map +1 -1
  30. package/dist/unplugin/index.d.ts.map +1 -1
  31. package/dist/unplugin/index.js +13 -3
  32. package/dist/unplugin/index.js.map +1 -1
  33. package/dist/unplugin/tunnel.cjs +21 -9
  34. package/dist/unplugin/tunnel.cjs.map +1 -1
  35. package/dist/unplugin/tunnel.d.cts +36 -3
  36. package/dist/unplugin/tunnel.d.cts.map +1 -1
  37. package/dist/unplugin/tunnel.d.ts +36 -3
  38. package/dist/unplugin/tunnel.d.ts.map +1 -1
  39. package/dist/unplugin/tunnel.js +21 -9
  40. package/dist/unplugin/tunnel.js.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/deeplink-BONXxWEO.cjs.map +0 -1
  43. package/dist/deeplink-CCGiyoHq.cjs.map +0 -1
  44. package/dist/deeplink-CaO6hZVG.js.map +0 -1
  45. package/dist/deeplink-Cqli4qzm.js.map +0 -1
  46. package/dist/qr-http-server-Bh9qkiRm.js.map +0 -1
  47. package/dist/qr-http-server-CG198dpc.cjs.map +0 -1
  48. package/dist/qr-http-server-CwkTFbMV.js.map +0 -1
  49. package/dist/qr-http-server-DPOOrh6y.cjs.map +0 -1
  50. package/dist/tunnel-BS6Td6f4.cjs.map +0 -1
  51. package/dist/tunnel-CQdoKopR.js.map +0 -1
package/dist/mcp/cli.js CHANGED
@@ -1012,6 +1012,11 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
1012
1012
  * is injected. `&at=<totpCode>` is added only when a code is provided (same
1013
1013
  * conditional as {@link buildDeepLinkAttachUrl}).
1014
1014
  *
1015
+ * When `opts.name` is given (non-blank), it is added as `&name=` so the
1016
+ * launcher partner bar shows the app name instead of the generic default (#498).
1017
+ * When `opts.icon` is an absolute https:// URL, it is added as `&icon=` so the
1018
+ * launcher can render an icon next to the title (#498).
1019
+ *
1015
1020
  * Unlike `buildDeepLinkAttachUrl` (which splices onto a non-special scheme URL
1016
1021
  * via raw string manipulation), this function uses WHATWG `encodeURIComponent`
1017
1022
  * because the target is a standard `https:` URL.
@@ -1026,12 +1031,23 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
1026
1031
  * @param totpCode - Optional current TOTP code (6 digits). When provided, it
1027
1032
  * is appended as `at=<totpCode>`. Must be computed at call time — it rotates
1028
1033
  * every 30 s. Omit when TOTP is disabled.
1034
+ * @param opts - Optional app identity hints: `name` and `icon` (#498).
1029
1035
  * @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>
1030
- * [&at=<code>]` params.
1036
+ * [&at=<code>][&name=<enc>][&icon=<enc>]` params.
1031
1037
  */
1032
- function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode) {
1038
+ function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode, opts) {
1033
1039
  let url = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}&debug=1&relay=${encodeURIComponent(wssUrl)}`;
1034
1040
  if (totpCode !== void 0 && totpCode !== "") url += `&at=${encodeURIComponent(totpCode)}`;
1041
+ if (opts?.name !== void 0 && opts.name.trim() !== "") url += `&name=${encodeURIComponent(opts.name.trim())}`;
1042
+ if (opts?.icon !== void 0) {
1043
+ let iconParsed;
1044
+ try {
1045
+ iconParsed = new URL(opts.icon);
1046
+ } catch {
1047
+ iconParsed = null;
1048
+ }
1049
+ if (iconParsed?.protocol === "https:") url += `&icon=${encodeURIComponent(opts.icon)}`;
1050
+ }
1035
1051
  return url;
1036
1052
  }
1037
1053
  /**
@@ -2080,6 +2096,9 @@ const en = {
2080
2096
  "dashboard.pages.empty": "No attached pages",
2081
2097
  "dashboard.url.copy": "Copy",
2082
2098
  "dashboard.url.copied": "Copied",
2099
+ "dashboard.inspector.section": "Inspector",
2100
+ "dashboard.inspector.open": "Open inspector",
2101
+ "dashboard.inspector.waiting": "Inspector URL pending — appears after a page attaches",
2083
2102
  "attach.title": "AIT Debug Session — QR Scan",
2084
2103
  "attach.deployment": "deployment: {label}",
2085
2104
  "attach.steps.section": "How to scan",
@@ -2110,7 +2129,6 @@ const en = {
2110
2129
  "launcher.urlPlaceholder": "https://example.trycloudflare.com",
2111
2130
  "launcher.openBtn": "Open",
2112
2131
  "launcher.scanBtn": "Scan QR with camera",
2113
- "launcher.rescanBtn": "Rescan",
2114
2132
  "launcher.noCamera": "No camera available — paste the URL instead.",
2115
2133
  "launcher.cameraError": "Could not access the camera — paste the URL instead.",
2116
2134
  "launcher.invalidUrlHttps": "Enter a valid https:// URL (the tunnel URL from your terminal).",
@@ -2119,11 +2137,16 @@ const en = {
2119
2137
  "launcher.debugAuthFailedHint": "The QR code may have expired. Scan a fresh QR code.",
2120
2138
  "launcher.debugAuthExpiredHint": "The debug session has expired. Scan a fresh QR from the attach page on your Mac.",
2121
2139
  "launcher.debugAuthRescanCta": "Scan a new QR",
2122
- "launcher.diagFab": "Diag",
2123
2140
  "launcher.diagTitle": "Viewport diagnostics",
2124
2141
  "launcher.diagYes": "yes",
2125
2142
  "launcher.diagNo": "no",
2126
- "launcher.letterboxDetected": "Display area is {pt}pt short — likely an iOS standalone letterbox. Removing and re-adding the launcher to the home screen may fix it."
2143
+ "launcher.letterboxDetected": "Display area is {pt}pt short — likely an iOS standalone letterbox. Removing and re-adding the launcher to the home screen may fix it.",
2144
+ "launcher.navbar.defaultTitle": "Mini App",
2145
+ "launcher.navbar.menu": "Menu",
2146
+ "launcher.navbar.close": "Close",
2147
+ "launcher.navbar.menuRescan": "Rescan",
2148
+ "launcher.navbar.menuDiag": "Viewport diagnostics",
2149
+ "launcher.navbar.menuLanguage": "Language"
2127
2150
  };
2128
2151
  //#endregion
2129
2152
  //#region src/i18n/index.ts
@@ -2324,6 +2347,9 @@ const tables = {
2324
2347
  "dashboard.pages.empty": "attach된 페이지 없음",
2325
2348
  "dashboard.url.copy": "복사",
2326
2349
  "dashboard.url.copied": "복사됨",
2350
+ "dashboard.inspector.section": "인스펙터",
2351
+ "dashboard.inspector.open": "인스펙터 열기",
2352
+ "dashboard.inspector.waiting": "인스펙터 URL 대기 중 (페이지 attach 후 표시됩니다)",
2327
2353
  "attach.title": "AIT 디버그 세션 — QR 스캔",
2328
2354
  "attach.deployment": "deployment: {label}",
2329
2355
  "attach.steps.section": "스캔 절차",
@@ -2354,7 +2380,6 @@ const tables = {
2354
2380
  "launcher.urlPlaceholder": "https://example.trycloudflare.com",
2355
2381
  "launcher.openBtn": "Open",
2356
2382
  "launcher.scanBtn": "QR 카메라로 스캔",
2357
- "launcher.rescanBtn": "Rescan",
2358
2383
  "launcher.noCamera": "카메라를 사용할 수 없습니다 — URL을 직접 붙여넣으세요.",
2359
2384
  "launcher.cameraError": "카메라에 접근할 수 없습니다 — URL을 직접 붙여넣으세요.",
2360
2385
  "launcher.invalidUrlHttps": "올바른 https:// URL을 입력하세요 (터미널의 터널 URL).",
@@ -2363,11 +2388,16 @@ const tables = {
2363
2388
  "launcher.debugAuthFailedHint": "QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.",
2364
2389
  "launcher.debugAuthExpiredHint": "디버그 세션이 만료됐어요. Mac의 attach 페이지에서 새 QR을 스캔하세요.",
2365
2390
  "launcher.debugAuthRescanCta": "새 QR 스캔하기",
2366
- "launcher.diagFab": "진단",
2367
2391
  "launcher.diagTitle": "뷰포트 진단",
2368
2392
  "launcher.diagYes": "예",
2369
2393
  "launcher.diagNo": "아니요",
2370
- "launcher.letterboxDetected": "표시 영역이 {pt}pt 부족합니다 — iOS standalone letterbox로 보입니다. 런처를 홈 화면에서 제거 후 다시 설치하면 해소될 수 있어요."
2394
+ "launcher.letterboxDetected": "표시 영역이 {pt}pt 부족합니다 — iOS standalone letterbox로 보입니다. 런처를 홈 화면에서 제거 후 다시 설치하면 해소될 수 있어요.",
2395
+ "launcher.navbar.defaultTitle": "미니앱",
2396
+ "launcher.navbar.menu": "메뉴",
2397
+ "launcher.navbar.close": "닫기",
2398
+ "launcher.navbar.menuRescan": "다시 스캔",
2399
+ "launcher.navbar.menuDiag": "뷰포트 진단",
2400
+ "launcher.navbar.menuLanguage": "언어"
2371
2401
  },
2372
2402
  en
2373
2403
  };
@@ -2455,6 +2485,14 @@ img.qr {
2455
2485
  }
2456
2486
  .copy-btn:hover { background: #30363d; }
2457
2487
  .hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }
2488
+ .inspector-link {
2489
+ display: inline-block; margin-top: 0.5rem;
2490
+ padding: 0.45rem 1rem; border-radius: 6px;
2491
+ background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;
2492
+ text-decoration: none; text-align: center;
2493
+ }
2494
+ .inspector-link:hover { background: #388bfd; }
2495
+ .inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
2458
2496
  ul { margin: 0; padding-left: 1.25rem; }
2459
2497
  li { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }
2460
2498
  li.empty { opacity: 0.4; list-style: none; padding-left: 0; }
@@ -2464,7 +2502,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
2464
2502
  .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2465
2503
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2466
2504
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2467
- </style></head><body><h1>AIT 디버그 Dashboard</h1>__LANG_SWITCHER__<p class="updated" id="updated">마지막 갱신: __NOW__</p><section><h2>터널 상태</h2><span class="status __TUNNEL_CLASS__" id="tunnel-status">__TUNNEL_STATUS__</span></section><hr/><section><h2>Attach QR</h2><div id="attach-section">__ATTACH_SECTION__</div></section>__PAGES_SECTION__</body></html>`;
2505
+ </style></head><body><h1>AIT 디버그 Dashboard</h1>__LANG_SWITCHER__<p class="updated" id="updated">마지막 갱신: __NOW__</p><section><h2>터널 상태</h2><span class="status __TUNNEL_CLASS__" id="tunnel-status">__TUNNEL_STATUS__</span></section><hr/><section><h2>Attach QR</h2><div id="attach-section">__ATTACH_SECTION__</div></section><hr/><section id="inspector-section"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section>__PAGES_SECTION__</body></html>`;
2468
2506
  const attachChromeHtmlKoSandbox = `<!DOCTYPE html>
2469
2507
  <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>
2470
2508
  *, *::before, *::after { box-sizing: border-box; }
@@ -2607,6 +2645,14 @@ img.qr {
2607
2645
  }
2608
2646
  .copy-btn:hover { background: #30363d; }
2609
2647
  .hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }
2648
+ .inspector-link {
2649
+ display: inline-block; margin-top: 0.5rem;
2650
+ padding: 0.45rem 1rem; border-radius: 6px;
2651
+ background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;
2652
+ text-decoration: none; text-align: center;
2653
+ }
2654
+ .inspector-link:hover { background: #388bfd; }
2655
+ .inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
2610
2656
  ul { margin: 0; padding-left: 1.25rem; }
2611
2657
  li { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }
2612
2658
  li.empty { opacity: 0.4; list-style: none; padding-left: 0; }
@@ -2616,7 +2662,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
2616
2662
  .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2617
2663
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2618
2664
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2619
- </style></head><body><h1>AIT Debug Dashboard</h1>__LANG_SWITCHER__<p class="updated" id="updated">Last updated: __NOW__</p><section><h2>Tunnel status</h2><span class="status __TUNNEL_CLASS__" id="tunnel-status">__TUNNEL_STATUS__</span></section><hr/><section><h2>Attach QR</h2><div id="attach-section">__ATTACH_SECTION__</div></section>__PAGES_SECTION__</body></html>`;
2665
+ </style></head><body><h1>AIT Debug Dashboard</h1>__LANG_SWITCHER__<p class="updated" id="updated">Last updated: __NOW__</p><section><h2>Tunnel status</h2><span class="status __TUNNEL_CLASS__" id="tunnel-status">__TUNNEL_STATUS__</span></section><hr/><section><h2>Attach QR</h2><div id="attach-section">__ATTACH_SECTION__</div></section><hr/><section id="inspector-section"><h2>Inspector</h2>__INSPECTOR_SECTION__</section>__PAGES_SECTION__</body></html>`;
2620
2666
  const attachChromeHtmlEnSandbox = `<!DOCTYPE html>
2621
2667
  <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>
2622
2668
  *, *::before, *::after { box-sizing: border-box; }
@@ -2793,13 +2839,15 @@ function buildLangSwitcher(path, existingParams, locale, s) {
2793
2839
  *
2794
2840
  * 동적 파트 분류:
2795
2841
  * - "token-fill": 단일 값 교체 (__NOW__, __TUNNEL_CLASS__, __TUNNEL_STATUS__,
2796
- * __ATTACH_SECTION__)
2842
+ * __ATTACH_SECTION__, __INSPECTOR_SECTION__)
2797
2843
  * - "runtime builder": 가변 길이 구조 (__PAGES_SECTION__ — 조건부 렌더 + 가변 rows)
2798
2844
  * - "suffix": inline SSE <script> (빌드 파이프라인 없는 클라이언트 스크립트, locale
2799
2845
  * aware 문자열 포함)
2800
2846
  *
2801
2847
  * SECRET-HANDLING:
2802
2848
  * - attachUrl은 url-box 안에서만 노출 (TOTP at= 코드 캡슐 그대로).
2849
+ * - inspectorUrl은 anchor href 안에서만 노출 (TOTP at= 코드 캡슐 그대로).
2850
+ * relay host + TOTP 코드가 담길 수 있으나 대시보드 HTML은 의도된 transport.
2803
2851
  * - tunnel wssUrl은 "터널 연결됨" 상태 표시에서 UP/DOWN만 노출.
2804
2852
  * wssUrl 값 자체는 dashboard HTML에 넣지 않는다.
2805
2853
  */
@@ -2814,6 +2862,9 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
2814
2862
  const copyLabel = escapeHtml(s("dashboard.url.copy"));
2815
2863
  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>`;
2816
2864
  } else attachSection = `<p class="hint">${escapeHtml(s("dashboard.attach.hint"))}</p>`;
2865
+ let inspectorSection;
2866
+ 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>`;
2867
+ else inspectorSection = `<span class="inspector-hint" id="inspector-link">${escapeHtml(s("dashboard.inspector.waiting"))}</span>`;
2817
2868
  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) => {
2818
2869
  return `<li><span class="page-id">${escapeHtml(p.id)}</span> <span class="page-url">${escapeHtml(p.url.slice(0, 120))}</span></li>`;
2819
2870
  }).join("\n") : `<li class="empty">${escapeHtml(s("dashboard.pages.empty"))}</li>`}</ul></section>`;
@@ -2824,10 +2875,12 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
2824
2875
  attachHint: JSON.stringify(s("dashboard.attach.hint")),
2825
2876
  copyLabel: JSON.stringify(s("dashboard.url.copy")),
2826
2877
  copiedLabel: JSON.stringify(s("dashboard.url.copied")),
2878
+ inspectorOpenLabel: JSON.stringify(s("dashboard.inspector.open")),
2879
+ inspectorWaitingLabel: JSON.stringify(s("dashboard.inspector.waiting")),
2827
2880
  dashboardSurface: true
2828
2881
  };
2829
2882
  const langSwitcher = buildLangSwitcher(path, params, locale, s);
2830
- const filled = dashboardChromeByLocale[locale].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__NOW__", escapeHtml(now)).replaceAll("__TUNNEL_CLASS__", tunnelClass).replaceAll("__TUNNEL_STATUS__", escapeHtml(tunnelStatus)).replaceAll("__ATTACH_SECTION__", attachSection).replaceAll("__PAGES_SECTION__", pagesSection);
2883
+ const filled = dashboardChromeByLocale[locale].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__NOW__", escapeHtml(now)).replaceAll("__TUNNEL_CLASS__", tunnelClass).replaceAll("__TUNNEL_STATUS__", escapeHtml(tunnelStatus)).replaceAll("__ATTACH_SECTION__", attachSection).replaceAll("__INSPECTOR_SECTION__", inspectorSection).replaceAll("__PAGES_SECTION__", pagesSection);
2831
2884
  const sseScript = buildSseScript(sseStrings);
2832
2885
  return filled.replace("</body>", `${sseScript}\n</body>`);
2833
2886
  }
@@ -2865,6 +2918,8 @@ function buildSseScript(strings) {
2865
2918
  var ATTACH_HINT = ${strings.attachHint};
2866
2919
  var COPY_LABEL = ${strings.copyLabel};
2867
2920
  var COPIED_LABEL = ${strings.copiedLabel};
2921
+ var INSPECTOR_OPEN_LABEL = ${strings.inspectorOpenLabel};
2922
+ var INSPECTOR_WAITING_LABEL = ${strings.inspectorWaitingLabel};
2868
2923
 
2869
2924
  // ── 클립보드 복사 헬퍼 ────────────────────────────────────────────────
2870
2925
  function copyText(text) {
@@ -2978,6 +3033,17 @@ function buildSseScript(strings) {
2978
3033
  sec.innerHTML = '<p class=\\"hint\\">' + ATTACH_HINT + '</p>';`}
2979
3034
  }
2980
3035
  }
3036
+ // 인스펙터 링크 갱신 — #inspector-link (#503).
3037
+ // SECRET-HANDLING: inspectorUrl을 console.log 등으로 출력하지 않는다.
3038
+ var insp = document.getElementById('inspector-link');
3039
+ if (insp) {
3040
+ if (s.inspectorUrl) {
3041
+ var safeInspUrl = String(s.inspectorUrl).slice(0, 2000).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
3042
+ insp.outerHTML = '<a class=\\"inspector-link\\" id=\\"inspector-link\\" href=\\"' + safeInspUrl + '\\" target=\\"_blank\\" rel=\\"noopener noreferrer\\">' + INSPECTOR_OPEN_LABEL + '</a>';
3043
+ } else {
3044
+ insp.outerHTML = '<span class=\\"inspector-hint\\" id=\\"inspector-link\\">' + INSPECTOR_WAITING_LABEL + '</span>';
3045
+ }
3046
+ }
2981
3047
  // 갱신 시각 (dashboard만 #updated 요소 있음)
2982
3048
  var upd = document.getElementById('updated');
2983
3049
  if (upd) upd.textContent = upd.textContent.replace(/[^ ]+$/, new Date().toISOString());
@@ -3022,6 +3088,8 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/a
3022
3088
  attachHint: JSON.stringify(s("dashboard.attach.hint")),
3023
3089
  copyLabel: JSON.stringify(s("dashboard.url.copy")),
3024
3090
  copiedLabel: JSON.stringify(s("dashboard.url.copied")),
3091
+ inspectorOpenLabel: JSON.stringify(s("dashboard.inspector.open")),
3092
+ inspectorWaitingLabel: JSON.stringify(s("dashboard.inspector.waiting")),
3025
3093
  dashboardSurface: false
3026
3094
  });
3027
3095
  return filled.replace("</body>", `${sseScript}\n</body>`);
@@ -3045,7 +3113,8 @@ async function startQrHttpServer(getDashboardState) {
3045
3113
  wssUrl: state.tunnel.wssUrl
3046
3114
  },
3047
3115
  pages: state.pages,
3048
- attachUrl: state.attachUrl
3116
+ attachUrl: state.attachUrl,
3117
+ inspectorUrl: state.inspectorUrl ?? null
3049
3118
  });
3050
3119
  res.write(`data: ${payload}\n\n`);
3051
3120
  }
@@ -3552,7 +3621,7 @@ const DEBUG_TOOL_DEFINITIONS = [
3552
3621
  },
3553
3622
  {
3554
3623
  name: "build_attach_url",
3555
- 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=…). 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.",
3624
+ 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.",
3556
3625
  inputSchema: {
3557
3626
  type: "object",
3558
3627
  properties: {
@@ -4566,7 +4635,7 @@ async function readMcpSdkVersion() {
4566
4635
  * some test environments that skip the build step).
4567
4636
  */
4568
4637
  function readDevtoolsVersion() {
4569
- return "0.1.72";
4638
+ return "0.1.74";
4570
4639
  }
4571
4640
  /**
4572
4641
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -5070,7 +5139,7 @@ function createDebugServer(deps) {
5070
5139
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
5071
5140
  const server = new Server({
5072
5141
  name: "ait-debug",
5073
- version: "0.1.72"
5142
+ version: "0.1.74"
5074
5143
  }, { capabilities: { tools: { listChanged: true } } });
5075
5144
  server.setRequestHandler(ListToolsRequestSchema, () => {
5076
5145
  const conn = router.active;
@@ -5170,11 +5239,20 @@ function createDebugServer(deps) {
5170
5239
  expiresAt: new Date(expiresAtMs).toISOString()
5171
5240
  };
5172
5241
  }
5173
- const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode);
5242
+ let launcherAppName;
5243
+ if (buildProjectRoot !== void 0) try {
5244
+ const { readFileSync } = await import("node:fs");
5245
+ const pkgRaw = readFileSync(`${buildProjectRoot}/package.json`, "utf8");
5246
+ const pkg = JSON.parse(pkgRaw);
5247
+ const rawName = typeof pkg.name === "string" ? pkg.name : "";
5248
+ launcherAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
5249
+ } catch {}
5250
+ const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode, { name: launcherAppName });
5174
5251
  onAttachUrlBuilt?.({
5175
5252
  kind: "launcher",
5176
5253
  tunnelHttpUrl,
5177
- wssUrl: tunnelStatus.wssUrl
5254
+ wssUrl: tunnelStatus.wssUrl,
5255
+ appName: launcherAppName
5178
5256
  });
5179
5257
  const relayUrl = tunnelStatus.wssUrl;
5180
5258
  const totp = totpMeta;
@@ -5610,7 +5688,7 @@ function makeSingleConnectionRouter(connection) {
5610
5688
  function rebuildAttachUrl(parts) {
5611
5689
  const secret = process.env.AIT_DEBUG_TOTP_SECRET;
5612
5690
  const code = secret ? generateTotp(secret) : void 0;
5613
- return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code) : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);
5691
+ return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, { name: parts.appName }) : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);
5614
5692
  }
5615
5693
  function jsonResult$1(value) {
5616
5694
  return { content: [{
@@ -5987,6 +6065,15 @@ var DualConnectionRouter = class {
5987
6065
  get activeRelayOrigin() {
5988
6066
  return this.activeFamily?.relayOrigin;
5989
6067
  }
6068
+ /**
6069
+ * Local HTTP base URL of the Chii relay for the currently-active family (#503).
6070
+ * Used by `getDashboardState` to build the inspector URL via `buildChiiInspectorUrl`.
6071
+ * Returns `undefined` when no relay family is active (local/mock mode).
6072
+ * SECRET-HANDLING: not logged — callers must not write this to stdout/logs.
6073
+ */
6074
+ get activeRelayHttpUrl() {
6075
+ return this.activeFamily?.relayHttpUrl;
6076
+ }
5990
6077
  /** Every booted family (for unified shutdown). All families are lazy (#396). */
5991
6078
  bootedFamilies() {
5992
6079
  return [...this.lazyFamilies.values()];
@@ -6114,18 +6201,25 @@ async function runDebugServer(options = {}) {
6114
6201
  return router.active;
6115
6202
  });
6116
6203
  let lastAttachParts = null;
6117
- const getDashboardState = () => ({
6118
- tunnel: {
6119
- up: router.relayTunnelStatus().up,
6120
- wssUrl: router.relayTunnelStatus().wssUrl
6121
- },
6122
- pages: router.active.listTargets().map((t) => ({
6123
- id: t.id,
6124
- url: t.url
6125
- })),
6126
- attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
6127
- mode: deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin)
6128
- });
6204
+ const getDashboardState = () => {
6205
+ const targets = router.active.listTargets();
6206
+ const relayHttpUrl = router.activeRelayHttpUrl;
6207
+ const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;
6208
+ const inspectorUrl = relayHttpUrl && targets.length > 0 ? buildChiiInspectorUrl(relayHttpUrl, targets[0].id, totpSecret ? () => generateTotp(totpSecret, Date.now()) : void 0) : null;
6209
+ return {
6210
+ tunnel: {
6211
+ up: router.relayTunnelStatus().up,
6212
+ wssUrl: router.relayTunnelStatus().wssUrl
6213
+ },
6214
+ pages: targets.map((t) => ({
6215
+ id: t.id,
6216
+ url: t.url
6217
+ })),
6218
+ attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
6219
+ inspectorUrl,
6220
+ mode: deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin)
6221
+ };
6222
+ };
6129
6223
  let qrServer;
6130
6224
  try {
6131
6225
  qrServer = await startQrHttpServer(getDashboardState);
@@ -6280,17 +6374,24 @@ async function runLocalDebugServer(options = {}) {
6280
6374
  return router.active;
6281
6375
  });
6282
6376
  let lastAttachParts = null;
6283
- const getDashboardState = () => ({
6284
- tunnel: {
6285
- up: router.relayTunnelStatus().up,
6286
- wssUrl: router.relayTunnelStatus().wssUrl
6287
- },
6288
- pages: router.active.listTargets().map((t) => ({
6289
- id: t.id,
6290
- url: t.url
6291
- })),
6292
- attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null
6293
- });
6377
+ const getDashboardState = () => {
6378
+ const targets = router.active.listTargets();
6379
+ const relayHttpUrl = router.activeRelayHttpUrl;
6380
+ const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;
6381
+ const inspectorUrl = relayHttpUrl && targets.length > 0 ? buildChiiInspectorUrl(relayHttpUrl, targets[0].id, totpSecret ? () => generateTotp(totpSecret, Date.now()) : void 0) : null;
6382
+ return {
6383
+ tunnel: {
6384
+ up: router.relayTunnelStatus().up,
6385
+ wssUrl: router.relayTunnelStatus().wssUrl
6386
+ },
6387
+ pages: targets.map((t) => ({
6388
+ id: t.id,
6389
+ url: t.url
6390
+ })),
6391
+ attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
6392
+ inspectorUrl
6393
+ };
6394
+ };
6294
6395
  let qrServer;
6295
6396
  try {
6296
6397
  qrServer = await startQrHttpServer(getDashboardState);
@@ -6429,17 +6530,24 @@ async function runMobileDebugServer(options = {}) {
6429
6530
  return router.active;
6430
6531
  });
6431
6532
  let lastAttachParts = null;
6432
- const getDashboardState = () => ({
6433
- tunnel: {
6434
- up: router.relayTunnelStatus().up,
6435
- wssUrl: router.relayTunnelStatus().wssUrl
6436
- },
6437
- pages: router.active.listTargets().map((t) => ({
6438
- id: t.id,
6439
- url: t.url
6440
- })),
6441
- attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null
6442
- });
6533
+ const getDashboardState = () => {
6534
+ const targets = router.active.listTargets();
6535
+ const relayHttpUrl = router.activeRelayHttpUrl;
6536
+ const totpSecret = process.env.AIT_DEBUG_TOTP_SECRET;
6537
+ const inspectorUrl = relayHttpUrl && targets.length > 0 ? buildChiiInspectorUrl(relayHttpUrl, targets[0].id, totpSecret ? () => generateTotp(totpSecret, Date.now()) : void 0) : null;
6538
+ return {
6539
+ tunnel: {
6540
+ up: router.relayTunnelStatus().up,
6541
+ wssUrl: router.relayTunnelStatus().wssUrl
6542
+ },
6543
+ pages: targets.map((t) => ({
6544
+ id: t.id,
6545
+ url: t.url
6546
+ })),
6547
+ attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
6548
+ inspectorUrl
6549
+ };
6550
+ };
6443
6551
  let qrServer;
6444
6552
  try {
6445
6553
  qrServer = await startQrHttpServer(getDashboardState);
@@ -6957,7 +7065,7 @@ function createDevServer(deps = {}) {
6957
7065
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
6958
7066
  const server = new Server({
6959
7067
  name: "ait-devtools",
6960
- version: "0.1.72"
7068
+ version: "0.1.74"
6961
7069
  }, { capabilities: { tools: {} } });
6962
7070
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
6963
7071
  server.setRequestHandler(CallToolRequestSchema, async (request) => {