@ait-co/devtools 0.1.113 → 0.1.115

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 (47) hide show
  1. package/dist/bundle-KFs4t-wc.d.ts.map +1 -1
  2. package/dist/mcp/cli.js +166 -12
  3. package/dist/mcp/cli.js.map +1 -1
  4. package/dist/mcp/server.js +1 -1
  5. package/dist/panel/index.js +7 -1
  6. package/dist/panel/index.js.map +1 -1
  7. package/dist/{pool-mZlgCmNQ.d.ts → pool-D23t4ibd.d.ts} +2 -2
  8. package/dist/{pool-mZlgCmNQ.d.ts.map → pool-D23t4ibd.d.ts.map} +1 -1
  9. package/dist/{qr-http-server-BVS-HZjU.cjs → qr-http-server-BpTvfeku.cjs} +92 -8
  10. package/dist/qr-http-server-BpTvfeku.cjs.map +1 -0
  11. package/dist/{qr-http-server-BJJt3ush.js → qr-http-server-BsWlOA85.js} +92 -8
  12. package/dist/qr-http-server-BsWlOA85.js.map +1 -0
  13. package/dist/{qr-http-server-C1T4RNbq.cjs → qr-http-server-CrVKno9V.cjs} +92 -8
  14. package/dist/qr-http-server-CrVKno9V.cjs.map +1 -0
  15. package/dist/{qr-http-server-Cs93vEPH.js → qr-http-server-UBV2qUEd.js} +92 -8
  16. package/dist/qr-http-server-UBV2qUEd.js.map +1 -0
  17. package/dist/{relay-worker-Dppp2yZj.d.ts → relay-worker-DERQUao2.d.ts} +2 -2
  18. package/dist/{relay-worker-Dppp2yZj.d.ts.map → relay-worker-DERQUao2.d.ts.map} +1 -1
  19. package/dist/runtime-BKMMoeMj.d.ts +153 -0
  20. package/dist/runtime-BKMMoeMj.d.ts.map +1 -0
  21. package/dist/test-runner/bundle.js +72 -2
  22. package/dist/test-runner/bundle.js.map +1 -1
  23. package/dist/test-runner/cli.js +72 -2
  24. package/dist/test-runner/cli.js.map +1 -1
  25. package/dist/test-runner/config.d.ts +1 -1
  26. package/dist/test-runner/pool.d.ts +1 -1
  27. package/dist/test-runner/relay-worker.d.ts +1 -1
  28. package/dist/test-runner/rpc.d.ts +1 -1
  29. package/dist/test-runner/runtime.d.ts +2 -2
  30. package/dist/test-runner/runtime.js +296 -18
  31. package/dist/test-runner/runtime.js.map +1 -1
  32. package/dist/test-runner/task-graph.d.ts +1 -1
  33. package/dist/{tunnel-Cpn3mA4u.js → tunnel-B7U5k1xa.js} +2 -2
  34. package/dist/{tunnel-Cpn3mA4u.js.map → tunnel-B7U5k1xa.js.map} +1 -1
  35. package/dist/{tunnel-Dj8Kf2QS.cjs → tunnel-C-cgMnyY.cjs} +2 -2
  36. package/dist/{tunnel-Dj8Kf2QS.cjs.map → tunnel-C-cgMnyY.cjs.map} +1 -1
  37. package/dist/unplugin/index.cjs +1 -1
  38. package/dist/unplugin/index.js +1 -1
  39. package/dist/unplugin/tunnel.cjs +1 -1
  40. package/dist/unplugin/tunnel.js +1 -1
  41. package/package.json +1 -1
  42. package/dist/qr-http-server-BJJt3ush.js.map +0 -1
  43. package/dist/qr-http-server-BVS-HZjU.cjs.map +0 -1
  44. package/dist/qr-http-server-C1T4RNbq.cjs.map +0 -1
  45. package/dist/qr-http-server-Cs93vEPH.js.map +0 -1
  46. package/dist/runtime-C7uxh3Mf.d.ts +0 -62
  47. package/dist/runtime-C7uxh3Mf.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"bundle-KFs4t-wc.d.ts","names":[],"sources":["../src/test-runner/bundle.ts"],"mappings":";;AAqEA;;;;;AAmBA;;;;;AAuPA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA1QiB,aAAA;;;;;;;EAOf,cAAA;;;;;EAKA,UAAA;AAAA;;;;;UAOe,YAAA;EACf,IAAA;EACA,QAAA;AAAA;;;;;;;;;;;;;;;iBAqPoB,cAAA,CAAe,OAAA,UAAiB,IAAA,GAAO,aAAA,GAAgB,OAAA,CAAQ,YAAA"}
1
+ {"version":3,"file":"bundle-KFs4t-wc.d.ts","names":[],"sources":["../src/test-runner/bundle.ts"],"mappings":";;AAqEA;;;;;AAmBA;;;;;AA8TA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAjViB,aAAA;;;;;;;EAOf,cAAA;;;;;EAKA,UAAA;AAAA;;;;;UAOe,YAAA;EACf,IAAA;EACA,QAAA;AAAA;;;;;;;;;;;;;;;iBA4ToB,cAAA,CAAe,OAAA,UAAiB,IAAA,GAAO,aAAA,GAAgB,OAAA,CAAQ,YAAA"}
package/dist/mcp/cli.js CHANGED
@@ -298,12 +298,31 @@ async function discoverTestFiles(patterns, cwd) {
298
298
  *
299
299
  * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
300
300
  */
301
+ /** The SDK package name that mini-app test code imports from. */
302
+ const SDK_PACKAGE = "@apps-in-toss/web-framework";
303
+ /**
304
+ * Names the runtime installs as globals before invoking the user factory.
305
+ * The `vitest` virtual module re-exports each as a lazy getter that reads from
306
+ * `globalThis` at access time. Keep in sync with the globals installed in
307
+ * `runtime.ts#runTestModule`.
308
+ */
309
+ const VITEST_GLOBAL_NAMES = [
310
+ "describe",
311
+ "it",
312
+ "test",
313
+ "expect",
314
+ "beforeAll",
315
+ "afterAll",
316
+ "beforeEach",
317
+ "afterEach",
318
+ "vi"
319
+ ];
301
320
  /**
302
321
  * Matches the bare SDK package and any sub-path import
303
322
  * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).
304
323
  * Built from {@link SDK_PACKAGE} so the package name has a single source.
305
324
  */
306
- const SDK_IMPORT_FILTER = new RegExp(`^${"@apps-in-toss/web-framework".replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
325
+ const SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
307
326
  /**
308
327
  * esbuild plugin that intercepts SDK imports and redirects them to the
309
328
  * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.
@@ -344,6 +363,53 @@ module.exports = __proxy;
344
363
  };
345
364
  }
346
365
  /**
366
+ * esbuild plugin that intercepts `import … from 'vitest'` and replaces it with
367
+ * a virtual module that delegates every named import to `globalThis` at ACCESS
368
+ * time (not at bundle-evaluation time).
369
+ *
370
+ * The runtime installs `describe/it/test/expect/beforeAll/afterAll/beforeEach/
371
+ * afterEach/vi` as globals inside `runTestModule`, which runs AFTER the bundle
372
+ * IIFE is evaluated. A value-copy redirect (`export var describe =
373
+ * globalThis.describe`) would therefore capture `undefined` at evaluation time
374
+ * and the user's `describe(...)` calls would be no-ops — registering zero tests.
375
+ *
376
+ * The fix defers the lookup to call time using per-name **getter** exports.
377
+ * We emit a CommonJS module that:
378
+ * 1. sets `__esModule = true` so esbuild's `__toESM` interop maps each named
379
+ * import directly to a property access on the module (NOT wrapped under a
380
+ * `default` shim — which is what happens for a bare Proxy whose own-keys
381
+ * are empty, leaving every named import `undefined`);
382
+ * 2. defines each global name as a getter that reads `globalThis[name]` on
383
+ * every access. So `import { describe } from 'vitest'` compiles to
384
+ * `import_vitest.describe`, whose getter returns the real `describe` only
385
+ * when the factory calls it — after `runTestModule` installs the globals.
386
+ *
387
+ * A plain `module.exports = new Proxy(...)` does NOT work here: esbuild routes
388
+ * the virtual module through `__toESM`, which enumerates own-keys (none on an
389
+ * empty Proxy target) and therefore exposes zero named exports. Explicit getter
390
+ * properties give `__toESM` real keys to map while keeping access lazy.
391
+ */
392
+ function vitestRedirectPlugin() {
393
+ return {
394
+ name: "vitest-redirect",
395
+ setup(build) {
396
+ build.onResolve({ filter: /^vitest$/ }, () => ({
397
+ path: "vitest",
398
+ namespace: "vitest-redirect"
399
+ }));
400
+ build.onLoad({
401
+ filter: /^vitest$/,
402
+ namespace: "vitest-redirect"
403
+ }, () => {
404
+ return {
405
+ contents: `Object.defineProperty(exports, '__esModule', { value: true });\n${VITEST_GLOBAL_NAMES.map((name) => `Object.defineProperty(exports, ${JSON.stringify(name)}, { enumerable: true, get: function() { return globalThis[${JSON.stringify(name)}]; } });`).join("\n")}\n`,
406
+ loader: "js"
407
+ };
408
+ });
409
+ }
410
+ };
411
+ }
412
+ /**
347
413
  * esbuild plugin that transforms the user test file into a module that exports
348
414
  * an async `__userFactory` function. The factory defers the user's top-level
349
415
  * test registration code (describe/it/test calls) so it only runs when
@@ -490,7 +556,11 @@ async function bundleTestFile(absPath, opts) {
490
556
  platform: "browser",
491
557
  target: "es2022",
492
558
  write: false,
493
- plugins: [userFactoryPlugin(absPath), sdkRedirectPlugin()],
559
+ plugins: [
560
+ userFactoryPlugin(absPath),
561
+ vitestRedirectPlugin(),
562
+ sdkRedirectPlugin()
563
+ ],
494
564
  external: extraExternals,
495
565
  treeShaking: true,
496
566
  footer: { js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};` }
@@ -2767,6 +2837,9 @@ const en = {
2767
2837
  "dashboard.inspector.waiting": "Attach a page to enable the \"Open DevTools\" button",
2768
2838
  "inspector.error.noTarget": "No page attached. Attach a device and try again.",
2769
2839
  "inspector.error.relayDown": "Relay is not active. Start a relay session first.",
2840
+ "dashboard.watchdog.title": "Server has shut down",
2841
+ "dashboard.watchdog.body": "The MCP server has stopped. This session is no longer active. You may close this tab.",
2842
+ "dashboard.watchdog.close": "Close tab",
2770
2843
  "attach.title": "AIT Debug Session — QR Scan",
2771
2844
  "attach.deployment": "deployment: {label}",
2772
2845
  "attach.steps.section": "How to scan",
@@ -3002,6 +3075,9 @@ const tables = {
3002
3075
  "dashboard.inspector.waiting": "페이지를 attach하면 \"디버그 툴 열기\" 버튼이 표시됩니다",
3003
3076
  "inspector.error.noTarget": "연결된 페이지가 없습니다. 기기를 attach한 후 다시 시도하세요.",
3004
3077
  "inspector.error.relayDown": "relay가 활성화되지 않았습니다. start_debug로 relay를 기동하세요.",
3078
+ "dashboard.watchdog.title": "서버가 종료되었습니다",
3079
+ "dashboard.watchdog.body": "MCP server가 종료되어 이 세션이 더 이상 유효하지 않습니다. 이 탭을 닫아도 됩니다.",
3080
+ "dashboard.watchdog.close": "탭 닫기",
3005
3081
  "attach.title": "AIT 디버그 세션 — QR 스캔",
3006
3082
  "attach.deployment": "deployment: {label}",
3007
3083
  "attach.steps.section": "스캔 절차",
@@ -3549,7 +3625,7 @@ function buildLangSwitcher(path, existingParams, locale, s) {
3549
3625
  * - tunnel wssUrl은 "터널 연결됨" 상태 표시에서 UP/DOWN만 노출.
3550
3626
  * wssUrl 값 자체는 dashboard HTML에 넣지 않는다.
3551
3627
  */
3552
- function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new URLSearchParams(), devtoolsEntryUrl = null) {
3628
+ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new URLSearchParams(), devtoolsEntryUrl = null, sseRefreshIntervalMs = 9e4) {
3553
3629
  const s = resolveLocaleStrings(locale);
3554
3630
  const now = (/* @__PURE__ */ new Date()).toISOString();
3555
3631
  const tunnelStatus = state.tunnel.up ? s("dashboard.tunnel.up") : s("dashboard.tunnel.down");
@@ -3568,6 +3644,7 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
3568
3644
  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) => {
3569
3645
  return `<li><span class="page-id">${escapeHtml(p.id)}</span> <span class="page-url">${escapeHtml(p.url.slice(0, 120))}</span></li>`;
3570
3646
  }).join("\n") : `<li class="empty">${escapeHtml(s("dashboard.pages.empty"))}</li>`}</ul></section>`;
3647
+ const watchdogTimeoutMs = sseRefreshIntervalMs < 5e3 ? sseRefreshIntervalMs * 3 : sseRefreshIntervalMs * 2 + 3e4;
3571
3648
  const sseStrings = {
3572
3649
  tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
3573
3650
  tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
@@ -3578,7 +3655,11 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
3578
3655
  copiedLabel: JSON.stringify(s("dashboard.url.copied")),
3579
3656
  inspectorOpenLabel: JSON.stringify(s("dashboard.inspector.open")),
3580
3657
  inspectorWaitingLabel: JSON.stringify(s("dashboard.inspector.waiting")),
3581
- dashboardSurface: true
3658
+ dashboardSurface: true,
3659
+ watchdogTimeoutMs,
3660
+ watchdogTitle: JSON.stringify(s("dashboard.watchdog.title")),
3661
+ watchdogBody: JSON.stringify(s("dashboard.watchdog.body")),
3662
+ watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close"))
3582
3663
  };
3583
3664
  const langSwitcher = buildLangSwitcher(path, params, locale, s);
3584
3665
  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);
@@ -3604,6 +3685,10 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
3604
3685
  * - clipboard: navigator.clipboard.writeText → 실패/부재 시 textarea execCommand fallback.
3605
3686
  * - 피드백: 버튼 라벨이 COPIED_LABEL로 ~1.5초 전환 후 COPY_LABEL로 복귀.
3606
3687
  *
3688
+ * SSE watchdog (#681): SSE 주기 갱신이 `watchdogTimeoutMs` 동안 수신되지 않으면
3689
+ * server가 종료된 것으로 판단한다. `window.close()`를 시도하고, 닫히지 않으면
3690
+ * 폴백 안내 화면으로 교체해 stale QR이 계속 보이는 오해를 항상 방지한다.
3691
+ *
3607
3692
  * 문자열 인자는 빌드타임에 ko/en 테이블에서 가져와 JSON.stringify로 이미 escape됨.
3608
3693
  *
3609
3694
  * SECRET-HANDLING: URL 값을 console.log 등으로 출력하지 않는다.
@@ -3623,6 +3708,14 @@ function buildSseScript(strings) {
3623
3708
  var INSPECTOR_OPEN_LABEL = ${strings.inspectorOpenLabel};
3624
3709
  var INSPECTOR_WAITING_LABEL = ${strings.inspectorWaitingLabel};
3625
3710
 
3711
+ // ── SSE watchdog 상수 (#681) ──────────────────────────────────────────
3712
+ // server의 SSE refresh 주기(기본 90s)보다 넉넉히 큰 값 — 주기 × 2 + 30s 여유.
3713
+ // token-fill 패턴으로 server에서 계산해 주입한다.
3714
+ var WATCHDOG_TIMEOUT_MS = ${strings.watchdogTimeoutMs};
3715
+ var WATCHDOG_TITLE = ${strings.watchdogTitle};
3716
+ var WATCHDOG_BODY = ${strings.watchdogBody};
3717
+ var WATCHDOG_CLOSE_LABEL = ${strings.watchdogCloseLabel};
3718
+
3626
3719
  // ── 클립보드 복사 헬퍼 ────────────────────────────────────────────────
3627
3720
  function copyText(text) {
3628
3721
  if (navigator.clipboard && navigator.clipboard.writeText) {
@@ -3677,9 +3770,65 @@ function buildSseScript(strings) {
3677
3770
  }
3678
3771
  });
3679
3772
 
3773
+ // ── SSE watchdog (#681) ───────────────────────────────────────────────
3774
+ // 마지막으로 SSE 메시지를 수신한 시각. 스크립트 로드 시각으로 초기화한다.
3775
+ // (서버가 /events 연결 즉시 초기 상태를 push하므로 onmessage가 곧 갱신한다.)
3776
+ var lastSeen = Date.now();
3777
+ // watchdog이 한 번 발화하면 타이머를 clear해 중복 실행 방지.
3778
+ var watchdogFired = false;
3779
+ var watchdogTimer = null;
3780
+
3781
+ // 폴백 UI — window.close()가 무시되면 stale QR 대신 안내 화면을 표시한다.
3782
+ function showWatchdogFallback() {
3783
+ if (document.body) {
3784
+ document.body.innerHTML =
3785
+ '<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;' +
3786
+ 'height:100vh;font-family:sans-serif;text-align:center;padding:2rem;box-sizing:border-box;">' +
3787
+ '<p style="font-size:1.4rem;font-weight:bold;margin-bottom:1rem;">' + WATCHDOG_TITLE + '</p>' +
3788
+ '<p style="color:#555;margin-bottom:2rem;">' + WATCHDOG_BODY + '</p>' +
3789
+ '<button onclick="window.close()" ' +
3790
+ 'style="padding:0.6rem 1.4rem;font-size:1rem;cursor:pointer;border:1px solid #ccc;' +
3791
+ 'border-radius:6px;background:#f5f5f5;">' + WATCHDOG_CLOSE_LABEL + '</button>' +
3792
+ '</div>';
3793
+ }
3794
+ }
3795
+
3796
+ function fireWatchdog() {
3797
+ if (watchdogFired) return;
3798
+ watchdogFired = true;
3799
+ if (watchdogTimer) { clearInterval(watchdogTimer); watchdogTimer = null; }
3800
+ // 폴백 UI를 먼저 표시한다 — close가 성공하면 어차피 페이지가 닫혀 보이지 않는다.
3801
+ // close 실패(opener 없는 탭)에 대비해 stale QR 대신 안내 화면을 항상 보장한다.
3802
+ showWatchdogFallback();
3803
+ // window.close() 시도 — OS가 연 탭은 opener가 없어 무시될 수 있다.
3804
+ window.close();
3805
+ }
3806
+
3807
+ // 주기적으로 lastSeen을 검사 — WATCHDOG_TIMEOUT_MS 초과 시 발화.
3808
+ // 검사 간격: WATCHDOG_TIMEOUT_MS / 6, 단 최소 1s 이상이되 timeout 자체보다 크지 않게 한다.
3809
+ // (sseRefreshIntervalMs가 작을 때 최솟값 1s가 timeout을 초과하는 경우 방지 — 테스트 격리.)
3810
+ var checkIntervalMs = Math.min(
3811
+ Math.max(1000, Math.round(WATCHDOG_TIMEOUT_MS / 6)),
3812
+ WATCHDOG_TIMEOUT_MS,
3813
+ );
3814
+ watchdogTimer = setInterval(function () {
3815
+ if (watchdogFired) return;
3816
+ if (Date.now() - lastSeen > WATCHDOG_TIMEOUT_MS) {
3817
+ fireWatchdog();
3818
+ }
3819
+ }, checkIntervalMs);
3820
+
3680
3821
  // ── SSE 구독 ──────────────────────────────────────────────────────────
3681
3822
  var src = new EventSource('/events');
3823
+ src.onopen = function () {
3824
+ // 연결 복구 시 lastSeen 갱신 — 일시적 끊김이 watchdog을 발화시키지 않게.
3825
+ // watchdog이 이미 발화했으면 갱신하지 않는다(폴백 UI를 그대로 유지).
3826
+ if (!watchdogFired) { lastSeen = Date.now(); }
3827
+ };
3682
3828
  src.onmessage = function (e) {
3829
+ // SSE 메시지 수신 — lastSeen 갱신으로 watchdog 리셋.
3830
+ // watchdog 발화 후라면 갱신해도 타이머가 이미 clear됐으므로 효과 없음.
3831
+ if (!watchdogFired) { lastSeen = Date.now(); }
3683
3832
  try {
3684
3833
  var s = JSON.parse(e.data);
3685
3834
  // 터널 상태 갱신
@@ -3786,7 +3935,7 @@ function buildSseScript(strings) {
3786
3935
  * SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
3787
3936
  * inspectorStableUrl은 /inspector 안정 URL (127.0.0.1, 시크릿 없음) — 노출 가능.
3788
3937
  */
3789
- function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams(), mode, pagesAttached = false, inspectorStableUrl = null) {
3938
+ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams(), mode, pagesAttached = false, inspectorStableUrl = null, sseRefreshIntervalMs = 9e4) {
3790
3939
  const s = resolveLocaleStrings(locale);
3791
3940
  const langSwitcher = buildLangSwitcher(path, params, locale, s);
3792
3941
  const family = attachFamilyForMode(mode);
@@ -3794,6 +3943,7 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/a
3794
3943
  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>`;
3795
3944
  else inspectorSection = `<span class="inspector-hint" id="inspector-link">${escapeHtml(s("dashboard.inspector.waiting"))}</span>`;
3796
3945
  const filled = attachChromeByLocale[locale][family].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__MODE_LABEL__", buildModeLabel(mode, s)).replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl).replaceAll("__INSPECTOR_SECTION__", inspectorSection);
3946
+ const watchdogTimeoutMs = sseRefreshIntervalMs < 5e3 ? sseRefreshIntervalMs * 3 : sseRefreshIntervalMs * 2 + 3e4;
3797
3947
  const sseScript = buildSseScript({
3798
3948
  tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
3799
3949
  tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
@@ -3804,7 +3954,11 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/a
3804
3954
  copiedLabel: JSON.stringify(s("dashboard.url.copied")),
3805
3955
  inspectorOpenLabel: JSON.stringify(s("dashboard.inspector.open")),
3806
3956
  inspectorWaitingLabel: JSON.stringify(s("dashboard.inspector.waiting")),
3807
- dashboardSurface: false
3957
+ dashboardSurface: false,
3958
+ watchdogTimeoutMs,
3959
+ watchdogTitle: JSON.stringify(s("dashboard.watchdog.title")),
3960
+ watchdogBody: JSON.stringify(s("dashboard.watchdog.body")),
3961
+ watchdogCloseLabel: JSON.stringify(s("dashboard.watchdog.close"))
3808
3962
  });
3809
3963
  return filled.replace("</body>", `${sseScript}\n</body>`);
3810
3964
  }
@@ -3821,6 +3975,7 @@ async function startQrHttpServer(getDashboardState, options) {
3821
3975
  const { default: QRCode } = await import("qrcode");
3822
3976
  /** SSE 활성 연결 목록 — `notifyStateChange()` 시 전체 push. */
3823
3977
  const sseClients = [];
3978
+ const refreshIntervalMs = options?.sseRefreshIntervalMs ?? 9e4;
3824
3979
  /** SSE 연결 하나에 상태 이벤트를 flush한다. */
3825
3980
  function pushStateToClient(res, state) {
3826
3981
  const payload = JSON.stringify({
@@ -3859,7 +4014,7 @@ async function startQrHttpServer(getDashboardState, options) {
3859
4014
  if (!addr || typeof addr === "string") return null;
3860
4015
  return `http://127.0.0.1:${addr.port}/devtools/`;
3861
4016
  })();
3862
- const html = buildDashboardHtml(state, qrDataUrl, locale, path, params, devtoolsEntryUrl);
4017
+ const html = buildDashboardHtml(state, qrDataUrl, locale, path, params, devtoolsEntryUrl, refreshIntervalMs);
3863
4018
  res.writeHead(200, {
3864
4019
  "Content-Type": "text/html; charset=utf-8",
3865
4020
  "Cache-Control": "no-store"
@@ -3915,7 +4070,7 @@ async function startQrHttpServer(getDashboardState, options) {
3915
4070
  type: "image/png",
3916
4071
  errorCorrectionLevel: "M"
3917
4072
  }).then((dataUrl) => {
3918
- const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params, mode, pagesAttached, inspectorStableUrlForAttach);
4073
+ const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params, mode, pagesAttached, inspectorStableUrlForAttach, refreshIntervalMs);
3919
4074
  res.writeHead(200, {
3920
4075
  "Content-Type": "text/html; charset=utf-8",
3921
4076
  "Cache-Control": "no-store"
@@ -3998,7 +4153,6 @@ async function startQrHttpServer(getDashboardState, options) {
3998
4153
  pushStateToClient(client, state);
3999
4154
  } catch {}
4000
4155
  }
4001
- const refreshIntervalMs = options?.sseRefreshIntervalMs ?? 9e4;
4002
4156
  const refreshHandle = setInterval(() => {
4003
4157
  if (sseClients.length > 0 && getDashboardState) notifyStateChangeInternal();
4004
4158
  }, refreshIntervalMs).unref();
@@ -5411,7 +5565,7 @@ async function readMcpSdkVersion() {
5411
5565
  * some test environments that skip the build step).
5412
5566
  */
5413
5567
  function readDevtoolsVersion() {
5414
- return "0.1.113";
5568
+ return "0.1.115";
5415
5569
  }
5416
5570
  /**
5417
5571
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -6331,7 +6485,7 @@ function createDebugServer(deps) {
6331
6485
  }
6332
6486
  const server = new Server({
6333
6487
  name: "ait-debug",
6334
- version: "0.1.113"
6488
+ version: "0.1.115"
6335
6489
  }, { capabilities: { tools: { listChanged: true } } });
6336
6490
  server.setRequestHandler(ListToolsRequestSchema, () => {
6337
6491
  const conn = router.active;
@@ -8231,7 +8385,7 @@ function createDevServer(deps = {}) {
8231
8385
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
8232
8386
  const server = new Server({
8233
8387
  name: "ait-devtools",
8234
- version: "0.1.113"
8388
+ version: "0.1.115"
8235
8389
  }, { capabilities: { tools: {} } });
8236
8390
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
8237
8391
  server.setRequestHandler(CallToolRequestSchema, async (request) => {