@ait-co/devtools 0.1.38 → 0.1.39
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 +310 -101
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- 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`). */
|
|
@@ -1623,10 +1824,10 @@ async function printAttachBanner(input) {
|
|
|
1623
1824
|
* naturally via `enableDomains`). The tier only controls visibility.
|
|
1624
1825
|
*/
|
|
1625
1826
|
function createDebugServer(deps) {
|
|
1626
|
-
const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4 } = deps;
|
|
1827
|
+
const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer } = deps;
|
|
1627
1828
|
const server = new Server({
|
|
1628
1829
|
name: "ait-debug",
|
|
1629
|
-
version: "0.1.
|
|
1830
|
+
version: "0.1.39"
|
|
1630
1831
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
1631
1832
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
1632
1833
|
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 +1867,10 @@ function createDebugServer(deps) {
|
|
|
1666
1867
|
const { attachUrl, relayUrl, authorityWarning } = buildAttachUrl(schemeUrl, getTunnelStatus());
|
|
1667
1868
|
const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
|
|
1668
1869
|
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);
|
|
1870
|
+
if (openInBrowser && canOpenBrowser() && qrHttpServer) {
|
|
1871
|
+
const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
|
|
1676
1872
|
if (browserResult.opened) {
|
|
1677
|
-
const shortText = `${warningPrefix}${header}\n${JSON.stringify({ relayUrl }, null, 2)}\n\n
|
|
1873
|
+
const shortText = `${warningPrefix}${header}\n${JSON.stringify({ relayUrl }, null, 2)}\n\n브라우저에서 QR을 열었습니다. 폰 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`;
|
|
1678
1874
|
if (!waitForAttach) return { content: [{
|
|
1679
1875
|
type: "text",
|
|
1680
1876
|
text: shortText
|
|
@@ -1701,7 +1897,8 @@ function createDebugServer(deps) {
|
|
|
1701
1897
|
text: `${shortText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
1702
1898
|
}] };
|
|
1703
1899
|
}
|
|
1704
|
-
const
|
|
1900
|
+
const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
|
|
1901
|
+
const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
|
|
1705
1902
|
const qr = await renderQr(attachUrl);
|
|
1706
1903
|
const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
|
|
1707
1904
|
attachUrl,
|
|
@@ -1941,10 +2138,21 @@ async function runDebugServer(options = {}) {
|
|
|
1941
2138
|
`);
|
|
1942
2139
|
});
|
|
1943
2140
|
const connection = new ChiiCdpConnection({ relayBaseUrl: relay.baseUrl });
|
|
2141
|
+
const aitSource = new ChiiAitSource(connection);
|
|
2142
|
+
let qrServer;
|
|
2143
|
+
startQrHttpServer().then((s) => {
|
|
2144
|
+
qrServer = s;
|
|
2145
|
+
}, (err) => {
|
|
2146
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2147
|
+
process.stderr.write(`[ait-debug] QR HTTP 서버 시작 실패 (text QR fallback 사용): ${message}\n`);
|
|
2148
|
+
});
|
|
1944
2149
|
const server = createDebugServer({
|
|
1945
2150
|
connection,
|
|
1946
|
-
aitSource
|
|
1947
|
-
getTunnelStatus: () => tunnelStatus
|
|
2151
|
+
aitSource,
|
|
2152
|
+
getTunnelStatus: () => tunnelStatus,
|
|
2153
|
+
get qrHttpServer() {
|
|
2154
|
+
return qrServer;
|
|
2155
|
+
}
|
|
1948
2156
|
});
|
|
1949
2157
|
const transport = new StdioServerTransport();
|
|
1950
2158
|
let closed = false;
|
|
@@ -1957,6 +2165,7 @@ async function runDebugServer(options = {}) {
|
|
|
1957
2165
|
tunnel?.stop();
|
|
1958
2166
|
relay.close();
|
|
1959
2167
|
server.close();
|
|
2168
|
+
qrServer?.close();
|
|
1960
2169
|
};
|
|
1961
2170
|
process.once("SIGINT", shutdown);
|
|
1962
2171
|
process.once("SIGTERM", shutdown);
|
|
@@ -2169,7 +2378,7 @@ function createDevServer(deps = {}) {
|
|
|
2169
2378
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
2170
2379
|
const server = new Server({
|
|
2171
2380
|
name: "ait-devtools",
|
|
2172
|
-
version: "0.1.
|
|
2381
|
+
version: "0.1.39"
|
|
2173
2382
|
}, { capabilities: { tools: {} } });
|
|
2174
2383
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
2175
2384
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|