@ait-co/devtools 0.1.80 → 0.1.84
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/{chii-relay-DSVG4Ui1.js → chii-relay-BASitNMw.js} +1 -1
- package/dist/{chii-relay-DSVG4Ui1.js.map → chii-relay-BASitNMw.js.map} +1 -1
- package/dist/{chii-relay-BcnVJBqm.cjs → chii-relay-BzUf0LH3.cjs} +1 -1
- package/dist/{chii-relay-BcnVJBqm.cjs.map → chii-relay-BzUf0LH3.cjs.map} +1 -1
- package/dist/{deeplink-B-94XmWA.js → deeplink-BpO9qc-D.js} +5 -3
- package/dist/{deeplink-BLU2_hg6.cjs.map → deeplink-BpO9qc-D.js.map} +1 -1
- package/dist/{deeplink-CYqDwVYs.js → deeplink-D1HXJ2YG.js} +5 -3
- package/dist/{deeplink-CU6opogq.cjs.map → deeplink-D1HXJ2YG.js.map} +1 -1
- package/dist/{deeplink-BLU2_hg6.cjs → deeplink-DCScMYcp.cjs} +5 -3
- package/dist/deeplink-DCScMYcp.cjs.map +1 -0
- package/dist/{deeplink-CU6opogq.cjs → deeplink-DDOe0FQl.cjs} +5 -3
- package/dist/deeplink-DDOe0FQl.cjs.map +1 -0
- package/dist/{devtools-opener-Bp671YXu.cjs → devtools-opener-BDY0w3_0.cjs} +11 -5
- package/dist/devtools-opener-BDY0w3_0.cjs.map +1 -0
- package/dist/{devtools-opener-D84kZFtR.js → devtools-opener-BTl5A6Cd.js} +11 -5
- package/dist/devtools-opener-BTl5A6Cd.js.map +1 -0
- package/dist/{devtools-opener-BbUXBzgA.js → devtools-opener-XpwL3fZ9.js} +22 -6
- package/dist/devtools-opener-XpwL3fZ9.js.map +1 -0
- package/dist/{devtools-opener-h6A-UjzC.cjs → devtools-opener-mDgeg_MX.cjs} +11 -5
- package/dist/devtools-opener-mDgeg_MX.cjs.map +1 -0
- package/dist/machine-state-Chg_6SPq.js +188 -0
- package/dist/machine-state-Chg_6SPq.js.map +1 -0
- package/dist/machine-state-DOUweFsJ.cjs +216 -0
- package/dist/machine-state-DOUweFsJ.cjs.map +1 -0
- package/dist/mcp/cli.js +104 -37
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +6 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +109 -8
- package/dist/panel/index.js.map +1 -1
- package/dist/{qr-http-server-DJ5K3Odk.js → qr-http-server-Buorblrx.js} +73 -23
- package/dist/qr-http-server-Buorblrx.js.map +1 -0
- package/dist/{qr-http-server-Dkx2-pKF.cjs → qr-http-server-BvAtX9Lc.cjs} +73 -23
- package/dist/qr-http-server-BvAtX9Lc.cjs.map +1 -0
- package/dist/{qr-http-server-DkOFfZsR.js → qr-http-server-BxjrJr9t.js} +73 -23
- package/dist/qr-http-server-BxjrJr9t.js.map +1 -0
- package/dist/{qr-http-server-MIUHaiYw.cjs → qr-http-server-CAUyOrCm.cjs} +73 -23
- package/dist/qr-http-server-CAUyOrCm.cjs.map +1 -0
- package/dist/{relay-secret-store-CsCOfpWt.cjs → relay-secret-store-B5WAozDv.cjs} +2 -2
- package/dist/{relay-secret-store-CsCOfpWt.cjs.map → relay-secret-store-B5WAozDv.cjs.map} +1 -1
- package/dist/{relay-secret-store-6pPzLkUO.js → relay-secret-store-BvNWdSjV.js} +2 -2
- package/dist/{relay-secret-store-6pPzLkUO.js.map → relay-secret-store-BvNWdSjV.js.map} +1 -1
- package/dist/{relay-url-store-DH8-VUFc.js → relay-url-store-1CXVqNDL.js} +2 -2
- package/dist/{relay-url-store-DH8-VUFc.js.map → relay-url-store-1CXVqNDL.js.map} +1 -1
- package/dist/{relay-url-store-BiEK9BN1.cjs → relay-url-store-D2lX9POP.cjs} +2 -2
- package/dist/{relay-url-store-BiEK9BN1.cjs.map → relay-url-store-D2lX9POP.cjs.map} +1 -1
- package/dist/{totp-DYdP9N3o.js → totp-CauHjkdE.js} +1 -1
- package/dist/{totp-DYdP9N3o.js.map → totp-CauHjkdE.js.map} +1 -1
- package/dist/{totp-CNw0w89F.cjs → totp-D9fjaVak.cjs} +1 -1
- package/dist/{totp-CNw0w89F.cjs.map → totp-D9fjaVak.cjs.map} +1 -1
- package/dist/{tunnel-C-AFdAVL.cjs → tunnel-CfT31xho.cjs} +6 -6
- package/dist/{tunnel-C-AFdAVL.cjs.map → tunnel-CfT31xho.cjs.map} +1 -1
- package/dist/{tunnel-BTlq1mmH.js → tunnel-DPwJBn1u.js} +6 -6
- package/dist/{tunnel-BTlq1mmH.js.map → tunnel-DPwJBn1u.js.map} +1 -1
- package/dist/unplugin/index.cjs +90 -5
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +90 -5
- package/dist/unplugin/index.js.map +1 -1
- package/dist/unplugin/tunnel.cjs +4 -4
- package/dist/unplugin/tunnel.js +4 -4
- package/package.json +1 -1
- package/dist/deeplink-B-94XmWA.js.map +0 -1
- package/dist/deeplink-CYqDwVYs.js.map +0 -1
- package/dist/devtools-opener-BbUXBzgA.js.map +0 -1
- package/dist/devtools-opener-Bp671YXu.cjs.map +0 -1
- package/dist/devtools-opener-D84kZFtR.js.map +0 -1
- package/dist/devtools-opener-h6A-UjzC.cjs.map +0 -1
- package/dist/qr-http-server-DJ5K3Odk.js.map +0 -1
- package/dist/qr-http-server-DkOFfZsR.js.map +0 -1
- package/dist/qr-http-server-Dkx2-pKF.cjs.map +0 -1
- package/dist/qr-http-server-MIUHaiYw.cjs.map +0 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
let node_os = require("node:os");
|
|
14
|
+
let node_path = require("node:path");
|
|
15
|
+
//#region src/telemetry/machine-state.ts
|
|
16
|
+
/**
|
|
17
|
+
* Machine-level telemetry consent store — #542
|
|
18
|
+
*
|
|
19
|
+
* Persists consent to `~/.ait-devtools/telemetry.json` (Node.js side only).
|
|
20
|
+
* This eliminates repeated consent prompts when the dev origin rotates
|
|
21
|
+
* (quick-tunnel host changes per session, localhost port varies).
|
|
22
|
+
*
|
|
23
|
+
* Design invariants:
|
|
24
|
+
* - localStorage keys in src/telemetry/state.ts are LOCKED — this module does
|
|
25
|
+
* NOT replace them. It adds a machine-level overlay that takes precedence
|
|
26
|
+
* when a dev server is running.
|
|
27
|
+
* - Tier 0 / Tier 1 consent semantics are unchanged — only the storage
|
|
28
|
+
* location and lifetime change.
|
|
29
|
+
* - No PII. The file stores: consent enum + decided_at ISO timestamp +
|
|
30
|
+
* policy_version + anon_id (pseudonymous UUID).
|
|
31
|
+
* - anon_id is promoted to machine level so the same identity is used across
|
|
32
|
+
* all dev origins (prevents counting the same developer multiple times).
|
|
33
|
+
*
|
|
34
|
+
* Node.js-only: imported exclusively from the unplugin (server-side).
|
|
35
|
+
* Never imported from panel/browser code.
|
|
36
|
+
*/
|
|
37
|
+
var machine_state_exports = /* @__PURE__ */ __exportAll({
|
|
38
|
+
ensureMachineConsent: () => ensureMachineConsent,
|
|
39
|
+
machineStateDir: () => machineStateDir,
|
|
40
|
+
machineStateFile: () => machineStateFile,
|
|
41
|
+
promptTtyConsent: () => promptTtyConsent,
|
|
42
|
+
readMachineState: () => readMachineState,
|
|
43
|
+
writeMachineState: () => writeMachineState
|
|
44
|
+
});
|
|
45
|
+
/** Resolved directory: `~/.ait-devtools`. */
|
|
46
|
+
function machineStateDir(homeOverride) {
|
|
47
|
+
return (0, node_path.join)(homeOverride ?? (0, node_os.homedir)(), ".ait-devtools");
|
|
48
|
+
}
|
|
49
|
+
/** Resolved file: `~/.ait-devtools/telemetry.json`. */
|
|
50
|
+
function machineStateFile(homeOverride) {
|
|
51
|
+
return (0, node_path.join)(machineStateDir(homeOverride), "telemetry.json");
|
|
52
|
+
}
|
|
53
|
+
function isoNow() {
|
|
54
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
55
|
+
}
|
|
56
|
+
function newUUID() {
|
|
57
|
+
return crypto.randomUUID();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Returns a minimal MachineStateFsDep using real `node:fs` synchronous APIs.
|
|
61
|
+
* Resolved lazily to avoid pulling node:fs into non-Node environments.
|
|
62
|
+
*/
|
|
63
|
+
async function realFs() {
|
|
64
|
+
const { existsSync, mkdirSync, readFileSync, writeFileSync } = await import("node:fs");
|
|
65
|
+
return {
|
|
66
|
+
existsSync,
|
|
67
|
+
mkdirSync,
|
|
68
|
+
readFileSync,
|
|
69
|
+
writeFileSync
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Reads the machine-level telemetry state.
|
|
74
|
+
* Returns `null` when the file is absent or cannot be parsed.
|
|
75
|
+
*
|
|
76
|
+
* NODE-ONLY. Never call from browser/panel code.
|
|
77
|
+
*/
|
|
78
|
+
async function readMachineState(deps) {
|
|
79
|
+
const fs = deps?.fs ?? await realFs();
|
|
80
|
+
const filePath = machineStateFile(deps?.homeDir);
|
|
81
|
+
if (!fs.existsSync(filePath)) return null;
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = fs.readFileSync(filePath, "utf8");
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(raw);
|
|
90
|
+
if (parsed !== null && typeof parsed === "object" && "consent" in parsed && typeof parsed.consent === "string" && [
|
|
91
|
+
"granted",
|
|
92
|
+
"denied",
|
|
93
|
+
"undecided"
|
|
94
|
+
].includes(parsed.consent)) return parsed;
|
|
95
|
+
return null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Writes (or patches) the machine-level telemetry state file.
|
|
102
|
+
* Creates `~/.ait-devtools/` if it does not yet exist.
|
|
103
|
+
*
|
|
104
|
+
* NODE-ONLY. Never call from browser/panel code.
|
|
105
|
+
*/
|
|
106
|
+
async function writeMachineState(patch, deps) {
|
|
107
|
+
const fs = deps?.fs ?? await realFs();
|
|
108
|
+
const dir = machineStateDir(deps?.homeDir);
|
|
109
|
+
const filePath = machineStateFile(deps?.homeDir);
|
|
110
|
+
const now = deps?.now ?? isoNow;
|
|
111
|
+
const uuid = deps?.randomUUID ?? newUUID;
|
|
112
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
113
|
+
let existing = null;
|
|
114
|
+
if (fs.existsSync(filePath)) try {
|
|
115
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
116
|
+
existing = JSON.parse(raw);
|
|
117
|
+
} catch {}
|
|
118
|
+
const next = {
|
|
119
|
+
...existing ?? {
|
|
120
|
+
consent: "undecided",
|
|
121
|
+
decided_at: now(),
|
|
122
|
+
policy_version: "",
|
|
123
|
+
anon_id: null
|
|
124
|
+
},
|
|
125
|
+
...patch,
|
|
126
|
+
anon_id: existing?.anon_id ?? patch.anon_id ?? uuid(),
|
|
127
|
+
decided_at: now()
|
|
128
|
+
};
|
|
129
|
+
fs.writeFileSync(filePath, JSON.stringify(next, null, 2), { mode: 384 });
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Prompts the user in the terminal (stdin/stdout TTY) to accept or deny
|
|
133
|
+
* telemetry consent — called once per machine on the first `pnpm dev` run.
|
|
134
|
+
*
|
|
135
|
+
* Returns the chosen `ConsentState`, or `'undecided'` when:
|
|
136
|
+
* - The TTY is not interactive (CI, headless, piped stdin).
|
|
137
|
+
* - The prompt times out.
|
|
138
|
+
* - An error occurs.
|
|
139
|
+
*
|
|
140
|
+
* NODE-ONLY. Called from the unplugin `configureServer` hook.
|
|
141
|
+
*/
|
|
142
|
+
async function promptTtyConsent() {
|
|
143
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return "undecided";
|
|
144
|
+
const { createInterface } = await import("node:readline");
|
|
145
|
+
const rl = createInterface({
|
|
146
|
+
input: process.stdin,
|
|
147
|
+
output: process.stdout
|
|
148
|
+
});
|
|
149
|
+
const question = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
150
|
+
process.stdout.write("\n[@ait-co/devtools] 익명 사용 통계를 보내도 될까요?\n 버전·날짜만 수집하고 PII는 없습니다. 언제든 Environment 탭에서 끌 수 있어요.\n 자세히: https://docs.aitc.dev/privacy\n (y) 네, 보낼게요 (n) 아니요 (무응답 시 보류 — 다음 기동 때 다시 물어요)\n");
|
|
151
|
+
let answer = "";
|
|
152
|
+
try {
|
|
153
|
+
answer = await Promise.race([question(" > "), new Promise((resolve) => setTimeout(() => resolve(""), 3e4))]);
|
|
154
|
+
} catch {} finally {
|
|
155
|
+
rl.close();
|
|
156
|
+
}
|
|
157
|
+
const normalized = answer.trim().toLowerCase();
|
|
158
|
+
if (normalized === "y" || normalized === "yes" || normalized === "네" || normalized === "ㅇ") return "granted";
|
|
159
|
+
if (normalized === "n" || normalized === "no" || normalized === "아니요" || normalized === "ㄴ") return "denied";
|
|
160
|
+
return "undecided";
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Ensures the machine-level consent state is resolved.
|
|
164
|
+
*
|
|
165
|
+
* Algorithm:
|
|
166
|
+
* 1. Read the machine file.
|
|
167
|
+
* 2. If already decided (granted/denied) → return as-is (no re-prompt).
|
|
168
|
+
* Exception: granted + stale policy_version → revert to undecided (mirrors
|
|
169
|
+
* browser-side resolveEffectiveConsent).
|
|
170
|
+
* 3. If undecided or absent → TTY-prompt once. Persist the answer (including
|
|
171
|
+
* 'undecided' — the directory is guaranteed to exist for subsequent writes,
|
|
172
|
+
* and decided_at is set for potential future reprompt-window logic).
|
|
173
|
+
*
|
|
174
|
+
* Returns the resolved `MachineTelemetryState`. Callers decide what to do with it.
|
|
175
|
+
*
|
|
176
|
+
* NODE-ONLY.
|
|
177
|
+
*/
|
|
178
|
+
async function ensureMachineConsent(policyVersion, deps) {
|
|
179
|
+
const existing = await readMachineState(deps);
|
|
180
|
+
if (existing?.consent === "granted" || existing?.consent === "denied") {
|
|
181
|
+
if (existing.consent === "granted" && existing.policy_version !== policyVersion) {
|
|
182
|
+
await writeMachineState({
|
|
183
|
+
consent: "undecided",
|
|
184
|
+
policy_version: policyVersion
|
|
185
|
+
}, deps);
|
|
186
|
+
return ensureMachineConsent(policyVersion, deps);
|
|
187
|
+
}
|
|
188
|
+
return existing;
|
|
189
|
+
}
|
|
190
|
+
await writeMachineState({
|
|
191
|
+
consent: await (deps?.promptConsent ?? promptTtyConsent)(),
|
|
192
|
+
policy_version: policyVersion
|
|
193
|
+
}, deps);
|
|
194
|
+
return await readMachineState(deps);
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
Object.defineProperty(exports, "ensureMachineConsent", {
|
|
198
|
+
enumerable: true,
|
|
199
|
+
get: function() {
|
|
200
|
+
return ensureMachineConsent;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
Object.defineProperty(exports, "machine_state_exports", {
|
|
204
|
+
enumerable: true,
|
|
205
|
+
get: function() {
|
|
206
|
+
return machine_state_exports;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
Object.defineProperty(exports, "writeMachineState", {
|
|
210
|
+
enumerable: true,
|
|
211
|
+
get: function() {
|
|
212
|
+
return writeMachineState;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
//# sourceMappingURL=machine-state-DOUweFsJ.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-state-DOUweFsJ.cjs","names":[],"sources":["../src/telemetry/machine-state.ts"],"sourcesContent":["/**\n * Machine-level telemetry consent store — #542\n *\n * Persists consent to `~/.ait-devtools/telemetry.json` (Node.js side only).\n * This eliminates repeated consent prompts when the dev origin rotates\n * (quick-tunnel host changes per session, localhost port varies).\n *\n * Design invariants:\n * - localStorage keys in src/telemetry/state.ts are LOCKED — this module does\n * NOT replace them. It adds a machine-level overlay that takes precedence\n * when a dev server is running.\n * - Tier 0 / Tier 1 consent semantics are unchanged — only the storage\n * location and lifetime change.\n * - No PII. The file stores: consent enum + decided_at ISO timestamp +\n * policy_version + anon_id (pseudonymous UUID).\n * - anon_id is promoted to machine level so the same identity is used across\n * all dev origins (prevents counting the same developer multiple times).\n *\n * Node.js-only: imported exclusively from the unplugin (server-side).\n * Never imported from panel/browser code.\n */\n\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nimport type { ConsentState } from './state.js';\n\n// ---------------------------------------------------------------------------\n// Schema\n// ---------------------------------------------------------------------------\n\nexport interface MachineTelemetryState {\n consent: ConsentState;\n /** ISO 8601 timestamp of the decision (or first-write). */\n decided_at: string;\n /** Policy version string at decision time. */\n policy_version: string;\n /**\n * Machine-level anon_id (UUID v4). Created once and never overwritten.\n * Promoted from per-origin localStorage to here so all dev origins share\n * a single pseudonymous identity.\n */\n anon_id: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// File path helpers\n// ---------------------------------------------------------------------------\n\n/** Resolved directory: `~/.ait-devtools`. */\nexport function machineStateDir(homeOverride?: string): string {\n return join(homeOverride ?? homedir(), '.ait-devtools');\n}\n\n/** Resolved file: `~/.ait-devtools/telemetry.json`. */\nexport function machineStateFile(homeOverride?: string): string {\n return join(machineStateDir(homeOverride), 'telemetry.json');\n}\n\n// ---------------------------------------------------------------------------\n// Dependency injection surface (injectable for tests)\n// ---------------------------------------------------------------------------\n\nexport interface MachineStateFsDep {\n existsSync(path: string): boolean;\n mkdirSync(path: string, options?: { recursive?: boolean }): void;\n readFileSync(path: string, encoding: BufferEncoding): string;\n writeFileSync(path: string, data: string, options?: { mode?: number }): void;\n}\n\nexport interface MachineStateDeps {\n fs?: MachineStateFsDep;\n homeDir?: string;\n now?: () => string;\n randomUUID?: () => string;\n /**\n * Override for the TTY consent prompt. When provided, `ensureMachineConsent`\n * calls this instead of the real `promptTtyConsent` — useful in tests where\n * `process.stdin.isTTY` cannot be spied on.\n */\n promptConsent?: () => Promise<ConsentState>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction isoNow(): string {\n return new Date().toISOString();\n}\n\nfunction newUUID(): string {\n return crypto.randomUUID();\n}\n\n/**\n * Returns a minimal MachineStateFsDep using real `node:fs` synchronous APIs.\n * Resolved lazily to avoid pulling node:fs into non-Node environments.\n */\nasync function realFs(): Promise<MachineStateFsDep> {\n const { existsSync, mkdirSync, readFileSync, writeFileSync } = await import('node:fs');\n return { existsSync, mkdirSync, readFileSync, writeFileSync };\n}\n\n// ---------------------------------------------------------------------------\n// Read\n// ---------------------------------------------------------------------------\n\n/**\n * Reads the machine-level telemetry state.\n * Returns `null` when the file is absent or cannot be parsed.\n *\n * NODE-ONLY. Never call from browser/panel code.\n */\nexport async function readMachineState(\n deps?: MachineStateDeps,\n): Promise<MachineTelemetryState | null> {\n const fs = deps?.fs ?? (await realFs());\n const filePath = machineStateFile(deps?.homeDir);\n\n if (!fs.existsSync(filePath)) return null;\n\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, 'utf8');\n } catch {\n return null;\n }\n\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (\n parsed !== null &&\n typeof parsed === 'object' &&\n 'consent' in parsed &&\n typeof (parsed as { consent: unknown }).consent === 'string' &&\n ['granted', 'denied', 'undecided'].includes((parsed as { consent: string }).consent)\n ) {\n return parsed as MachineTelemetryState;\n }\n return null;\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Write\n// ---------------------------------------------------------------------------\n\n/**\n * Writes (or patches) the machine-level telemetry state file.\n * Creates `~/.ait-devtools/` if it does not yet exist.\n *\n * NODE-ONLY. Never call from browser/panel code.\n */\nexport async function writeMachineState(\n patch: Partial<MachineTelemetryState>,\n deps?: MachineStateDeps,\n): Promise<void> {\n const fs = deps?.fs ?? (await realFs());\n const dir = machineStateDir(deps?.homeDir);\n const filePath = machineStateFile(deps?.homeDir);\n const now = deps?.now ?? isoNow;\n const uuid = deps?.randomUUID ?? newUUID;\n\n // Ensure directory exists.\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n // Merge with existing state (if any).\n let existing: MachineTelemetryState | null = null;\n if (fs.existsSync(filePath)) {\n try {\n const raw = fs.readFileSync(filePath, 'utf8');\n existing = JSON.parse(raw) as MachineTelemetryState;\n } catch {\n // Corrupt file — overwrite.\n }\n }\n\n const base: MachineTelemetryState = existing ?? {\n consent: 'undecided',\n decided_at: now(),\n policy_version: '',\n anon_id: null,\n };\n\n const next: MachineTelemetryState = {\n ...base,\n ...patch,\n // Never overwrite an existing anon_id — generate one if creating fresh.\n anon_id: existing?.anon_id ?? patch.anon_id ?? uuid(),\n decided_at: now(),\n };\n\n fs.writeFileSync(filePath, JSON.stringify(next, null, 2), { mode: 0o600 });\n}\n\n// ---------------------------------------------------------------------------\n// TTY consent prompt (dev server only)\n// ---------------------------------------------------------------------------\n\n/**\n * Prompts the user in the terminal (stdin/stdout TTY) to accept or deny\n * telemetry consent — called once per machine on the first `pnpm dev` run.\n *\n * Returns the chosen `ConsentState`, or `'undecided'` when:\n * - The TTY is not interactive (CI, headless, piped stdin).\n * - The prompt times out.\n * - An error occurs.\n *\n * NODE-ONLY. Called from the unplugin `configureServer` hook.\n */\nexport async function promptTtyConsent(): Promise<ConsentState> {\n // Guard: require an interactive terminal on both ends.\n if (!process.stdin.isTTY || !process.stdout.isTTY) {\n return 'undecided';\n }\n\n const { createInterface } = await import('node:readline');\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n\n const question = (q: string): Promise<string> =>\n new Promise((resolve) => rl.question(q, resolve));\n\n process.stdout.write(\n '\\n[@ait-co/devtools] 익명 사용 통계를 보내도 될까요?\\n' +\n ' 버전·날짜만 수집하고 PII는 없습니다. 언제든 Environment 탭에서 끌 수 있어요.\\n' +\n ' 자세히: https://docs.aitc.dev/privacy\\n' +\n ' (y) 네, 보낼게요 (n) 아니요 (무응답 시 보류 — 다음 기동 때 다시 물어요)\\n',\n );\n\n let answer = '';\n try {\n // 30-second timeout — if the user ignores the prompt, default to 'undecided'.\n answer = await Promise.race([\n question(' > '),\n new Promise<string>((resolve) => setTimeout(() => resolve(''), 30_000)),\n ]);\n } catch {\n // readline error — stay undecided.\n } finally {\n rl.close();\n }\n\n const normalized = answer.trim().toLowerCase();\n if (normalized === 'y' || normalized === 'yes' || normalized === '네' || normalized === 'ㅇ') {\n return 'granted';\n }\n if (normalized === 'n' || normalized === 'no' || normalized === '아니요' || normalized === 'ㄴ') {\n return 'denied';\n }\n // Empty / timeout / unrecognised → undecided (will be asked again next session).\n return 'undecided';\n}\n\n// ---------------------------------------------------------------------------\n// Bootstrap helper — called from configureServer once dev server is ready\n// ---------------------------------------------------------------------------\n\n/**\n * Ensures the machine-level consent state is resolved.\n *\n * Algorithm:\n * 1. Read the machine file.\n * 2. If already decided (granted/denied) → return as-is (no re-prompt).\n * Exception: granted + stale policy_version → revert to undecided (mirrors\n * browser-side resolveEffectiveConsent).\n * 3. If undecided or absent → TTY-prompt once. Persist the answer (including\n * 'undecided' — the directory is guaranteed to exist for subsequent writes,\n * and decided_at is set for potential future reprompt-window logic).\n *\n * Returns the resolved `MachineTelemetryState`. Callers decide what to do with it.\n *\n * NODE-ONLY.\n */\nexport async function ensureMachineConsent(\n policyVersion: string,\n deps?: MachineStateDeps,\n): Promise<MachineTelemetryState> {\n const existing = await readMachineState(deps);\n\n if (existing?.consent === 'granted' || existing?.consent === 'denied') {\n // Policy-version bump: re-prompt only when previously granted (same logic as\n // browser-side resolveEffectiveConsent — denied stays denied on version change).\n if (existing.consent === 'granted' && existing.policy_version !== policyVersion) {\n await writeMachineState({ consent: 'undecided', policy_version: policyVersion }, deps);\n // Recurse: will now prompt the user.\n return ensureMachineConsent(policyVersion, deps);\n }\n return existing;\n }\n\n // Undecided or absent → prompt (use injected override in tests).\n const prompt = deps?.promptConsent ?? promptTtyConsent;\n const answer = await prompt();\n await writeMachineState({ consent: answer, policy_version: policyVersion }, deps);\n\n // Re-read to get the full written state (includes newly generated anon_id).\n return (await readMachineState(deps)) as MachineTelemetryState;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,SAAgB,gBAAgB,cAA+B;AAC7D,SAAA,GAAA,UAAA,MAAY,iBAAA,GAAA,QAAA,UAAyB,EAAE,gBAAgB;;;AAIzD,SAAgB,iBAAiB,cAA+B;AAC9D,SAAA,GAAA,UAAA,MAAY,gBAAgB,aAAa,EAAE,iBAAiB;;AA+B9D,SAAS,SAAiB;AACxB,yBAAO,IAAI,MAAM,EAAC,aAAa;;AAGjC,SAAS,UAAkB;AACzB,QAAO,OAAO,YAAY;;;;;;AAO5B,eAAe,SAAqC;CAClD,MAAM,EAAE,YAAY,WAAW,cAAc,kBAAkB,MAAM,OAAO;AAC5E,QAAO;EAAE;EAAY;EAAW;EAAc;EAAe;;;;;;;;AAa/D,eAAsB,iBACpB,MACuC;CACvC,MAAM,KAAK,MAAM,MAAO,MAAM,QAAQ;CACtC,MAAM,WAAW,iBAAiB,MAAM,QAAQ;AAEhD,KAAI,CAAC,GAAG,WAAW,SAAS,CAAE,QAAO;CAErC,IAAI;AACJ,KAAI;AACF,QAAM,GAAG,aAAa,UAAU,OAAO;SACjC;AACN,SAAO;;AAGT,KAAI;EACF,MAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,MACE,WAAW,QACX,OAAO,WAAW,YAClB,aAAa,UACb,OAAQ,OAAgC,YAAY,YACpD;GAAC;GAAW;GAAU;GAAY,CAAC,SAAU,OAA+B,QAAQ,CAEpF,QAAO;AAET,SAAO;SACD;AACN,SAAO;;;;;;;;;AAcX,eAAsB,kBACpB,OACA,MACe;CACf,MAAM,KAAK,MAAM,MAAO,MAAM,QAAQ;CACtC,MAAM,MAAM,gBAAgB,MAAM,QAAQ;CAC1C,MAAM,WAAW,iBAAiB,MAAM,QAAQ;CAChD,MAAM,MAAM,MAAM,OAAO;CACzB,MAAM,OAAO,MAAM,cAAc;AAGjC,KAAI,CAAC,GAAG,WAAW,IAAI,CACrB,IAAG,UAAU,KAAK,EAAE,WAAW,MAAM,CAAC;CAIxC,IAAI,WAAyC;AAC7C,KAAI,GAAG,WAAW,SAAS,CACzB,KAAI;EACF,MAAM,MAAM,GAAG,aAAa,UAAU,OAAO;AAC7C,aAAW,KAAK,MAAM,IAAI;SACpB;CAYV,MAAM,OAA8B;EAClC,GARkC,YAAY;GAC9C,SAAS;GACT,YAAY,KAAK;GACjB,gBAAgB;GAChB,SAAS;GACV;EAIC,GAAG;EAEH,SAAS,UAAU,WAAW,MAAM,WAAW,MAAM;EACrD,YAAY,KAAK;EAClB;AAED,IAAG,cAAc,UAAU,KAAK,UAAU,MAAM,MAAM,EAAE,EAAE,EAAE,MAAM,KAAO,CAAC;;;;;;;;;;;;;AAkB5E,eAAsB,mBAA0C;AAE9D,KAAI,CAAC,QAAQ,MAAM,SAAS,CAAC,QAAQ,OAAO,MAC1C,QAAO;CAGT,MAAM,EAAE,oBAAoB,MAAM,OAAO;CACzC,MAAM,KAAK,gBAAgB;EAAE,OAAO,QAAQ;EAAO,QAAQ,QAAQ;EAAQ,CAAC;CAE5E,MAAM,YAAY,MAChB,IAAI,SAAS,YAAY,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEnD,SAAQ,OAAO,MACb,8LAID;CAED,IAAI,SAAS;AACb,KAAI;AAEF,WAAS,MAAM,QAAQ,KAAK,CAC1B,SAAS,OAAO,EAChB,IAAI,SAAiB,YAAY,iBAAiB,QAAQ,GAAG,EAAE,IAAO,CAAC,CACxE,CAAC;SACI,WAEE;AACR,KAAG,OAAO;;CAGZ,MAAM,aAAa,OAAO,MAAM,CAAC,aAAa;AAC9C,KAAI,eAAe,OAAO,eAAe,SAAS,eAAe,OAAO,eAAe,IACrF,QAAO;AAET,KAAI,eAAe,OAAO,eAAe,QAAQ,eAAe,SAAS,eAAe,IACtF,QAAO;AAGT,QAAO;;;;;;;;;;;;;;;;;;AAuBT,eAAsB,qBACpB,eACA,MACgC;CAChC,MAAM,WAAW,MAAM,iBAAiB,KAAK;AAE7C,KAAI,UAAU,YAAY,aAAa,UAAU,YAAY,UAAU;AAGrE,MAAI,SAAS,YAAY,aAAa,SAAS,mBAAmB,eAAe;AAC/E,SAAM,kBAAkB;IAAE,SAAS;IAAa,gBAAgB;IAAe,EAAE,KAAK;AAEtF,UAAO,qBAAqB,eAAe,KAAK;;AAElD,SAAO;;AAMT,OAAM,kBAAkB;EAAE,SADX,OADA,MAAM,iBAAiB,mBACT;EACc,gBAAgB;EAAe,EAAE,KAAK;AAGjF,QAAQ,MAAM,iBAAiB,KAAK"}
|
package/dist/mcp/cli.js
CHANGED
|
@@ -1031,9 +1031,10 @@ const LAUNCHER_URL = "https://devtools.aitc.dev/launcher/";
|
|
|
1031
1031
|
* @param totpCode - Optional current TOTP code (6 digits). When provided, it
|
|
1032
1032
|
* is appended as `at=<totpCode>`. Must be computed at call time — it rotates
|
|
1033
1033
|
* every 30 s. Omit when TOTP is disabled.
|
|
1034
|
-
* @param opts - Optional app identity hints: `name` and `
|
|
1034
|
+
* @param opts - Optional app identity hints: `name`, `icon`, and `selfdebug`
|
|
1035
|
+
* (#498, #543).
|
|
1035
1036
|
* @returns The launcher deep-link URL with `?url=<enc>&debug=1&relay=<enc>
|
|
1036
|
-
* [&at=<code>][&name=<enc>][&icon=<enc>]` params.
|
|
1037
|
+
* [&at=<code>][&name=<enc>][&icon=<enc>][&selfdebug=1]` params.
|
|
1037
1038
|
*/
|
|
1038
1039
|
function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode, opts) {
|
|
1039
1040
|
let url = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}&debug=1&relay=${encodeURIComponent(wssUrl)}`;
|
|
@@ -1048,6 +1049,7 @@ function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode, opts) {
|
|
|
1048
1049
|
}
|
|
1049
1050
|
if (iconParsed?.protocol === "https:") url += `&icon=${encodeURIComponent(opts.icon)}`;
|
|
1050
1051
|
}
|
|
1052
|
+
if (opts?.selfdebug === true) url += "&selfdebug=1";
|
|
1051
1053
|
return url;
|
|
1052
1054
|
}
|
|
1053
1055
|
/**
|
|
@@ -1233,12 +1235,18 @@ function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "co
|
|
|
1233
1235
|
return `${relayHttpBaseUrl.replace(/\/$/, "")}/front_end/chii_app.html?${params.toString()}`;
|
|
1234
1236
|
}
|
|
1235
1237
|
/**
|
|
1236
|
-
* Returns `true` when auto-open is **disabled
|
|
1237
|
-
*
|
|
1238
|
-
* absent)
|
|
1238
|
+
* Returns `true` when auto-open is **disabled**.
|
|
1239
|
+
*
|
|
1240
|
+
* Default (env var absent or any value other than `"1"`) is **disabled** —
|
|
1241
|
+
* the developer uses the "디버그 툴 열기" button on the /attach or dashboard
|
|
1242
|
+
* page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic
|
|
1243
|
+
* browser-open behaviour on device attach.
|
|
1244
|
+
*
|
|
1245
|
+
* `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward
|
|
1246
|
+
* compatibility (same effect as absent).
|
|
1239
1247
|
*/
|
|
1240
1248
|
function isAutoDevtoolsDisabled() {
|
|
1241
|
-
return process.env.AIT_AUTO_DEVTOOLS
|
|
1249
|
+
return process.env.AIT_AUTO_DEVTOOLS !== "1";
|
|
1242
1250
|
}
|
|
1243
1251
|
/**
|
|
1244
1252
|
* Opens the given URL in the OS default browser using a platform-appropriate
|
|
@@ -1341,8 +1349,8 @@ var AutoDevtoolsOpener = class {
|
|
|
1341
1349
|
if (options.inspectorStableUrl) {
|
|
1342
1350
|
this._openedTargets.add(targetId);
|
|
1343
1351
|
const stableUrl = options.inspectorStableUrl;
|
|
1344
|
-
process.stderr.write(`[ait-debug] 기기가
|
|
1345
|
-
[ait-debug]
|
|
1352
|
+
process.stderr.write(`[ait-debug] 기기가 연결됐습니다.
|
|
1353
|
+
[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace("/inspector", "")})의 "디버그 툴 열기" 버튼을 눌러 DevTools를 여세요.\n[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)
|
|
1346
1354
|
`);
|
|
1347
1355
|
if (!openUrlInBrowser(stableUrl)) process.stderr.write(`[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\n`);
|
|
1348
1356
|
return;
|
|
@@ -1354,8 +1362,8 @@ var AutoDevtoolsOpener = class {
|
|
|
1354
1362
|
process.stderr.write("[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\n[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\n");
|
|
1355
1363
|
return;
|
|
1356
1364
|
}
|
|
1357
|
-
process.stderr.write(`[ait-debug] 기기가
|
|
1358
|
-
[ait-debug] DevTools URL: ${inspectorUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=
|
|
1365
|
+
process.stderr.write(`[ait-debug] 기기가 연결됐습니다.
|
|
1366
|
+
[ait-debug] DevTools URL: ${inspectorUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)
|
|
1359
1367
|
[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.
|
|
1360
1368
|
`);
|
|
1361
1369
|
if (!openUrlInBrowser(inspectorUrl)) process.stderr.write("[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\n");
|
|
@@ -2134,8 +2142,8 @@ const en = {
|
|
|
2134
2142
|
"dashboard.url.copy": "Copy",
|
|
2135
2143
|
"dashboard.url.copied": "Copied",
|
|
2136
2144
|
"dashboard.inspector.section": "Inspector",
|
|
2137
|
-
"dashboard.inspector.open": "Open
|
|
2138
|
-
"dashboard.inspector.waiting": "
|
|
2145
|
+
"dashboard.inspector.open": "Open DevTools",
|
|
2146
|
+
"dashboard.inspector.waiting": "Attach a page to enable the \"Open DevTools\" button",
|
|
2139
2147
|
"inspector.error.noTarget": "No page attached. Attach a device and try again.",
|
|
2140
2148
|
"inspector.error.relayDown": "Relay is not active. Start a relay session first.",
|
|
2141
2149
|
"attach.title": "AIT Debug Session — QR Scan",
|
|
@@ -2388,8 +2396,8 @@ const tables = {
|
|
|
2388
2396
|
"dashboard.url.copy": "복사",
|
|
2389
2397
|
"dashboard.url.copied": "복사됨",
|
|
2390
2398
|
"dashboard.inspector.section": "인스펙터",
|
|
2391
|
-
"dashboard.inspector.open": "
|
|
2392
|
-
"dashboard.inspector.waiting": "
|
|
2399
|
+
"dashboard.inspector.open": "디버그 툴 열기",
|
|
2400
|
+
"dashboard.inspector.waiting": "페이지를 attach하면 \"디버그 툴 열기\" 버튼이 표시됩니다",
|
|
2393
2401
|
"inspector.error.noTarget": "연결된 페이지가 없습니다. 기기를 attach한 후 다시 시도하세요.",
|
|
2394
2402
|
"inspector.error.relayDown": "relay가 활성화되지 않았습니다. start_debug로 relay를 기동하세요.",
|
|
2395
2403
|
"attach.title": "AIT 디버그 세션 — QR 스캔",
|
|
@@ -2595,7 +2603,15 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
2595
2603
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2596
2604
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2597
2605
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2598
|
-
|
|
2606
|
+
.inspector-link {
|
|
2607
|
+
display: inline-block; margin-top: 0.5rem;
|
|
2608
|
+
padding: 0.45rem 1rem; border-radius: 6px;
|
|
2609
|
+
background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;
|
|
2610
|
+
text-decoration: none; text-align: center;
|
|
2611
|
+
}
|
|
2612
|
+
.inspector-link:hover { background: #388bfd; }
|
|
2613
|
+
.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
|
|
2614
|
+
</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).</li><li>launcher 안의 <strong>"QR 카메라로 스캔"</strong>으로 이 QR 코드를 스캔하세요.</li><li>미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요</li><li><strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요</li><li><strong>QR이 만료된 경우 (TOTP — 코드 1개는 30초 창, 만료 후 ~3분(±6 step) 이내 소급 허용)</strong> — 새 QR을 다시 스캔하세요</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section><hr/><section id="inspector-section"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section></body></html>`;
|
|
2599
2615
|
const attachChromeHtmlKoIntoss = `<!DOCTYPE html>
|
|
2600
2616
|
<html lang="ko"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="__QR_DATA_URL__"/><title>AIT 디버그 세션 — QR 스캔</title><style>
|
|
2601
2617
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -2645,7 +2661,15 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
2645
2661
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2646
2662
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2647
2663
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2648
|
-
|
|
2664
|
+
.inspector-link {
|
|
2665
|
+
display: inline-block; margin-top: 0.5rem;
|
|
2666
|
+
padding: 0.45rem 1rem; border-radius: 6px;
|
|
2667
|
+
background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;
|
|
2668
|
+
text-decoration: none; text-align: center;
|
|
2669
|
+
}
|
|
2670
|
+
.inspector-link:hover { background: #388bfd; }
|
|
2671
|
+
.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
|
|
2672
|
+
</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>토스 앱을 실행하세요.</li><li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li><li>팝업이 뜨면 <strong>"토스로 열기"</strong>를 탭하세요.</li><li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li><li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li><li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li>__LIVE_FAQ__</ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section><hr/><section id="inspector-section"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section></body></html>`;
|
|
2649
2673
|
const dashboardChromeHtmlEn = `<!DOCTYPE html>
|
|
2650
2674
|
<html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>AIT Debug Dashboard</title><style>
|
|
2651
2675
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -2755,7 +2779,15 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
2755
2779
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2756
2780
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2757
2781
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2758
|
-
|
|
2782
|
+
.inspector-link {
|
|
2783
|
+
display: inline-block; margin-top: 0.5rem;
|
|
2784
|
+
padding: 0.45rem 1rem; border-radius: 6px;
|
|
2785
|
+
background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;
|
|
2786
|
+
text-decoration: none; text-align: center;
|
|
2787
|
+
}
|
|
2788
|
+
.inspector-link:hover { background: #388bfd; }
|
|
2789
|
+
.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
|
|
2790
|
+
</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).</li><li>Scan this QR code with <strong>"Scan QR with camera"</strong> inside the launcher.</li><li>The mini-app opens fullscreen and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen</li><li><strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner</li><li><strong>QR expired (TOTP — 30-second step, ±6 steps (~3 min) accepted)</strong> — scan a fresh QR code</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section><hr/><section id="inspector-section"><h2>Inspector</h2>__INSPECTOR_SECTION__</section></body></html>`;
|
|
2759
2791
|
const attachChromeHtmlEnIntoss = `<!DOCTYPE html>
|
|
2760
2792
|
<html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="__QR_DATA_URL__"/><title>AIT Debug Session — QR Scan</title><style>
|
|
2761
2793
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -2805,7 +2837,15 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
2805
2837
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2806
2838
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2807
2839
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2808
|
-
|
|
2840
|
+
.inspector-link {
|
|
2841
|
+
display: inline-block; margin-top: 0.5rem;
|
|
2842
|
+
padding: 0.45rem 1rem; border-radius: 6px;
|
|
2843
|
+
background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;
|
|
2844
|
+
text-decoration: none; text-align: center;
|
|
2845
|
+
}
|
|
2846
|
+
.inspector-link:hover { background: #388bfd; }
|
|
2847
|
+
.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
|
|
2848
|
+
</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Open the Toss app.</li><li>Scan the QR code with your phone camera app.</li><li>Tap <strong>"Open in Toss"</strong> when the popup appears.</li><li>The mini-app opens and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)</li><li><strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li><li><strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server</li>__LIVE_FAQ__</ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section><hr/><section id="inspector-section"><h2>Inspector</h2>__INSPECTOR_SECTION__</section></body></html>`;
|
|
2809
2849
|
/** Map from Locale to the precompiled dashboard chrome string. */
|
|
2810
2850
|
const dashboardChromeByLocale = {
|
|
2811
2851
|
ko: dashboardChromeHtmlKo,
|
|
@@ -2905,8 +2945,9 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
|
|
|
2905
2945
|
const copyLabel = escapeHtml(s("dashboard.url.copy"));
|
|
2906
2946
|
attachSection = `<img class="qr" src="${qrDataUrl}" alt="attach QR" /><div class="url-row"><p class="url-box" id="url-box">${safeAttachUrl}</p><button class="copy-btn" id="copy-btn" type="button" aria-label="${copyLabel}">${copyLabel}</button></div>`;
|
|
2907
2947
|
} else attachSection = `<p class="hint">${escapeHtml(s("dashboard.attach.hint"))}</p>`;
|
|
2948
|
+
const pagesAttached = Array.isArray(state.pages) && state.pages.length > 0;
|
|
2908
2949
|
let inspectorSection;
|
|
2909
|
-
if (state.inspectorUrl) inspectorSection = `<a class="inspector-link" id="inspector-link" href="${escapeHtml(state.inspectorUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(s("dashboard.inspector.open"))}</a>`;
|
|
2950
|
+
if (pagesAttached && state.inspectorUrl) inspectorSection = `<a class="inspector-link" id="inspector-link" href="${escapeHtml(state.inspectorUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(s("dashboard.inspector.open"))}</a>`;
|
|
2910
2951
|
else inspectorSection = `<span class="inspector-hint" id="inspector-link">${escapeHtml(s("dashboard.inspector.waiting"))}</span>`;
|
|
2911
2952
|
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) => {
|
|
2912
2953
|
return `<li><span class="page-id">${escapeHtml(p.id)}</span> <span class="page-url">${escapeHtml(p.url.slice(0, 120))}</span></li>`;
|
|
@@ -3076,11 +3117,15 @@ function buildSseScript(strings) {
|
|
|
3076
3117
|
sec.innerHTML = '<p class=\\"hint\\">' + ATTACH_HINT + '</p>';`}
|
|
3077
3118
|
}
|
|
3078
3119
|
}
|
|
3079
|
-
// 인스펙터 링크 갱신 — #inspector-link (#503).
|
|
3120
|
+
// 인스펙터 링크 갱신 — #inspector-link (#503, gate 보정 #544).
|
|
3121
|
+
// 게이트: pages.length > 0 (페이지 attach 여부) — inspectorUrl 존재 여부가 아님.
|
|
3122
|
+
// #530 이후 inspectorUrl은 항상 안정 URL이므로 null 게이트는 사실상 항상 활성이었다.
|
|
3123
|
+
// pages.length > 0 으로 바꿔 미attach 시 대기 힌트를 보여주도록 수정.
|
|
3080
3124
|
// SECRET-HANDLING: inspectorUrl을 console.log 등으로 출력하지 않는다.
|
|
3081
3125
|
var insp = document.getElementById('inspector-link');
|
|
3082
3126
|
if (insp) {
|
|
3083
|
-
|
|
3127
|
+
var pagesAttachedSse = Array.isArray(s.pages) && s.pages.length > 0;
|
|
3128
|
+
if (pagesAttachedSse && s.inspectorUrl) {
|
|
3084
3129
|
var safeInspUrl = String(s.inspectorUrl).slice(0, 2000).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
|
|
3085
3130
|
insp.outerHTML = '<a class=\\"inspector-link\\" id=\\"inspector-link\\" href=\\"' + safeInspUrl + '\\" target=\\"_blank\\" rel=\\"noopener noreferrer\\">' + INSPECTOR_OPEN_LABEL + '</a>';
|
|
3086
3131
|
} else {
|
|
@@ -3102,28 +3147,33 @@ function buildSseScript(strings) {
|
|
|
3102
3147
|
* Attach 페이지 HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.
|
|
3103
3148
|
*
|
|
3104
3149
|
* 동적 파트:
|
|
3105
|
-
* - __QR_DATA_URL__
|
|
3106
|
-
* - __SAFE_LABEL__
|
|
3107
|
-
* - __SAFE_ATTACH_URL__
|
|
3108
|
-
* - __MODE_LABEL__
|
|
3109
|
-
* - __LIVE_FAQ__
|
|
3150
|
+
* - __QR_DATA_URL__ : base64 data URL (QR 이미지)
|
|
3151
|
+
* - __SAFE_LABEL__ : HTML-escaped deploymentId label (intoss family에만 존재)
|
|
3152
|
+
* - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)
|
|
3153
|
+
* - __MODE_LABEL__ : 환경 배지 (`<p class="mode-label">…</p>` 또는 빈 문자열, #468)
|
|
3154
|
+
* - __LIVE_FAQ__ : 환경 4 LIVE read-only `<li>` 또는 빈 문자열 (intoss family에만 존재)
|
|
3155
|
+
* - __INSPECTOR_SECTION__ : "디버그 툴 열기" 버튼 또는 대기 힌트 (#544)
|
|
3110
3156
|
*
|
|
3111
3157
|
* mode-aware 분기 (#468): mode가 `relay-mobile`이면 sandbox family chrome(launcher
|
|
3112
3158
|
* PWA 절차), 그 외는 intoss family chrome(토스 앱 절차)을 선택한다. `relay-live`는
|
|
3113
3159
|
* intoss chrome에 LIVE read-only 라인을 추가한다.
|
|
3114
3160
|
*
|
|
3115
3161
|
* SSE 스크립트도 주입 — `#attach-section` hook이 있으면 `/events` push 때 QR이
|
|
3116
|
-
* `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#
|
|
3117
|
-
*
|
|
3162
|
+
* `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#inspector-link`도 SSE push로
|
|
3163
|
+
* pages.length > 0 게이트에 따라 활성/비활성 전환된다 (#544).
|
|
3118
3164
|
*
|
|
3119
3165
|
* SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
|
|
3166
|
+
* inspectorStableUrl은 /inspector 안정 URL (127.0.0.1, 시크릿 없음) — 노출 가능.
|
|
3120
3167
|
*/
|
|
3121
|
-
function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams(), mode) {
|
|
3168
|
+
function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams(), mode, pagesAttached = false, inspectorStableUrl = null) {
|
|
3122
3169
|
const s = resolveLocaleStrings(locale);
|
|
3123
3170
|
const langSwitcher = buildLangSwitcher(path, params, locale, s);
|
|
3124
3171
|
const family = attachFamilyForMode(mode);
|
|
3125
3172
|
const liveFaq = mode === "relay-live" ? `<li>${s("attach.intoss.faq.liveReadOnly")}</li>` : "";
|
|
3126
|
-
|
|
3173
|
+
let inspectorSection;
|
|
3174
|
+
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>`;
|
|
3175
|
+
else inspectorSection = `<span class="inspector-hint" id="inspector-link">${escapeHtml(s("dashboard.inspector.waiting"))}</span>`;
|
|
3176
|
+
const filled = attachChromeByLocale[locale][family].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__MODE_LABEL__", buildModeLabel(mode, s)).replaceAll("__LIVE_FAQ__", liveFaq).replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl).replaceAll("__INSPECTOR_SECTION__", inspectorSection);
|
|
3127
3177
|
const sseScript = buildSseScript({
|
|
3128
3178
|
tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
|
|
3129
3179
|
tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
|
|
@@ -3225,12 +3275,20 @@ async function startQrHttpServer(getDashboardState, options) {
|
|
|
3225
3275
|
const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);
|
|
3226
3276
|
if (dpMatch?.[1]) deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);
|
|
3227
3277
|
} catch {}
|
|
3228
|
-
const
|
|
3278
|
+
const currentState = getDashboardState?.();
|
|
3279
|
+
const mode = currentState?.mode;
|
|
3280
|
+
const pagesAttached = Array.isArray(currentState?.pages) && (currentState?.pages.length ?? 0) > 0;
|
|
3281
|
+
const inspectorStableUrlForAttach = (() => {
|
|
3282
|
+
if (!options?.getDirectInspectorUrl) return null;
|
|
3283
|
+
const addr = server.address();
|
|
3284
|
+
if (!addr || typeof addr === "string") return null;
|
|
3285
|
+
return `http://127.0.0.1:${addr.port}/inspector`;
|
|
3286
|
+
})();
|
|
3229
3287
|
QRCode.toDataURL(attachUrl, {
|
|
3230
3288
|
type: "image/png",
|
|
3231
3289
|
errorCorrectionLevel: "M"
|
|
3232
3290
|
}).then((dataUrl) => {
|
|
3233
|
-
const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params, mode);
|
|
3291
|
+
const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params, mode, pagesAttached, inspectorStableUrlForAttach);
|
|
3234
3292
|
res.writeHead(200, {
|
|
3235
3293
|
"Content-Type": "text/html; charset=utf-8",
|
|
3236
3294
|
"Cache-Control": "no-store"
|
|
@@ -3703,7 +3761,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
3703
3761
|
},
|
|
3704
3762
|
{
|
|
3705
3763
|
name: "build_attach_url",
|
|
3706
|
-
description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep-link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep-link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). When projectRoot is given, the app name from <projectRoot>/package.json is automatically added as name= so the launcher partner bar shows it. Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (polls up to 30 s). On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). \n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>). The code is valid for ~3 minutes (the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.",
|
|
3764
|
+
description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Builds a self-attaching deep-link for the active relay environment and returns a QR code. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep-link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). When projectRoot is given, the app name from <projectRoot>/package.json is automatically added as name= so the launcher partner bar shows it. Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (polls up to 30 s). On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). \n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>). The code is valid for ~3 minutes (the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.\n\nselfdebug (env 2 / relay-sandbox only): pass selfdebug=true to add &selfdebug=1 to the launcher deep-link. The launcher PWA then registers its own document as the CDP target instead of the framed mini-app. SINGLE-ATTACH MODEL: attaching the launcher self-target evicts any currently-attached mini-app target — use this mode exclusively for diagnosing the launcher document itself (DOM, safe-area, console). Not applicable in env 3/4 (relay-staging/relay-live) — passing selfdebug=true there returns an error.",
|
|
3707
3765
|
inputSchema: {
|
|
3708
3766
|
type: "object",
|
|
3709
3767
|
properties: {
|
|
@@ -3722,6 +3780,10 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
3722
3780
|
projectRoot: {
|
|
3723
3781
|
type: "string",
|
|
3724
3782
|
description: "Absolute path to the mini-app project root (the directory containing its package.json and .ait_urls). When AIT_TUNNEL_BASE_URL is unset (env 2 / relay-mobile only), the daemon reads the app tunnel URL from <projectRoot>/.ait_urls written by the dev server (tunnel:{cdp:true}). Pass this because the daemon's own cwd is fixed at launch. Omit when AIT_TUNNEL_BASE_URL is set explicitly."
|
|
3783
|
+
},
|
|
3784
|
+
selfdebug: {
|
|
3785
|
+
type: "boolean",
|
|
3786
|
+
description: "Env 2 / relay-sandbox only. When true, adds &selfdebug=1 to the launcher deep-link so the launcher PWA registers its own document as the CDP target (launcher diagnostics mode). SINGLE-ATTACH MODEL: self-target attach evicts any currently-attached mini-app target. Use only when you need to inspect the launcher itself (DOM, safe-area, console). Passing selfdebug=true in env 3/4 (relay-staging/relay-live) returns an error. Default: false (omitted — output is byte-identical to previous behaviour)."
|
|
3725
3787
|
}
|
|
3726
3788
|
},
|
|
3727
3789
|
required: []
|
|
@@ -4717,7 +4779,7 @@ async function readMcpSdkVersion() {
|
|
|
4717
4779
|
* some test environments that skip the build step).
|
|
4718
4780
|
*/
|
|
4719
4781
|
function readDevtoolsVersion() {
|
|
4720
|
-
return "0.1.
|
|
4782
|
+
return "0.1.84";
|
|
4721
4783
|
}
|
|
4722
4784
|
/**
|
|
4723
4785
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -5221,7 +5283,7 @@ function createDebugServer(deps) {
|
|
|
5221
5283
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
5222
5284
|
const server = new Server({
|
|
5223
5285
|
name: "ait-debug",
|
|
5224
|
-
version: "0.1.
|
|
5286
|
+
version: "0.1.84"
|
|
5225
5287
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
5226
5288
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
5227
5289
|
const conn = router.active;
|
|
@@ -5295,6 +5357,8 @@ function createDebugServer(deps) {
|
|
|
5295
5357
|
if (name === "build_attach_url") {
|
|
5296
5358
|
const waitForAttach = request.params.arguments?.wait_for_attach === true;
|
|
5297
5359
|
const openInBrowser = request.params.arguments?.open_in_browser !== false;
|
|
5360
|
+
const selfdebug = request.params.arguments?.selfdebug === true;
|
|
5361
|
+
if (selfdebug && env !== "relay-mobile") return mcpError("build_attach_url: selfdebug=true는 env 2 / relay-sandbox 전용 기능입니다. 현재 환경(env 3/4)에서는 launcher가 없어 self-target 모드를 지원하지 않습니다. launcher self-target이 필요하다면 relay-sandbox 모드로 재시작하세요.");
|
|
5298
5362
|
if (env === "relay-mobile") {
|
|
5299
5363
|
const rawBuildProjectRoot = request.params.arguments?.projectRoot;
|
|
5300
5364
|
const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
|
|
@@ -5329,7 +5393,10 @@ function createDebugServer(deps) {
|
|
|
5329
5393
|
const rawName = typeof pkg.name === "string" ? pkg.name : "";
|
|
5330
5394
|
launcherAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
|
|
5331
5395
|
} catch {}
|
|
5332
|
-
const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode, {
|
|
5396
|
+
const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode, {
|
|
5397
|
+
name: launcherAppName,
|
|
5398
|
+
...selfdebug ? { selfdebug: true } : {}
|
|
5399
|
+
});
|
|
5333
5400
|
onAttachUrlBuilt?.({
|
|
5334
5401
|
kind: "launcher",
|
|
5335
5402
|
tunnelHttpUrl,
|
|
@@ -7264,7 +7331,7 @@ function createDevServer(deps = {}) {
|
|
|
7264
7331
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
7265
7332
|
const server = new Server({
|
|
7266
7333
|
name: "ait-devtools",
|
|
7267
|
-
version: "0.1.
|
|
7334
|
+
version: "0.1.84"
|
|
7268
7335
|
}, { capabilities: { tools: {} } });
|
|
7269
7336
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
7270
7337
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|