@ait-co/devtools 0.1.64 → 0.1.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/cli.js CHANGED
@@ -1821,6 +1821,8 @@ const en = {
1821
1821
  "notifications.option.newAgreement": "newAgreement (first-time agree)",
1822
1822
  "notifications.option.alreadyAgreed": "alreadyAgreed (already opted-in)",
1823
1823
  "notifications.option.agreementRejected": "agreementRejected (user declined)",
1824
+ "dashboard.lang.ko": "한국어",
1825
+ "dashboard.lang.en": "English",
1824
1826
  "dashboard.title": "AIT Debug Dashboard",
1825
1827
  "dashboard.updated": "Last updated: {ts}",
1826
1828
  "dashboard.tunnel.section": "Tunnel status",
@@ -2044,6 +2046,8 @@ const tables = {
2044
2046
  "notifications.option.newAgreement": "newAgreement (최초 동의)",
2045
2047
  "notifications.option.alreadyAgreed": "alreadyAgreed (이미 동의됨)",
2046
2048
  "notifications.option.agreementRejected": "agreementRejected (사용자 거절)",
2049
+ "dashboard.lang.ko": "한국어",
2050
+ "dashboard.lang.en": "English",
2047
2051
  "dashboard.title": "AIT 디버그 Dashboard",
2048
2052
  "dashboard.updated": "마지막 갱신: {ts}",
2049
2053
  "dashboard.tunnel.section": "터널 상태",
@@ -2096,11 +2100,11 @@ function localeFromLanguageTag(lang) {
2096
2100
  * surfaces (e.g. the qr-http-server dashboard) have no `navigator`, so the
2097
2101
  * request header is the only language signal. Reads the FIRST language tag
2098
2102
  * (highest priority, ignoring `q=` weights — good enough for ko/en) and feeds
2099
- * it through the same `ko`-vs-`en` heuristic `detectLocale` uses. Returns `'en'`
2100
- * for an empty/missing header.
2103
+ * it through the same `ko`-vs-`en` heuristic `detectLocale` uses. Returns `'ko'`
2104
+ * for an empty/missing header (ko is the primary locale).
2101
2105
  */
2102
2106
  function parseAcceptLanguage(header) {
2103
- if (!header) return "en";
2107
+ if (!header) return "ko";
2104
2108
  return localeFromLanguageTag(header.split(",")[0]?.trim().split(";")[0]?.trim() ?? "");
2105
2109
  }
2106
2110
  /**
@@ -2161,7 +2165,10 @@ li.empty { opacity: 0.4; list-style: none; padding-left: 0; }
2161
2165
  .page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }
2162
2166
  .page-url { word-break: break-all; }
2163
2167
  hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
2164
- </style></head><body><h1>AIT 디버그 Dashboard</h1><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>`;
2168
+ .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2169
+ .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2170
+ .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2171
+ </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>`;
2165
2172
  const attachChromeHtmlKo = `<!DOCTYPE html>
2166
2173
  <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>
2167
2174
  *, *::before, *::after { box-sizing: border-box; }
@@ -2191,7 +2198,10 @@ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
2191
2198
  border-radius: 6px; border: 1px solid #30363d;
2192
2199
  }
2193
2200
  hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
2194
- </style></head><body><h1>AIT 디버그 세션 QR 스캔</h1><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></ul></section><hr/><section><h2>URL (fallback)</h2><p class="url-box">__SAFE_ATTACH_URL__</p></section></body></html>`;
2201
+ .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2202
+ .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2203
+ .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2204
+ </style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__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></ul></section><hr/><section><h2>URL (fallback)</h2><p class="url-box">__SAFE_ATTACH_URL__</p></section></body></html>`;
2195
2205
  const dashboardChromeHtmlEn = `<!DOCTYPE html>
2196
2206
  <html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>AIT Debug Dashboard</title><style>
2197
2207
  *, *::before, *::after { box-sizing: border-box; }
@@ -2228,7 +2238,10 @@ li.empty { opacity: 0.4; list-style: none; padding-left: 0; }
2228
2238
  .page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }
2229
2239
  .page-url { word-break: break-all; }
2230
2240
  hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
2231
- </style></head><body><h1>AIT Debug Dashboard</h1><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>`;
2241
+ .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2242
+ .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2243
+ .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2244
+ </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>`;
2232
2245
  const attachChromeHtmlEn = `<!DOCTYPE html>
2233
2246
  <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>
2234
2247
  *, *::before, *::after { box-sizing: border-box; }
@@ -2258,7 +2271,10 @@ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
2258
2271
  border-radius: 6px; border: 1px solid #30363d;
2259
2272
  }
2260
2273
  hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
2261
- </style></head><body><h1>AIT Debug Session QR Scan</h1><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></ul></section><hr/><section><h2>URL (fallback)</h2><p class="url-box">__SAFE_ATTACH_URL__</p></section></body></html>`;
2274
+ .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2275
+ .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2276
+ .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2277
+ </style></head><body><h1>AIT Debug Session — QR Scan</h1>__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></ul></section><hr/><section><h2>URL (fallback)</h2><p class="url-box">__SAFE_ATTACH_URL__</p></section></body></html>`;
2262
2278
  /** Map from Locale to the precompiled dashboard chrome string. */
2263
2279
  const dashboardChromeByLocale = {
2264
2280
  ko: dashboardChromeHtmlKo,
@@ -2276,6 +2292,24 @@ function escapeHtml(s) {
2276
2292
  return s.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
2277
2293
  }
2278
2294
  /**
2295
+ * 현재 path+query에서 lang 파라미터만 교체한 ko/en 토글 링크를 생성한다.
2296
+ *
2297
+ * SECRET-HANDLING: u= (attachUrl, TOTP at= 캡슐 포함) 등 기존 query를 보존한다.
2298
+ * lang= 만 덮어쓴다. 링크 href에 at= 코드가 들어가는 건 의도된 전달 경로.
2299
+ */
2300
+ function buildLangSwitcher(path, existingParams, locale, s) {
2301
+ function switcherHref(targetLang) {
2302
+ const p = new URLSearchParams(existingParams);
2303
+ p.set("lang", targetLang);
2304
+ return `${escapeHtml(path)}?${p.toString()}`;
2305
+ }
2306
+ const koLabel = escapeHtml(s("dashboard.lang.ko"));
2307
+ const enLabel = escapeHtml(s("dashboard.lang.en"));
2308
+ const koClass = locale === "ko" ? "active" : "";
2309
+ const enClass = locale === "en" ? "active" : "";
2310
+ return `<div class="lang-switcher"><a href="${switcherHref("ko")}" class="${koClass}">${koLabel}</a><a href="${switcherHref("en")}" class="${enClass}">${enLabel}</a></div>`;
2311
+ }
2312
+ /**
2279
2313
  * Dashboard HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.
2280
2314
  *
2281
2315
  * 토큰 채우기 순서:
@@ -2295,7 +2329,7 @@ function escapeHtml(s) {
2295
2329
  * - tunnel wssUrl은 "터널 연결됨" 상태 표시에서 UP/DOWN만 노출.
2296
2330
  * wssUrl 값 자체는 dashboard HTML에 넣지 않는다.
2297
2331
  */
2298
- function buildDashboardHtml(state, qrDataUrl, locale) {
2332
+ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new URLSearchParams()) {
2299
2333
  const s = resolveLocaleStrings(locale);
2300
2334
  const now = (/* @__PURE__ */ new Date()).toISOString();
2301
2335
  const tunnelStatus = state.tunnel.up ? s("dashboard.tunnel.up") : s("dashboard.tunnel.down");
@@ -2312,7 +2346,8 @@ function buildDashboardHtml(state, qrDataUrl, locale) {
2312
2346
  pagesEmpty: JSON.stringify(s("dashboard.pages.empty")),
2313
2347
  attachHint: JSON.stringify(s("dashboard.attach.hint"))
2314
2348
  };
2315
- const filled = dashboardChromeByLocale[locale].replaceAll("__NOW__", escapeHtml(now)).replaceAll("__TUNNEL_CLASS__", tunnelClass).replaceAll("__TUNNEL_STATUS__", escapeHtml(tunnelStatus)).replaceAll("__ATTACH_SECTION__", attachSection).replaceAll("__PAGES_SECTION__", pagesSection);
2349
+ const langSwitcher = buildLangSwitcher(path, params, locale, s);
2350
+ 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);
2316
2351
  const sseScript = buildSseScript(sseStrings);
2317
2352
  return filled.replace("</body>", `${sseScript}\n</body>`);
2318
2353
  }
@@ -2401,9 +2436,10 @@ function buildSseScript(strings) {
2401
2436
  *
2402
2437
  * SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
2403
2438
  */
2404
- function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale) {
2439
+ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams()) {
2405
2440
  const s = resolveLocaleStrings(locale);
2406
- const filled = attachChromeByLocale[locale].replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl);
2441
+ const langSwitcher = buildLangSwitcher(path, params, locale, s);
2442
+ const filled = attachChromeByLocale[locale].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl);
2407
2443
  const sseScript = buildSseScript({
2408
2444
  tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
2409
2445
  tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
@@ -2438,7 +2474,8 @@ async function startQrHttpServer(getDashboardState) {
2438
2474
  const server = createServer(async (req, res) => {
2439
2475
  const [path, query = ""] = (req.url ?? "/").split("?", 2);
2440
2476
  const params = new URLSearchParams(query ?? "");
2441
- const locale = parseAcceptLanguage(req.headers["accept-language"]);
2477
+ const langParam = params.get("lang");
2478
+ const locale = langParam === "ko" || langParam === "en" ? langParam : parseAcceptLanguage(req.headers["accept-language"]);
2442
2479
  if (path === "/") {
2443
2480
  if (!getDashboardState) {
2444
2481
  res.writeHead(204, { "Content-Type": "text/plain; charset=utf-8" });
@@ -2453,7 +2490,7 @@ async function startQrHttpServer(getDashboardState) {
2453
2490
  errorCorrectionLevel: "M"
2454
2491
  });
2455
2492
  } catch {}
2456
- const html = buildDashboardHtml(state, qrDataUrl, locale);
2493
+ const html = buildDashboardHtml(state, qrDataUrl, locale, path, params);
2457
2494
  res.writeHead(200, {
2458
2495
  "Content-Type": "text/html; charset=utf-8",
2459
2496
  "Cache-Control": "no-store"
@@ -2500,7 +2537,7 @@ async function startQrHttpServer(getDashboardState) {
2500
2537
  type: "image/png",
2501
2538
  errorCorrectionLevel: "M"
2502
2539
  }).then((dataUrl) => {
2503
- const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale);
2540
+ const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params);
2504
2541
  res.writeHead(200, {
2505
2542
  "Content-Type": "text/html; charset=utf-8",
2506
2543
  "Cache-Control": "no-store"
@@ -3938,7 +3975,7 @@ async function readMcpSdkVersion() {
3938
3975
  * some test environments that skip the build step).
3939
3976
  */
3940
3977
  function readDevtoolsVersion() {
3941
- return "0.1.64";
3978
+ return "0.1.66";
3942
3979
  }
3943
3980
  /**
3944
3981
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -4426,7 +4463,7 @@ function createDebugServer(deps) {
4426
4463
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
4427
4464
  const server = new Server({
4428
4465
  name: "ait-debug",
4429
- version: "0.1.64"
4466
+ version: "0.1.66"
4430
4467
  }, { capabilities: { tools: { listChanged: true } } });
4431
4468
  server.setRequestHandler(ListToolsRequestSchema, () => {
4432
4469
  const conn = router.active;
@@ -4512,9 +4549,10 @@ function createDebugServer(deps) {
4512
4549
  const tunnelStatus = getTunnelStatus();
4513
4550
  if (!tunnelStatus.up || tunnelStatus.wssUrl === null) return mcpError("build_attach_url(mobile): relay wssUrl이 아직 설정되지 않았습니다. unplugin tunnel:{cdp:true}가 relay를 완전히 기동할 때까지 잠시 후 다시 시도하세요.");
4514
4551
  const secret = getTotpSecret();
4552
+ if (secret === void 0 || secret === "") return mcpError("build_attach_url(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.");
4515
4553
  let totpCode;
4516
4554
  let totpMeta;
4517
- if (secret !== void 0 && secret !== "") {
4555
+ {
4518
4556
  const now = Date.now();
4519
4557
  totpCode = generateTotp(secret, now);
4520
4558
  const STEP_SECONDS = 30;
@@ -4698,6 +4736,10 @@ function createDebugServer(deps) {
4698
4736
  const observedNote = observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : "";
4699
4737
  return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
4700
4738
  };
4739
+ {
4740
+ const relaySecret = getTotpSecret();
4741
+ if (relaySecret === void 0 || relaySecret === "") return mcpError("build_attach_url(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.");
4742
+ }
4701
4743
  try {
4702
4744
  const tunnelForBuild = getTunnelStatus();
4703
4745
  const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(schemeUrl, tunnelForBuild, getTotpSecret());
@@ -6292,7 +6334,7 @@ function createDevServer(deps = {}) {
6292
6334
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
6293
6335
  const server = new Server({
6294
6336
  name: "ait-devtools",
6295
- version: "0.1.64"
6337
+ version: "0.1.66"
6296
6338
  }, { capabilities: { tools: {} } });
6297
6339
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
6298
6340
  server.setRequestHandler(CallToolRequestSchema, async (request) => {