@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 +364 -121
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +24 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +2 -2
- package/package.json +1 -1
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
|
-
*
|
|
1014
|
-
*
|
|
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
|
-
* -
|
|
1018
|
-
*
|
|
1019
|
-
* -
|
|
1020
|
-
*
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
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(
|
|
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
|
|
1035
|
-
const
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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:
|
|
1106
|
-
|
|
1107
|
-
|
|
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()`
|
|
1135
|
-
*
|
|
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
|
-
|
|
1164
|
-
|
|
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)
|
|
1388
|
+
if (nb) {
|
|
1389
|
+
navBarHeight = nb.getBoundingClientRect().height;
|
|
1390
|
+
navBarHeightSource = 'dom-.ait-navbar';
|
|
1391
|
+
}
|
|
1171
1392
|
} catch(_) {}
|
|
1172
|
-
|
|
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
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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) => {
|