@ait-co/devtools 0.1.38 → 0.1.40

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
@@ -649,6 +649,173 @@ async function launchChromium(options = {}) {
649
649
  };
650
650
  }
651
651
  //#endregion
652
+ //#region src/mcp/qr-http-server.ts
653
+ /**
654
+ * 로컬 HTTP 서버를 127.0.0.1 random port(또는 `AIT_DEBUG_HTTP_PORT` env)로 시작한다.
655
+ * MCP debug server 생애주기에 묶어 사용 — `runDebugServer` shutdown 시 `close()`로 정리.
656
+ */
657
+ async function startQrHttpServer() {
658
+ const { default: QRCode } = await import("qrcode");
659
+ const server = createServer((req, res) => {
660
+ const [path, query = ""] = (req.url ?? "/").split("?", 2);
661
+ const params = new URLSearchParams(query ?? "");
662
+ if (path === "/attach") {
663
+ const encodedU = params.get("u") ?? "";
664
+ let attachUrl;
665
+ try {
666
+ attachUrl = decodeURIComponent(encodedU);
667
+ } catch {
668
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
669
+ res.end("잘못된 u 파라미터입니다.");
670
+ return;
671
+ }
672
+ let deploymentIdLabel = "attach";
673
+ try {
674
+ const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);
675
+ if (dpMatch?.[1]) deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);
676
+ } catch {}
677
+ QRCode.toDataURL(attachUrl, {
678
+ type: "image/png",
679
+ errorCorrectionLevel: "M"
680
+ }).then((dataUrl) => {
681
+ const html = buildAttachHtml(dataUrl, deploymentIdLabel.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`), attachUrl.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`));
682
+ res.writeHead(200, {
683
+ "Content-Type": "text/html; charset=utf-8",
684
+ "Cache-Control": "no-store"
685
+ });
686
+ res.end(html);
687
+ }).catch(() => {
688
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
689
+ res.end("QR 생성에 실패했습니다.");
690
+ });
691
+ return;
692
+ }
693
+ if (path === "/qr.png") {
694
+ const encodedU = params.get("u") ?? "";
695
+ let attachUrl;
696
+ try {
697
+ attachUrl = decodeURIComponent(encodedU);
698
+ } catch {
699
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
700
+ res.end("잘못된 u 파라미터입니다.");
701
+ return;
702
+ }
703
+ QRCode.toBuffer(attachUrl, {
704
+ type: "png",
705
+ errorCorrectionLevel: "M"
706
+ }).then((buf) => {
707
+ res.writeHead(200, {
708
+ "Content-Type": "image/png",
709
+ "Cache-Control": "no-store",
710
+ "Content-Length": String(buf.length)
711
+ });
712
+ res.end(buf);
713
+ }).catch(() => {
714
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
715
+ res.end("QR PNG 생성에 실패했습니다.");
716
+ });
717
+ return;
718
+ }
719
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
720
+ res.end("Not Found");
721
+ });
722
+ const listenPort = Number(process.env.AIT_DEBUG_HTTP_PORT ?? 0);
723
+ await new Promise((resolve, reject) => {
724
+ server.listen(listenPort, "127.0.0.1", () => resolve());
725
+ server.once("error", reject);
726
+ });
727
+ const address = server.address();
728
+ if (!address || typeof address === "string") throw new Error("qr-http-server: server.address()가 예상하지 못한 형태입니다.");
729
+ const port = address.port;
730
+ return {
731
+ port,
732
+ buildAttachPageUrl(attachUrl) {
733
+ return `http://127.0.0.1:${port}/attach?u=${encodeURIComponent(attachUrl)}`;
734
+ },
735
+ close() {
736
+ return new Promise((resolve, reject) => {
737
+ server.close((err) => err ? reject(err) : resolve());
738
+ });
739
+ }
740
+ };
741
+ }
742
+ /**
743
+ * QR 스캔 페이지 HTML 본문.
744
+ * dark theme, inline style, 외부 fetch 없음.
745
+ */
746
+ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl) {
747
+ return `<!DOCTYPE html>
748
+ <html lang="ko">
749
+ <head>
750
+ <meta charset="utf-8" />
751
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
752
+ <title>AIT 디버그 세션 — QR 스캔</title>
753
+ <style>
754
+ *, *::before, *::after { box-sizing: border-box; }
755
+ body {
756
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
757
+ background: #0d1117; color: #c9d1d9;
758
+ display: flex; flex-direction: column; align-items: center;
759
+ min-height: 100vh; margin: 0; padding: 2rem 1rem;
760
+ gap: 1.5rem;
761
+ }
762
+ h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
763
+ .label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
764
+ img.qr {
765
+ width: min(90vw, 360px); height: auto;
766
+ image-rendering: pixelated;
767
+ background: #fff; padding: 1rem; border-radius: 12px;
768
+ }
769
+ section { width: 100%; max-width: 480px; }
770
+ h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
771
+ ol, ul { margin: 0; padding-left: 1.25rem; }
772
+ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
773
+ .url-box {
774
+ font-family: monospace; font-size: 0.72rem;
775
+ word-break: break-all; opacity: 0.4;
776
+ background: #161b22; padding: 0.75rem 1rem;
777
+ border-radius: 6px; border: 1px solid #30363d;
778
+ }
779
+ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
780
+ </style>
781
+ </head>
782
+ <body>
783
+ <h1>AIT 디버그 세션 — QR 스캔</h1>
784
+ <p class="label">deployment: ${safeLabel}</p>
785
+ <img class="qr" src="${qrDataUrl}" alt="attach QR" />
786
+
787
+ <section>
788
+ <h2>스캔 절차</h2>
789
+ <ol>
790
+ <li>토스 앱을 실행하세요.</li>
791
+ <li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li>
792
+ <li>팝업이 뜨면 <strong>"토스로 열기"</strong>를 탭하세요.</li>
793
+ <li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li>
794
+ </ol>
795
+ </section>
796
+
797
+ <hr />
798
+
799
+ <section>
800
+ <h2>진단 체크리스트</h2>
801
+ <ul>
802
+ <li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li>
803
+ <li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li>
804
+ <li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li>
805
+ <li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li>
806
+ </ul>
807
+ </section>
808
+
809
+ <hr />
810
+
811
+ <section>
812
+ <h2>URL (fallback)</h2>
813
+ <p class="url-box">${safeAttachUrl}</p>
814
+ </section>
815
+ </body>
816
+ </html>`;
817
+ }
818
+ //#endregion
652
819
  //#region src/mcp/deeplink.ts
653
820
  /**
654
821
  * Build a self-attaching dogfood deep link.
@@ -1009,102 +1176,136 @@ function canOpenBrowser() {
1009
1176
  if (platform === "linux") return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);
1010
1177
  return false;
1011
1178
  }
1179
+ /** platform별 browser open 명령 후보 목록 — 앞에서부터 순차 시도. */
1180
+ function getBrowserCandidates(httpUrl) {
1181
+ const platform = process.platform;
1182
+ if (platform === "darwin") return [
1183
+ {
1184
+ cmd: "open",
1185
+ args: [httpUrl]
1186
+ },
1187
+ {
1188
+ cmd: "open",
1189
+ args: [
1190
+ "-a",
1191
+ "Safari",
1192
+ httpUrl
1193
+ ]
1194
+ },
1195
+ {
1196
+ cmd: "open",
1197
+ args: [
1198
+ "-a",
1199
+ "Google Chrome",
1200
+ httpUrl
1201
+ ]
1202
+ },
1203
+ {
1204
+ cmd: "open",
1205
+ args: [
1206
+ "-a",
1207
+ "Firefox",
1208
+ httpUrl
1209
+ ]
1210
+ }
1211
+ ];
1212
+ if (platform === "win32") return [{
1213
+ cmd: "cmd",
1214
+ args: [
1215
+ "/c",
1216
+ "start",
1217
+ "",
1218
+ httpUrl
1219
+ ]
1220
+ }, {
1221
+ cmd: "rundll32",
1222
+ args: ["url.dll,FileProtocolHandler", httpUrl]
1223
+ }];
1224
+ return [
1225
+ {
1226
+ cmd: "xdg-open",
1227
+ args: [httpUrl]
1228
+ },
1229
+ {
1230
+ cmd: "sensible-browser",
1231
+ args: [httpUrl]
1232
+ },
1233
+ {
1234
+ cmd: "x-www-browser",
1235
+ args: [httpUrl]
1236
+ },
1237
+ {
1238
+ cmd: "firefox",
1239
+ args: [httpUrl]
1240
+ },
1241
+ {
1242
+ cmd: "google-chrome",
1243
+ args: [httpUrl]
1244
+ },
1245
+ {
1246
+ cmd: "chromium",
1247
+ args: [httpUrl]
1248
+ }
1249
+ ];
1250
+ }
1251
+ /** stderr에서 at= TOTP 코드 값을 redact한다. */
1252
+ function redactSecrets(text) {
1253
+ return text.replace(/\bat=([^&\s"']+)/g, "at=<redacted>");
1254
+ }
1255
+ /** spawnSync exit 0이어도 stderr에 launch 실패 시그널이 있으면 실패로 판단한다. */
1256
+ const LAUNCH_FAILURE_PATTERNS = [
1257
+ /LSOpenURLsWithRole\(\) failed/,
1258
+ /kLSApplicationNotFoundErr/,
1259
+ /No application/,
1260
+ /Unable to find application/,
1261
+ /xdg-open: not found/,
1262
+ /command not found/
1263
+ ];
1264
+ function isLaunchFailureStderr(stderr) {
1265
+ return LAUNCH_FAILURE_PATTERNS.some((p) => p.test(stderr));
1266
+ }
1012
1267
  /**
1013
- * Writes the attach URL as a QR PNG + a wrapper HTML page to the OS temp
1014
- * directory, then opens the HTML in the OS default browser.
1268
+ * 로컬 HTTP 서버 URL(`http://127.0.0.1:<port>/attach?u=...`)을 OS 기본 브라우저로 연다.
1269
+ *
1270
+ * platform별 fallback chain으로 시도하며, 모두 실패해도 `opened: false` + `httpUrl`을
1271
+ * 반환해 사용자가 직접 브라우저에 붙여넣을 수 있게 한다.
1015
1272
  *
1016
1273
  * SECRET-HANDLING:
1017
- * - File names are derived from a short timestamp, NOT from the attach URL or
1018
- * any token/code value. The `at=` code is NOT in the file name.
1019
- * - The attach URL (which may carry `at=`) is embedded inside the HTML page
1020
- * body that is the intended delivery channel for the QR.
1021
- * - This function must NOT write the attach URL, deploymentId, or any
1022
- * TOTP code to stdout, stderr, or any log.
1023
- *
1024
- * @param attachUrl - The deep link to encode as a QR. May contain `at=<code>`.
1025
- * @param deploymentId - Optional human-readable label for the HTML page (e.g. UUID substring).
1026
- * Must NOT be derived from the `at=` code value.
1027
- * @returns `OpenQrInBrowserResult` — never throws (errors are returned in `.error`).
1274
+ * - tmp 파일을 만들지 않는다 (HTML/PNG는 HTTP 서버가 메모리에서 응답).
1275
+ * - httpUrl/pngUrl은 127.0.0.1 로컬 전용.
1276
+ * - stderr 캡처 결과에서 at= 코드 값을 redact한 stderrSummary에 포함.
1277
+ * - attachUrl, deploymentId, TOTP 코드를 stdout/stderr/로그에 직접 출력 금지.
1278
+ *
1279
+ * @param httpUrl - `http://127.0.0.1:<port>/attach?u=<encoded>` HTTP URL.
1280
+ * @param pngUrl - `http://127.0.0.1:<port>/qr.png?u=<encoded>` PNG fallback URL.
1028
1281
  */
1029
- async function openQrInBrowser(attachUrl, deploymentId) {
1030
- const { tmpdir } = await import("node:os");
1031
- const { writeFileSync } = await import("node:fs");
1032
- const { join } = await import("node:path");
1282
+ async function openQrInBrowser(httpUrl, pngUrl) {
1033
1283
  const { spawnSync } = await import("node:child_process");
1034
- const { default: QRCode } = await import("qrcode");
1035
- const stamp = Date.now();
1036
- const pngPath = join(tmpdir(), `ait-qr-${stamp}.png`);
1037
- const htmlPath = join(tmpdir(), `ait-qr-${stamp}.html`);
1038
- try {
1039
- await QRCode.toFile(pngPath, attachUrl, {
1040
- type: "png",
1041
- errorCorrectionLevel: "M"
1284
+ const candidates = getBrowserCandidates(httpUrl);
1285
+ const stderrLines = [];
1286
+ for (const { cmd, args } of candidates) {
1287
+ const result = spawnSync(cmd, args, {
1288
+ encoding: "utf8",
1289
+ timeout: 5e3
1042
1290
  });
1043
- } catch (err) {
1044
- return {
1045
- opened: false,
1046
- htmlPath,
1047
- pngPath,
1048
- error: `QR PNG write failed: ${err instanceof Error ? err.message : String(err)}`
1049
- };
1050
- }
1051
- const htmlContent = `<!DOCTYPE html>
1052
- <html lang="ko">
1053
- <head>
1054
- <meta charset="utf-8" />
1055
- <meta name="viewport" content="width=device-width, initial-scale=1" />
1056
- <title>AIT Debug — QR</title>
1057
- <style>
1058
- body { font-family: monospace; background: #111; color: #eee; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; gap: 1.5rem; padding: 2rem; box-sizing: border-box; }
1059
- img { width: min(90vw, 400px); height: auto; image-rendering: pixelated; background: #fff; padding: 1rem; border-radius: 8px; }
1060
- .label { font-size: 0.85rem; opacity: 0.6; }
1061
- .url { font-size: 0.75rem; word-break: break-all; max-width: 60ch; opacity: 0.5; }
1062
- </style>
1063
- </head>
1064
- <body>
1065
- <img src="${pngPath}" alt="QR code" />
1066
- <p class="label">deployment: ${deploymentId ? deploymentId.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`) : "attach"}</p>
1067
- </body>
1068
- </html>`;
1069
- try {
1070
- writeFileSync(htmlPath, htmlContent, "utf8");
1071
- } catch (err) {
1072
- return {
1073
- opened: false,
1074
- htmlPath,
1075
- pngPath,
1076
- error: `HTML write failed: ${err instanceof Error ? err.message : String(err)}`
1291
+ if (result.error) {
1292
+ stderrLines.push(`${cmd}: ${result.error.message}`);
1293
+ continue;
1294
+ }
1295
+ const stderr = typeof result.stderr === "string" ? result.stderr : "";
1296
+ if (stderr) stderrLines.push(`${cmd}: ${redactSecrets(stderr.trim())}`);
1297
+ if (result.status === 0 && !isLaunchFailureStderr(stderr)) return {
1298
+ opened: true,
1299
+ httpUrl,
1300
+ pngUrl
1077
1301
  };
1078
1302
  }
1079
- const platform = process.platform;
1080
- let openCmd;
1081
- let openArgs;
1082
- if (platform === "darwin") {
1083
- openCmd = "open";
1084
- openArgs = [htmlPath];
1085
- } else if (platform === "win32") {
1086
- openCmd = "cmd";
1087
- openArgs = [
1088
- "/c",
1089
- "start",
1090
- "",
1091
- htmlPath
1092
- ];
1093
- } else {
1094
- openCmd = "xdg-open";
1095
- openArgs = [htmlPath];
1096
- }
1097
- const spawnResult = spawnSync(openCmd, openArgs, { timeout: 5e3 });
1098
- if (spawnResult.error) return {
1099
- opened: false,
1100
- htmlPath,
1101
- pngPath,
1102
- error: `Browser open failed (${openCmd}): ${spawnResult.error.message}`
1103
- };
1104
1303
  return {
1105
- opened: true,
1106
- htmlPath,
1107
- pngPath
1304
+ opened: false,
1305
+ httpUrl,
1306
+ pngUrl,
1307
+ error: "모든 브라우저 실행 후보가 실패했습니다.",
1308
+ stderrSummary: stderrLines.length > 0 ? stderrLines.join("\n") : void 0
1108
1309
  };
1109
1310
  }
1110
1311
  /** Returns the DOM tree of the attached page (`DOM.getDocument`). */
@@ -1131,8 +1332,14 @@ async function takeScreenshot(connection) {
1131
1332
  * The JS probe injected via `Runtime.evaluate`. It reads:
1132
1333
  * 1. `env(safe-area-inset-*)` via a temporary element with padding set to
1133
1334
  * those CSS env vars, then `getComputedStyle`.
1134
- * 2. `SafeAreaInsets.get()` if the native SDK object is available.
1135
- * 3. nav bar geometry (first `.ait-navbar` element height, if present).
1335
+ * 2. `window.__sdk.SafeAreaInsets.get()` (1st priority) or
1336
+ * `window.__sdk.getSafeAreaInsets()` (2nd priority) both surfaces
1337
+ * confirmed live on iPhone 15 Pro relay. `window.__sdk` is only present
1338
+ * in dogfood (__DEBUG_BUILD__) bundles; outside those it is undefined.
1339
+ * If both paths fail the result carries `sdkInsetsError` explaining why.
1340
+ * 3. nav bar geometry: the SDK does not expose navBar height as a standalone
1341
+ * API — `.ait-navbar` DOM height is read as a cross-check, and
1342
+ * `navBarHeightSource` records where it came from.
1136
1343
  * 4. `innerWidth`, `innerHeight`, `devicePixelRatio`, `navigator.userAgent`.
1137
1344
  *
1138
1345
  * Returns a plain JSON-serialisable object so `returnByValue: true` works.
@@ -1159,25 +1366,42 @@ const SAFE_AREA_PROBE_EXPRESSION = `
1159
1366
  };
1160
1367
  document.documentElement.removeChild(el);
1161
1368
  var sdkInsets = null;
1369
+ var sdkInsetsError = undefined;
1162
1370
  try {
1163
- if (typeof SafeAreaInsets !== 'undefined' && SafeAreaInsets && typeof SafeAreaInsets.get === 'function') {
1164
- sdkInsets = SafeAreaInsets.get();
1371
+ var sdk = window.__sdk;
1372
+ if (sdk && sdk.SafeAreaInsets && typeof sdk.SafeAreaInsets.get === 'function') {
1373
+ sdkInsets = sdk.SafeAreaInsets.get();
1374
+ } else if (sdk && typeof sdk.getSafeAreaInsets === 'function') {
1375
+ sdkInsets = sdk.getSafeAreaInsets();
1376
+ } else if (!sdk) {
1377
+ sdkInsetsError = 'window.__sdk not available (non-dogfood bundle)';
1378
+ } else {
1379
+ sdkInsetsError = 'neither SafeAreaInsets.get nor getSafeAreaInsets found on window.__sdk';
1165
1380
  }
1166
- } catch(_) {}
1381
+ } catch(e) {
1382
+ sdkInsetsError = String(e && e.message || e);
1383
+ }
1167
1384
  var navBarHeight = null;
1385
+ var navBarHeightSource = 'not-exposed-by-sdk';
1168
1386
  try {
1169
1387
  var nb = document.querySelector('.ait-navbar');
1170
- if (nb) navBarHeight = nb.getBoundingClientRect().height;
1388
+ if (nb) {
1389
+ navBarHeight = nb.getBoundingClientRect().height;
1390
+ navBarHeightSource = 'dom-.ait-navbar';
1391
+ }
1171
1392
  } catch(_) {}
1172
- return JSON.stringify({
1393
+ var result = {
1173
1394
  cssEnv: cssEnv,
1174
1395
  sdkInsets: sdkInsets,
1175
1396
  navBarHeight: navBarHeight,
1397
+ navBarHeightSource: navBarHeightSource,
1176
1398
  innerWidth: window.innerWidth,
1177
1399
  innerHeight: window.innerHeight,
1178
1400
  devicePixelRatio: window.devicePixelRatio,
1179
1401
  userAgent: navigator.userAgent
1180
- });
1402
+ };
1403
+ if (sdkInsetsError !== undefined) result.sdkInsetsError = sdkInsetsError;
1404
+ return JSON.stringify(result);
1181
1405
  })()
1182
1406
  `.trim();
1183
1407
  /**
@@ -1209,19 +1433,30 @@ function normalizeSafeAreaResult(rawValue) {
1209
1433
  left: typeof r.left === "number" ? r.left : 0
1210
1434
  };
1211
1435
  }
1436
+ const cssEnv = requireInsets("cssEnv") ?? {
1437
+ top: 0,
1438
+ right: 0,
1439
+ bottom: 0,
1440
+ left: 0
1441
+ };
1442
+ const sdkInsets = requireInsets("sdkInsets");
1443
+ const sdkInsetsError = typeof obj.sdkInsetsError === "string" ? obj.sdkInsetsError : void 0;
1444
+ const navBarHeight = typeof obj.navBarHeight === "number" ? obj.navBarHeight : null;
1445
+ const navBarHeightSource = typeof obj.navBarHeightSource === "string" ? obj.navBarHeightSource : "not-exposed-by-sdk";
1446
+ const innerWidth = typeof obj.innerWidth === "number" ? obj.innerWidth : 0;
1447
+ const innerHeight = typeof obj.innerHeight === "number" ? obj.innerHeight : 0;
1448
+ const devicePixelRatio = typeof obj.devicePixelRatio === "number" ? obj.devicePixelRatio : 1;
1449
+ const userAgent = typeof obj.userAgent === "string" ? obj.userAgent : "";
1212
1450
  return {
1213
- cssEnv: requireInsets("cssEnv") ?? {
1214
- top: 0,
1215
- right: 0,
1216
- bottom: 0,
1217
- left: 0
1218
- },
1219
- sdkInsets: requireInsets("sdkInsets"),
1220
- navBarHeight: typeof obj.navBarHeight === "number" ? obj.navBarHeight : null,
1221
- innerWidth: typeof obj.innerWidth === "number" ? obj.innerWidth : 0,
1222
- innerHeight: typeof obj.innerHeight === "number" ? obj.innerHeight : 0,
1223
- devicePixelRatio: typeof obj.devicePixelRatio === "number" ? obj.devicePixelRatio : 1,
1224
- userAgent: typeof obj.userAgent === "string" ? obj.userAgent : ""
1451
+ cssEnv,
1452
+ sdkInsets,
1453
+ ...sdkInsetsError !== void 0 ? { sdkInsetsError } : {},
1454
+ navBarHeight,
1455
+ navBarHeightSource,
1456
+ innerWidth,
1457
+ innerHeight,
1458
+ devicePixelRatio,
1459
+ userAgent
1225
1460
  };
1226
1461
  }
1227
1462
  /**
@@ -1623,10 +1858,10 @@ async function printAttachBanner(input) {
1623
1858
  * naturally via `enableDomains`). The tier only controls visibility.
1624
1859
  */
1625
1860
  function createDebugServer(deps) {
1626
- const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4 } = deps;
1861
+ const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer } = deps;
1627
1862
  const server = new Server({
1628
1863
  name: "ait-debug",
1629
- version: "0.1.38"
1864
+ version: "0.1.40"
1630
1865
  }, { capabilities: { tools: { listChanged: true } } });
1631
1866
  server.setRequestHandler(ListToolsRequestSchema, () => {
1632
1867
  return { tools: connection.listTargets().length > 0 ? DEBUG_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) : DEBUG_TOOL_DEFINITIONS.filter((tool) => BOOTSTRAP_TOOL_NAMES.has(tool.name)).map((tool) => ({ ...tool })) };
@@ -1666,15 +1901,10 @@ function createDebugServer(deps) {
1666
1901
  const { attachUrl, relayUrl, authorityWarning } = buildAttachUrl(schemeUrl, getTunnelStatus());
1667
1902
  const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
1668
1903
  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).";
1669
- if (openInBrowser && canOpenBrowser()) {
1670
- let deploymentIdLabel;
1671
- try {
1672
- const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);
1673
- if (dpMatch?.[1]) deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);
1674
- } catch {}
1675
- const browserResult = await openQrInBrowser(attachUrl, deploymentIdLabel);
1904
+ if (openInBrowser && canOpenBrowser() && qrHttpServer) {
1905
+ const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
1676
1906
  if (browserResult.opened) {
1677
- const shortText = `${warningPrefix}${header}\n${JSON.stringify({ relayUrl }, null, 2)}\n\nブラウザにQRを表示しました。\nQR画像: ${browserResult.pngPath}\nHTMLページ: ${browserResult.htmlPath}\n\n브라우저에 QR을 띄웠습니다. 스마트폰 카메라로 스캔하세요.\nPNG: ${browserResult.pngPath}`;
1907
+ const shortText = `${warningPrefix}${header}\n${JSON.stringify({ relayUrl }, null, 2)}\n\n브라우저에서 QR을 열었습니다. 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`;
1678
1908
  if (!waitForAttach) return { content: [{
1679
1909
  type: "text",
1680
1910
  text: shortText
@@ -1701,7 +1931,8 @@ function createDebugServer(deps) {
1701
1931
  text: `${shortText}\n\n${JSON.stringify(pagesResult, null, 2)}`
1702
1932
  }] };
1703
1933
  }
1704
- const fallbackNote = `(브라우저 열기 실패: ${browserResult.error ?? "unknown"} — 텍스트 QR로 대체)\n`;
1934
+ const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
1935
+ const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
1705
1936
  const qr = await renderQr(attachUrl);
1706
1937
  const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
1707
1938
  attachUrl,
@@ -1941,10 +2172,21 @@ async function runDebugServer(options = {}) {
1941
2172
  `);
1942
2173
  });
1943
2174
  const connection = new ChiiCdpConnection({ relayBaseUrl: relay.baseUrl });
2175
+ const aitSource = new ChiiAitSource(connection);
2176
+ let qrServer;
2177
+ startQrHttpServer().then((s) => {
2178
+ qrServer = s;
2179
+ }, (err) => {
2180
+ const message = err instanceof Error ? err.message : String(err);
2181
+ process.stderr.write(`[ait-debug] QR HTTP 서버 시작 실패 (text QR fallback 사용): ${message}\n`);
2182
+ });
1944
2183
  const server = createDebugServer({
1945
2184
  connection,
1946
- aitSource: new ChiiAitSource(connection),
1947
- getTunnelStatus: () => tunnelStatus
2185
+ aitSource,
2186
+ getTunnelStatus: () => tunnelStatus,
2187
+ get qrHttpServer() {
2188
+ return qrServer;
2189
+ }
1948
2190
  });
1949
2191
  const transport = new StdioServerTransport();
1950
2192
  let closed = false;
@@ -1957,6 +2199,7 @@ async function runDebugServer(options = {}) {
1957
2199
  tunnel?.stop();
1958
2200
  relay.close();
1959
2201
  server.close();
2202
+ qrServer?.close();
1960
2203
  };
1961
2204
  process.once("SIGINT", shutdown);
1962
2205
  process.once("SIGTERM", shutdown);
@@ -2169,7 +2412,7 @@ function createDevServer(deps = {}) {
2169
2412
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
2170
2413
  const server = new Server({
2171
2414
  name: "ait-devtools",
2172
- version: "0.1.38"
2415
+ version: "0.1.40"
2173
2416
  }, { capabilities: { tools: {} } });
2174
2417
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
2175
2418
  server.setRequestHandler(CallToolRequestSchema, async (request) => {