@ait-co/devtools 0.1.108 → 0.1.110
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/README.en.md +13 -31
- package/README.md +13 -31
- package/dist/bundle-KFs4t-wc.d.ts +96 -0
- package/dist/bundle-KFs4t-wc.d.ts.map +1 -0
- package/dist/in-app/auto.d.ts.map +1 -1
- package/dist/in-app/auto.js +40 -3
- package/dist/in-app/auto.js.map +1 -1
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +39 -2
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.d.ts +4 -16
- package/dist/mcp/cli.d.ts.map +1 -1
- package/dist/mcp/cli.js +803 -712
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +47 -59
- package/dist/mcp/server.js.map +1 -1
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +21 -2
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +47 -32
- package/dist/panel/index.js.map +1 -1
- package/dist/{pool-Dkp7I9Bf.d.ts → pool-Bf6rQci4.d.ts} +210 -48
- package/dist/pool-Bf6rQci4.d.ts.map +1 -0
- package/dist/{qr-http-server-D4EAA7Il.js → qr-http-server-BJJt3ush.js} +8 -17
- package/dist/qr-http-server-BJJt3ush.js.map +1 -0
- package/dist/{qr-http-server-A9vld8r7.cjs → qr-http-server-BVS-HZjU.cjs} +8 -17
- package/dist/qr-http-server-BVS-HZjU.cjs.map +1 -0
- package/dist/{qr-http-server-Dj3Z0NHi.cjs → qr-http-server-C1T4RNbq.cjs} +8 -17
- package/dist/qr-http-server-C1T4RNbq.cjs.map +1 -0
- package/dist/{qr-http-server-HzdCLU8s.js → qr-http-server-Cs93vEPH.js} +8 -17
- package/dist/qr-http-server-Cs93vEPH.js.map +1 -0
- package/dist/{relay-worker-BzFQ3fv9.d.ts → relay-worker-xxanNQGs.d.ts} +3 -3
- package/dist/relay-worker-xxanNQGs.d.ts.map +1 -0
- package/dist/{runtime-ORdrpizY.d.ts → runtime-Wi5d6Ywz.d.ts} +3 -3
- package/dist/{runtime-ORdrpizY.d.ts.map → runtime-Wi5d6Ywz.d.ts.map} +1 -1
- package/dist/test-runner/bundle.d.ts +1 -1
- package/dist/test-runner/bundle.js +148 -11
- package/dist/test-runner/bundle.js.map +1 -1
- package/dist/test-runner/cli.d.ts +59 -14
- package/dist/test-runner/cli.d.ts.map +1 -1
- package/dist/test-runner/cli.js +171 -32
- package/dist/test-runner/cli.js.map +1 -1
- package/dist/test-runner/config.d.ts +1 -1
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/test-runner/relay-worker.d.ts +1 -1
- package/dist/test-runner/relay-worker.js.map +1 -1
- package/dist/test-runner/rpc.d.ts +1 -1
- package/dist/test-runner/rpc.d.ts.map +1 -1
- package/dist/test-runner/rpc.js +1 -1
- package/dist/test-runner/rpc.js.map +1 -1
- package/dist/test-runner/task-graph.d.ts +1 -1
- package/dist/{tunnel-BjJROkcj.js → tunnel-Cpn3mA4u.js} +3 -3
- package/dist/tunnel-Cpn3mA4u.js.map +1 -0
- package/dist/{tunnel-d_G9AIFn.cjs → tunnel-Dj8Kf2QS.cjs} +3 -3
- package/dist/tunnel-Dj8Kf2QS.cjs.map +1 -0
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.d.cts +196 -34
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts +196 -34
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +2 -2
- package/dist/unplugin/tunnel.cjs.map +1 -1
- package/dist/unplugin/tunnel.d.cts +1 -1
- package/dist/unplugin/tunnel.d.ts +1 -1
- package/dist/unplugin/tunnel.js +2 -2
- package/dist/unplugin/tunnel.js.map +1 -1
- package/package.json +14 -14
- package/dist/bundle-BJm5jk56.d.ts +0 -49
- package/dist/bundle-BJm5jk56.d.ts.map +0 -1
- package/dist/pool-Dkp7I9Bf.d.ts.map +0 -1
- package/dist/qr-http-server-A9vld8r7.cjs.map +0 -1
- package/dist/qr-http-server-D4EAA7Il.js.map +0 -1
- package/dist/qr-http-server-Dj3Z0NHi.cjs.map +0 -1
- package/dist/qr-http-server-HzdCLU8s.js.map +0 -1
- package/dist/relay-worker-BzFQ3fv9.d.ts.map +0 -1
- package/dist/tunnel-BjJROkcj.js.map +0 -1
- package/dist/tunnel-d_G9AIFn.cjs.map +0 -1
package/dist/mcp/cli.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-WY6l0ysP.js";
|
|
3
3
|
import { t as loadRelaySecretReadOnly } from "../relay-secret-store-DhzAnnj-.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { accessSync, existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { argv } from "node:process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
10
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
12
|
+
import * as fs from "node:fs/promises";
|
|
12
13
|
import { glob } from "node:fs/promises";
|
|
13
14
|
import * as path from "node:path";
|
|
14
15
|
import { isAbsolute, join, resolve } from "node:path";
|
|
@@ -23,6 +24,95 @@ import { Tunnel, bin, install } from "cloudflared";
|
|
|
23
24
|
//#region \0rolldown/runtime.js
|
|
24
25
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
25
26
|
//#endregion
|
|
27
|
+
//#region src/in-app/gate.ts
|
|
28
|
+
/**
|
|
29
|
+
* The host suffix the Toss app uses to serve dogfood / private mini-apps.
|
|
30
|
+
*
|
|
31
|
+
* A `intoss-private://` (dogfood) entry maps to a host such as
|
|
32
|
+
* `aitc-sdk-example.private-apps.tossmini.com`. A production `intoss://`
|
|
33
|
+
* entry is served from `*.apps.tossmini.com` — the `.private-apps.` segment
|
|
34
|
+
* is absent. Confirmed live over CDP for mini-app 31146; the exact production
|
|
35
|
+
* host is to be re-confirmed once 31146 passes review (spec open question 2).
|
|
36
|
+
*/
|
|
37
|
+
const PRIVATE_APPS_HOST_SUFFIX = ".private-apps.tossmini.com";
|
|
38
|
+
/**
|
|
39
|
+
* The host suffix Cloudflare quick-tunnels serve from — the env 2 (PWA) entry.
|
|
40
|
+
* See {@link isTrycloudflareHost} for why this host kind bypasses Layer B1.
|
|
41
|
+
*/
|
|
42
|
+
const TRYCLOUDFLARE_HOST_SUFFIX = ".trycloudflare.com";
|
|
43
|
+
/**
|
|
44
|
+
* Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain —
|
|
45
|
+
* the host the Toss app reserves for dogfood / private mini-app entries.
|
|
46
|
+
*
|
|
47
|
+
* The match is an exact suffix check, not a substring `.includes()`: a
|
|
48
|
+
* substring test would also accept an attacker-controlled host like
|
|
49
|
+
* `private-apps.tossmini.com.evil.example`, which ends in `.example`, not in
|
|
50
|
+
* `.tossmini.com`. Requiring the string to END with the suffix closes that.
|
|
51
|
+
* The leading `.` in the suffix also forces a real subdomain label, so a
|
|
52
|
+
* bare `private-apps.tossmini.com` (no mini-app subdomain) does not match.
|
|
53
|
+
*/
|
|
54
|
+
function isPrivateAppsHost(hostname) {
|
|
55
|
+
return hostname.endsWith(PRIVATE_APPS_HOST_SUFFIX);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The host suffix Cloudflare quick-tunnels use — the env 2 (PWA) entry.
|
|
59
|
+
*
|
|
60
|
+
* Env 2 serves the local Vite dev server through a `*.trycloudflare.com` quick
|
|
61
|
+
* tunnel (`src/unplugin/tunnel.ts`). It has no Toss app, no `intoss-private://`
|
|
62
|
+
* scheme, and — critically — no production runtime: the SDK is the devtools
|
|
63
|
+
* mock, and the page is the developer's own dev build. The Layer B1 safety net
|
|
64
|
+
* (which stops a dogfood build that lands on a Toss *production* host from
|
|
65
|
+
* attaching) has nothing to protect against here, because env 2 has no
|
|
66
|
+
* production host. So a trycloudflare host is allowed past B1 — but ONLY past
|
|
67
|
+
* B1: the remaining layers (C1 opt-in, C2 relay, C3 TOTP) still apply, so a
|
|
68
|
+
* leaked tunnel URL is still blocked by TOTP exactly as on the Toss path.
|
|
69
|
+
*
|
|
70
|
+
* The match is the same exact-suffix `endsWith` check as
|
|
71
|
+
* {@link isPrivateAppsHost} — never a substring `.includes()`, which would
|
|
72
|
+
* accept an attacker-controlled `evil.trycloudflare.com.example.com`. The
|
|
73
|
+
* leading `.` forces a real subdomain label, so a bare `trycloudflare.com`
|
|
74
|
+
* (no tunnel subdomain) does not match.
|
|
75
|
+
*/
|
|
76
|
+
function isTrycloudflareHost(hostname) {
|
|
77
|
+
return hostname.endsWith(TRYCLOUDFLARE_HOST_SUFFIX);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Returns true when the hostname is a localhost/loopback address.
|
|
81
|
+
* Allowed: `localhost`, `127.x.x.x` (full RFC 5735 loopback block), `[::1]`,
|
|
82
|
+
* `0.0.0.0`, `*.localhost`.
|
|
83
|
+
*
|
|
84
|
+
* Security note: `hostname.startsWith('127.')` is intentionally NOT used —
|
|
85
|
+
* that pattern would accept `127.evil.com`, which starts with "127." but is an
|
|
86
|
+
* attacker-controlled hostname, not a loopback address. Instead, the 127/8
|
|
87
|
+
* loopback block is matched with a strict numeric-quad regex so only valid
|
|
88
|
+
* dotted-decimal IPv4 in the 127.x.x.x range pass (#665 작업 A fix).
|
|
89
|
+
*/
|
|
90
|
+
function isLocalhostHost(hostname) {
|
|
91
|
+
if (hostname === "localhost" || hostname === "0.0.0.0") return true;
|
|
92
|
+
if (hostname === "[::1]") return true;
|
|
93
|
+
if (/^127\.\d+\.\d+\.\d+$/.test(hostname)) return true;
|
|
94
|
+
if (hostname.endsWith(".localhost")) return true;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Positive-allowlist kill-switch (#665): returns true when the hostname is a
|
|
99
|
+
* known debug-allowed host. The debug surface is ONLY active on:
|
|
100
|
+
* - localhost / loopback (env 1 desktop dev)
|
|
101
|
+
* - *.trycloudflare.com (env 2 PWA tunnel)
|
|
102
|
+
* - *.private-apps.tossmini.com (env 3 dog-food)
|
|
103
|
+
*
|
|
104
|
+
* Any other host (including apps.tossmini.com — the former env 4 LIVE host)
|
|
105
|
+
* is silently blocked. This is a positive allowlist — unlisted hosts never
|
|
106
|
+
* had debug surface regardless, but this function makes it explicit and
|
|
107
|
+
* auditable in a single place.
|
|
108
|
+
*
|
|
109
|
+
* SECRET-HANDLING: the hostname value MUST NOT be logged or included in any
|
|
110
|
+
* error reason string — only benign labels ('host not in allowlist') are safe.
|
|
111
|
+
*/
|
|
112
|
+
function isDebugAllowedHost(hostname) {
|
|
113
|
+
return isLocalhostHost(hostname) || isTrycloudflareHost(hostname) || isPrivateAppsHost(hostname);
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
26
116
|
//#region src/shared/parent-watcher.ts
|
|
27
117
|
/**
|
|
28
118
|
* Shared parent-PID watcher — used by both the MCP debug daemon and the
|
|
@@ -158,11 +248,53 @@ async function discoverTestFiles(patterns, cwd) {
|
|
|
158
248
|
* esbuild-based bundler for user test files.
|
|
159
249
|
*
|
|
160
250
|
* Bundles a single test file into a self-contained IIFE string that can be
|
|
161
|
-
* injected into a WebView via `Runtime.evaluate`. The
|
|
162
|
-
* (
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
251
|
+
* injected into a WebView via `Runtime.evaluate`. The bundle includes the
|
|
252
|
+
* test runtime (`runtime.ts`), which provides `describe/it/test/expect` and
|
|
253
|
+
* the `runTestModule(factory)` entry point.
|
|
254
|
+
*
|
|
255
|
+
* ## How the wiring works
|
|
256
|
+
*
|
|
257
|
+
* The bundle exposes two exports on `globalThis.__testBundle`:
|
|
258
|
+
* - `runTestModule` — the runtime's entry function.
|
|
259
|
+
* - `__userFactory` — an async function whose body is the user's top-level
|
|
260
|
+
* test registration code (describe/it/test calls).
|
|
261
|
+
*
|
|
262
|
+
* The Node-side RPC (`rpc.ts`) calls:
|
|
263
|
+
* `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
|
|
264
|
+
*
|
|
265
|
+
* `runTestModule` then installs `describe/it/test/expect` as globals, invokes
|
|
266
|
+
* the factory (which registers all tests), runs them, and returns a `RunReport`.
|
|
267
|
+
*
|
|
268
|
+
* ## Why a factory wrapper is needed
|
|
269
|
+
*
|
|
270
|
+
* Naively adding the runtime to `entryPoints` and bundling the user file would
|
|
271
|
+
* fail for two reasons:
|
|
272
|
+
* 1. `describe/it/test/expect` from the runtime are module-local in the IIFE
|
|
273
|
+
* scope. The user's top-level `describe(...)` calls expect them as globals —
|
|
274
|
+
* they are not globals until `runTestModule` installs them.
|
|
275
|
+
* 2. Even with globals pre-installed, the user file runs at IIFE-evaluation
|
|
276
|
+
* time, before the RPC layer calls `runTestModule` to reset state and start
|
|
277
|
+
* the test clock.
|
|
278
|
+
*
|
|
279
|
+
* The factory approach solves both: the user's registration code is deferred
|
|
280
|
+
* into a function that `runTestModule` calls AFTER installing the globals.
|
|
281
|
+
*
|
|
282
|
+
* ## Factory extraction algorithm
|
|
283
|
+
*
|
|
284
|
+
* The `userFactoryPlugin` reads the user file and splits lines into:
|
|
285
|
+
* - **top-level**: `import …` and re-export lines — kept at module scope
|
|
286
|
+
* (the only valid position for static `import` in ESM).
|
|
287
|
+
* - **body**: all other statements — moved into the body of the exported
|
|
288
|
+
* `__userFactory` async function.
|
|
289
|
+
*
|
|
290
|
+
* esbuild processes the re-generated module, following each static import
|
|
291
|
+
* through the normal dependency graph (including the SDK-redirect plugin).
|
|
292
|
+
*
|
|
293
|
+
* ## SDK redirect
|
|
294
|
+
*
|
|
295
|
+
* Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via
|
|
296
|
+
* the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that
|
|
297
|
+
* `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.
|
|
166
298
|
*
|
|
167
299
|
* SECRET-HANDLING: the returned bundle code is caller-managed; never log it.
|
|
168
300
|
*/
|
|
@@ -212,10 +344,90 @@ module.exports = __proxy;
|
|
|
212
344
|
};
|
|
213
345
|
}
|
|
214
346
|
/**
|
|
347
|
+
* esbuild plugin that transforms the user test file into a module that exports
|
|
348
|
+
* an async `__userFactory` function. The factory defers the user's top-level
|
|
349
|
+
* test registration code (describe/it/test calls) so it only runs when
|
|
350
|
+
* `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has
|
|
351
|
+
* installed describe/it/test/expect as globals.
|
|
352
|
+
*
|
|
353
|
+
* Algorithm:
|
|
354
|
+
* - Lines matching import declarations or re-export statements are kept at
|
|
355
|
+
* module top-level (the only valid ESM position for static `import`).
|
|
356
|
+
* - All other lines (describe/it/test calls, local declarations, etc.) are
|
|
357
|
+
* moved into the body of the exported async factory function.
|
|
358
|
+
*
|
|
359
|
+
* This preserves SDK import resolution (the sdk-redirect plugin processes
|
|
360
|
+
* top-level imports normally) while deferring test registration to the factory.
|
|
361
|
+
*/
|
|
362
|
+
function userFactoryPlugin(absPath) {
|
|
363
|
+
const NAMESPACE = "user-test-factory";
|
|
364
|
+
return {
|
|
365
|
+
name: "user-test-factory",
|
|
366
|
+
setup(build) {
|
|
367
|
+
build.onResolve({ filter: /^user-test-factory$/ }, () => ({
|
|
368
|
+
path: absPath,
|
|
369
|
+
namespace: NAMESPACE
|
|
370
|
+
}));
|
|
371
|
+
build.onLoad({
|
|
372
|
+
filter: /.*/,
|
|
373
|
+
namespace: NAMESPACE
|
|
374
|
+
}, async (args) => {
|
|
375
|
+
const lines = (await fs.readFile(args.path, "utf8")).split("\n");
|
|
376
|
+
const topLevelLines = [];
|
|
377
|
+
const bodyLines = [];
|
|
378
|
+
const EXPORT_DECLARATION_RE = /^(export\s+)(default\s+|async\s+function\s+|function\s+|class\s+|const\s+|let\s+|var\s+)/;
|
|
379
|
+
for (const line of lines) {
|
|
380
|
+
const trimmed = line.trimStart();
|
|
381
|
+
const indent = line.slice(0, line.length - trimmed.length);
|
|
382
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("import{") || trimmed.startsWith("import'") || trimmed.startsWith("import\"")) topLevelLines.push(line);
|
|
383
|
+
else if (trimmed.startsWith("export ")) if (trimmed.match(EXPORT_DECLARATION_RE)) bodyLines.push(indent + trimmed.slice(7));
|
|
384
|
+
else topLevelLines.push(line);
|
|
385
|
+
else bodyLines.push(line);
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
contents: [
|
|
389
|
+
...topLevelLines,
|
|
390
|
+
"",
|
|
391
|
+
"// biome-ignore lint: generated factory wrapper",
|
|
392
|
+
"export default async function __userFactory(): Promise<void> {",
|
|
393
|
+
...bodyLines.map((l) => ` ${l}`),
|
|
394
|
+
"}"
|
|
395
|
+
].join("\n"),
|
|
396
|
+
loader: "ts",
|
|
397
|
+
resolveDir: path.dirname(absPath)
|
|
398
|
+
};
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Returns the absolute path to the co-located runtime module.
|
|
405
|
+
*
|
|
406
|
+
* In the source tree (running via tsx / ts-node) the file is `runtime.ts`.
|
|
407
|
+
* After `tsdown` compiles to `dist/test-runner/`, it becomes `runtime.js`.
|
|
408
|
+
* We try both extensions to support both environments.
|
|
409
|
+
*/
|
|
410
|
+
function getRuntimePath() {
|
|
411
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
412
|
+
for (const ext of [".ts", ".js"]) {
|
|
413
|
+
const candidate = path.join(dir, `runtime${ext}`);
|
|
414
|
+
try {
|
|
415
|
+
accessSync(candidate);
|
|
416
|
+
return candidate;
|
|
417
|
+
} catch {}
|
|
418
|
+
}
|
|
419
|
+
return path.join(dir, "runtime.js");
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
215
422
|
* Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.
|
|
216
423
|
*
|
|
217
|
-
* The IIFE installs `window.__testBundle` (or the custom `globalName`) with
|
|
218
|
-
* `runTestModule`
|
|
424
|
+
* The IIFE installs `window.__testBundle` (or the custom `globalName`) with:
|
|
425
|
+
* - `runTestModule` — the runtime entry (from `runtime.ts`).
|
|
426
|
+
* - `__userFactory` — an async function wrapping the user's test registration
|
|
427
|
+
* code so it runs AFTER `runTestModule` installs the globals.
|
|
428
|
+
*
|
|
429
|
+
* Callers (rpc.ts) invoke:
|
|
430
|
+
* `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`
|
|
219
431
|
*
|
|
220
432
|
* @param absPath - Absolute path to the user test file.
|
|
221
433
|
* @param opts - Optional bundling overrides.
|
|
@@ -223,17 +435,29 @@ module.exports = __proxy;
|
|
|
223
435
|
async function bundleTestFile(absPath, opts) {
|
|
224
436
|
const globalName = opts?.globalName ?? "__testBundle";
|
|
225
437
|
const extraExternals = opts?.extraExternals ?? [];
|
|
226
|
-
const
|
|
227
|
-
|
|
438
|
+
const esbuild = await import("esbuild");
|
|
439
|
+
const runtimePath = getRuntimePath();
|
|
440
|
+
const wrapperContent = [
|
|
441
|
+
`import { runTestModule } from ${JSON.stringify(runtimePath)};`,
|
|
442
|
+
`import __userFactory from "user-test-factory";`,
|
|
443
|
+
`export { runTestModule, __userFactory };`
|
|
444
|
+
].join("\n");
|
|
445
|
+
const result = await esbuild.build({
|
|
446
|
+
stdin: {
|
|
447
|
+
contents: wrapperContent,
|
|
448
|
+
loader: "ts",
|
|
449
|
+
resolveDir: path.dirname(absPath)
|
|
450
|
+
},
|
|
228
451
|
bundle: true,
|
|
229
452
|
format: "iife",
|
|
230
453
|
globalName,
|
|
231
454
|
platform: "browser",
|
|
232
455
|
target: "es2022",
|
|
233
456
|
write: false,
|
|
234
|
-
plugins: [sdkRedirectPlugin()],
|
|
457
|
+
plugins: [userFactoryPlugin(absPath), sdkRedirectPlugin()],
|
|
235
458
|
external: extraExternals,
|
|
236
|
-
treeShaking: true
|
|
459
|
+
treeShaking: true,
|
|
460
|
+
footer: { js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};` }
|
|
237
461
|
});
|
|
238
462
|
const warnings = result.warnings.map((w) => `${path.relative(process.cwd(), w.location?.file ?? "")}:${w.location?.line ?? "?"}: ${w.text}`);
|
|
239
463
|
const outputFile = result.outputFiles?.[0];
|
|
@@ -260,7 +484,7 @@ const DEFAULT_TIMEOUT_MS = 3e4;
|
|
|
260
484
|
* SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.
|
|
261
485
|
*/
|
|
262
486
|
function buildRunTestsExpression(bundleCode) {
|
|
263
|
-
return `(async () => { try { ${bundleCode} } catch(e) { return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)}); } if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function') { return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule is not a function'}); } try { const report = await globalThis.__testBundle.runTestModule(); return JSON.stringify({ok:true,value:report}); } catch(e) { return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)}); }})()`;
|
|
487
|
+
return `(async () => { try { ${bundleCode} } catch(e) { return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)}); } if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function' || typeof globalThis.__testBundle.__userFactory !== 'function') { return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule or __userFactory is not a function'}); } try { const report = await globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory); return JSON.stringify({ok:true,value:report}); } catch(e) { return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)}); }})()`;
|
|
264
488
|
}
|
|
265
489
|
/**
|
|
266
490
|
* Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`
|
|
@@ -393,14 +617,15 @@ async function runTestFilesOverRelay(connection, files, opts) {
|
|
|
393
617
|
//#endregion
|
|
394
618
|
//#region src/test-runner/cli.ts
|
|
395
619
|
/**
|
|
396
|
-
* `devtools-test` CLI
|
|
397
|
-
*
|
|
398
|
-
* Parses argv, prints usage, and delegates to `runTestFilesOverRelay` when
|
|
399
|
-
* a live CDP connection is provided. The relay connection wiring
|
|
400
|
-
* (attach → run → detach) is tracked in issue #645 / #646.
|
|
620
|
+
* `devtools-test` CLI.
|
|
401
621
|
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
622
|
+
* Shares test-file discovery with the `run_tests` MCP tool (`discoverTestFiles`)
|
|
623
|
+
* and exposes `runWithConnection` — the pure run core that bundles, injects, and
|
|
624
|
+
* collects each file over a CDP connection. Today the run path that has a live
|
|
625
|
+
* connection is the `run_tests` MCP tool (it runs these files against the
|
|
626
|
+
* daemon's attached page); the CLI's own standalone relay attach (resolve CDP
|
|
627
|
+
* URL → attach → run → close) is not wired yet, so `main()` resolves the matched
|
|
628
|
+
* files and points the operator at the MCP tool.
|
|
404
629
|
*
|
|
405
630
|
* NOTE: no shebang in this source file — the tsdown entry's `banner` option
|
|
406
631
|
* injects `#!/usr/bin/env node` into the compiled output (same pattern as
|
|
@@ -421,12 +646,10 @@ DESCRIPTION
|
|
|
421
646
|
window.__sdk), injects the bundle into the attached WebView via
|
|
422
647
|
Runtime.evaluate, and returns a RunReport.
|
|
423
648
|
|
|
424
|
-
A live CDP relay connection must be active before running tests.
|
|
425
|
-
|
|
426
|
-
the
|
|
427
|
-
|
|
428
|
-
Full Vitest pool integration and the \`run_tests\` MCP tool are tracked in
|
|
429
|
-
issues #645 and #646 respectively. This MVP provides the transport layer.
|
|
649
|
+
A live CDP relay connection must be active before running tests. Use the
|
|
650
|
+
\`run_tests\` MCP tool (via \`devtools-mcp\` / \`/ait debug\`) to run these files
|
|
651
|
+
against an attached page — the CLI's own standalone relay attach is not wired
|
|
652
|
+
yet (it currently resolves the matched files and defers to that tool).
|
|
430
653
|
|
|
431
654
|
EXAMPLE
|
|
432
655
|
devtools-test 'src/**/*.phone.test.ts' --timeout 60000
|
|
@@ -434,11 +657,12 @@ EXAMPLE
|
|
|
434
657
|
`.trimStart();
|
|
435
658
|
/**
|
|
436
659
|
* Runs `files` over `connection` and returns the aggregate report.
|
|
437
|
-
* This pure function is the testable core of the CLI
|
|
438
|
-
* `
|
|
660
|
+
* This pure function is the testable core of the CLI (and is what the
|
|
661
|
+
* `run_tests` MCP tool calls against the daemon's attached connection); it is
|
|
662
|
+
* separate from `main()` so tests can call it without spawning a subprocess.
|
|
439
663
|
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
664
|
+
* A standalone CLI relay attach/detach lifecycle (connect via Chii relay URL,
|
|
665
|
+
* `enableDomains`, run, then close) is not wired into `main()` yet.
|
|
442
666
|
*/
|
|
443
667
|
async function runWithConnection(connection, files, opts) {
|
|
444
668
|
const report = await runTestFilesOverRelay(connection, files, opts);
|
|
@@ -451,8 +675,10 @@ async function runWithConnection(connection, files, opts) {
|
|
|
451
675
|
/**
|
|
452
676
|
* CLI entry point.
|
|
453
677
|
*
|
|
454
|
-
*
|
|
455
|
-
* (resolve CDP URL, attach, run, close) is
|
|
678
|
+
* Resolves the matched test files and prints a "relay attach required" notice:
|
|
679
|
+
* the CLI's own standalone relay attach (resolve CDP URL, attach, run, close) is
|
|
680
|
+
* not wired yet, so today these files run via the `run_tests` MCP tool against
|
|
681
|
+
* the daemon's attached page.
|
|
456
682
|
*/
|
|
457
683
|
async function main$1(argv = process.argv.slice(2)) {
|
|
458
684
|
let parsed;
|
|
@@ -483,7 +709,7 @@ async function main$1(argv = process.argv.slice(2)) {
|
|
|
483
709
|
process.exitCode = 1;
|
|
484
710
|
return;
|
|
485
711
|
}
|
|
486
|
-
process.stderr.write(`devtools-test: matched ${files.length} test file(s), but direct CLI relay attach is not yet wired.\n Use the devtools-mcp server (\`devtools-mcp\`) to start a debug session,\n then the \`run_tests\` MCP tool to run these files against the attached page.\n
|
|
712
|
+
process.stderr.write(`devtools-test: matched ${files.length} test file(s), but direct CLI relay attach is not yet wired.\n Use the devtools-mcp server (\`devtools-mcp\`) to start a debug session,\n then the \`run_tests\` MCP tool to run these files against the attached page.\n`);
|
|
487
713
|
process.exitCode = 1;
|
|
488
714
|
}
|
|
489
715
|
if (import.meta.url === new URL(process.argv[1], "file://").href) main$1().catch((e) => {
|
|
@@ -680,7 +906,7 @@ function logError(event, fields = {}) {
|
|
|
680
906
|
* Attach reliability (#281):
|
|
681
907
|
* `refreshTargets()` emits an internal 'target:attached' event whenever a
|
|
682
908
|
* new target is added to the relay. `waitForFirstTarget()` awaits that event
|
|
683
|
-
* (with a polling-interval fallback) so `
|
|
909
|
+
* (with a polling-interval fallback) so `start_attach`'s attach wait
|
|
684
910
|
* resolves deterministically rather than racing between polling rounds.
|
|
685
911
|
*/
|
|
686
912
|
/** Max events retained per domain ring buffer. */
|
|
@@ -1820,9 +2046,10 @@ function isCompatMode() {
|
|
|
1820
2046
|
return process.env.AIT_MCP_COMPAT === "chrome-devtools";
|
|
1821
2047
|
}
|
|
1822
2048
|
/**
|
|
1823
|
-
* Maps `McpEnvironment` to `EnvelopeEnv`.
|
|
1824
|
-
* union (`mock | relay-dev | relay-live`
|
|
1825
|
-
* named export for surface stability if
|
|
2049
|
+
* Maps `McpEnvironment` to `EnvelopeEnv`. These are now the same 3-value
|
|
2050
|
+
* union (`mock | relay-dev | relay-mobile`; `relay-live` removed in #665),
|
|
2051
|
+
* so this is identity — kept as a named export for surface stability if
|
|
2052
|
+
* envelope env diverges in the future.
|
|
1826
2053
|
*/
|
|
1827
2054
|
function toEnvelopeEnv(env) {
|
|
1828
2055
|
return env;
|
|
@@ -1858,9 +2085,9 @@ function wrapEnvelope(data, ctx) {
|
|
|
1858
2085
|
//#endregion
|
|
1859
2086
|
//#region src/mcp/environment.ts
|
|
1860
2087
|
/**
|
|
1861
|
-
* Returns `true` when the environment is any relay variant (`relay-dev
|
|
1862
|
-
* `relay-
|
|
1863
|
-
*
|
|
2088
|
+
* Returns `true` when the environment is any relay variant (`relay-dev` or
|
|
2089
|
+
* `relay-mobile`). Use this instead of `env === 'relay'` for tier checks —
|
|
2090
|
+
* every relay env surfaces the Tier B / relay-only tool set.
|
|
1864
2091
|
*
|
|
1865
2092
|
* Written as an exhaustive switch so a future `McpEnvironment` member that is
|
|
1866
2093
|
* missing an arm is a TS compile error rather than a silent `false`.
|
|
@@ -1868,78 +2095,49 @@ function wrapEnvelope(data, ctx) {
|
|
|
1868
2095
|
function isRelayEnv(env) {
|
|
1869
2096
|
switch (env) {
|
|
1870
2097
|
case "relay-dev":
|
|
1871
|
-
case "relay-live":
|
|
1872
2098
|
case "relay-mobile": return true;
|
|
1873
2099
|
case "mock": return false;
|
|
1874
2100
|
}
|
|
1875
2101
|
}
|
|
1876
2102
|
/**
|
|
1877
|
-
* Returns `true` when the environment is the LIVE relay (`relay-live`).
|
|
1878
|
-
* This is the guard condition for side-effect tool protection. `relay-mobile`
|
|
1879
|
-
* is a dev-intent env (env 2 PWA) and is NOT live.
|
|
1880
|
-
*/
|
|
1881
|
-
function isLiveRelayEnv(env) {
|
|
1882
|
-
return env === "relay-live";
|
|
1883
|
-
}
|
|
1884
|
-
/**
|
|
1885
2103
|
* Maps the `McpEnvironment` union to the legacy two-value union
|
|
1886
2104
|
* (`'mock' | 'relay'`) for backward-compatible fields in diagnostics output.
|
|
1887
|
-
* Every relay variant (
|
|
2105
|
+
* Every relay variant (`relay-dev`, `relay-mobile`) collapses to `'relay'`.
|
|
2106
|
+
* Written as an exhaustive switch so a missing arm is a TS compile error.
|
|
1888
2107
|
*/
|
|
1889
2108
|
function toLegacyEnv(env) {
|
|
1890
|
-
|
|
1891
|
-
|
|
2109
|
+
switch (env) {
|
|
2110
|
+
case "mock": return "mock";
|
|
2111
|
+
case "relay-dev":
|
|
2112
|
+
case "relay-mobile": return "relay";
|
|
2113
|
+
}
|
|
1892
2114
|
}
|
|
1893
2115
|
/**
|
|
1894
|
-
* Reconstructs the
|
|
1895
|
-
* orthogonal signals (issues #348, #378):
|
|
2116
|
+
* Reconstructs the three-value `McpEnvironment` output string from the
|
|
2117
|
+
* orthogonal signals (issues #348, #378, #665):
|
|
1896
2118
|
*
|
|
1897
|
-
* - `kind === 'local'`
|
|
1898
|
-
* - `kind === 'relay'` &&
|
|
1899
|
-
* - `kind === 'relay'` &&
|
|
1900
|
-
* - `kind === 'relay'` && !liveIntent && origin intoss/undefined → `'relay-dev'`
|
|
2119
|
+
* - `kind === 'local'` → `'mock'`
|
|
2120
|
+
* - `kind === 'relay'` && origin 'external-pwa' → `'relay-mobile'`
|
|
2121
|
+
* - `kind === 'relay'` && origin intoss/undefined → `'relay-dev'`
|
|
1901
2122
|
*
|
|
1902
2123
|
* `relayOrigin` is the booted-family discriminator (NOT sniffed from the URL)
|
|
1903
2124
|
* that distinguishes the env-2 external-PWA relay (`relay-mobile`) from the
|
|
1904
2125
|
* intoss-private dog-food relay (`relay-dev`); both are `kind: 'relay'`.
|
|
1905
2126
|
*
|
|
2127
|
+
* `relay-live` (env 4) has been removed (#665). `liveIntent` parameter is gone.
|
|
2128
|
+
*
|
|
1906
2129
|
* Pure — used at every output boundary (envelope `meta.env`, `get_debug_status`,
|
|
1907
2130
|
* `measure_safe_area` provenance) so the surface never sniffs a URL again.
|
|
1908
2131
|
*
|
|
1909
2132
|
* Written switch-style so a missing arm is a TS compile error (never falls
|
|
1910
2133
|
* through to a default).
|
|
1911
2134
|
*/
|
|
1912
|
-
function deriveEnvironment(kind,
|
|
2135
|
+
function deriveEnvironment(kind, relayOrigin) {
|
|
1913
2136
|
switch (kind) {
|
|
1914
2137
|
case "local": return "mock";
|
|
1915
|
-
case "relay":
|
|
1916
|
-
if (liveIntent) return "relay-live";
|
|
1917
|
-
return relayOrigin === "external-pwa" ? "relay-mobile" : "relay-dev";
|
|
2138
|
+
case "relay": return relayOrigin === "external-pwa" ? "relay-mobile" : "relay-dev";
|
|
1918
2139
|
}
|
|
1919
2140
|
}
|
|
1920
|
-
/**
|
|
1921
|
-
* Module-level `relay-dev` vs `relay-live` intent bit (issue #348).
|
|
1922
|
-
*
|
|
1923
|
-
* Armed by `start_debug({ mode: 'relay-live' })` (and seeded at boot by the
|
|
1924
|
-
* deprecated `MCP_ENV=relay-live` alias). Disarming is implicit: when the
|
|
1925
|
-
* active connection becomes local, the LIVE guard reads
|
|
1926
|
-
* `connection.kind === 'relay' && liveIntent`, so a stale `true` bit is inert.
|
|
1927
|
-
*
|
|
1928
|
-
* SECRET-HANDLING: this is a boolean — never a secret. Safe to read in logs.
|
|
1929
|
-
*/
|
|
1930
|
-
let liveIntent = false;
|
|
1931
|
-
/** Returns the current `liveIntent` bit. */
|
|
1932
|
-
function getLiveIntent() {
|
|
1933
|
-
return liveIntent;
|
|
1934
|
-
}
|
|
1935
|
-
/**
|
|
1936
|
-
* Sets the `liveIntent` bit. Called by `start_debug` (true for `relay-live`,
|
|
1937
|
-
* false for every other mode) and once at boot by the `MCP_ENV=relay-live`
|
|
1938
|
-
* deprecated alias.
|
|
1939
|
-
*/
|
|
1940
|
-
function setLiveIntent(value) {
|
|
1941
|
-
liveIntent = value;
|
|
1942
|
-
}
|
|
1943
2141
|
//#endregion
|
|
1944
2142
|
//#region src/mcp/errors.ts
|
|
1945
2143
|
/**
|
|
@@ -1966,12 +2164,12 @@ function mcpError(message) {
|
|
|
1966
2164
|
* (예: `derived:kind=relay,liveIntent=true`).
|
|
1967
2165
|
*/
|
|
1968
2166
|
function tierRejectionError(toolName, requiredEnv, currentEnv, reason) {
|
|
1969
|
-
return mcpError(`${`${toolName}은 ${requiredEnv === "relay" ? "relay (실기기 연결)" : "mock (로컬 브라우저)"} 환경에서만 사용할 수 있습니다. 현재 환경: ${currentEnv === "relay" ? "relay" : "mock"} (${reason}). ${requiredEnv === "relay" ? "relay로 전환하려면 MCP_ENV=relay 설정 후 서버를 재시작하고
|
|
2167
|
+
return mcpError(`${`${toolName}은 ${requiredEnv === "relay" ? "relay (실기기 연결)" : "mock (로컬 브라우저)"} 환경에서만 사용할 수 있습니다. 현재 환경: ${currentEnv === "relay" ? "relay" : "mock"} (${reason}). ${requiredEnv === "relay" ? "relay로 전환하려면 MCP_ENV=relay 설정 후 서버를 재시작하고 start_attach → QR 스캔으로 실기기를 attach하세요." : "mock으로 전환하려면 MCP_ENV=mock 설정 후 서버를 재시작하세요."}`}\n\n${`tool ${toolName} is available only in ${requiredEnv}. Current environment is ${currentEnv} (${reason}).`}`);
|
|
1970
2168
|
}
|
|
1971
2169
|
/**
|
|
1972
2170
|
* 상태 1: tunnel 미가동 — cloudflared 터널이 아직 뜨지 않았다.
|
|
1973
2171
|
*
|
|
1974
|
-
* `
|
|
2172
|
+
* `start_attach` 호출 시 tunnel.up === false 인 경우.
|
|
1975
2173
|
*/
|
|
1976
2174
|
function tunnelDownError() {
|
|
1977
2175
|
return mcpError("cloudflared 터널이 안 떠 있습니다. MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.");
|
|
@@ -1982,7 +2180,7 @@ function tunnelDownError() {
|
|
|
1982
2180
|
* enableDomains()가 "No mini-app page attached" 에러를 던질 때.
|
|
1983
2181
|
*/
|
|
1984
2182
|
function pageMissingError(toolName) {
|
|
1985
|
-
return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 attach 안 됨. dog-food 번들 배포 후
|
|
2183
|
+
return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 attach 안 됨. dog-food 번들 배포 후 start_attach를 호출해 QR 생성 + attach까지 진행하세요: \`ait deploy --scheme-only\` → \`start_attach(scheme_url)\` → QR 스캔.`);
|
|
1986
2184
|
}
|
|
1987
2185
|
/**
|
|
1988
2186
|
* 상태 3: page crash — 연결됐던 페이지가 crash/destroy됐다.
|
|
@@ -1991,7 +2189,7 @@ function pageMissingError(toolName) {
|
|
|
1991
2189
|
* 던질 때 이 메시지를 사용한다.
|
|
1992
2190
|
*/
|
|
1993
2191
|
function pageCrashError(toolName) {
|
|
1994
|
-
return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 crash됐습니다. 토스 앱을 재실행한 뒤
|
|
2192
|
+
return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 crash됐습니다. 토스 앱을 재실행한 뒤 start_attach → QR 스캔으로 재attach하세요.`);
|
|
1995
2193
|
}
|
|
1996
2194
|
/**
|
|
1997
2195
|
* 상태 4: SDK 부재 — window.__sdkCall이 주입되지 않았다.
|
|
@@ -2013,23 +2211,6 @@ function sdkAbsentError(toolName, isLocal = false) {
|
|
|
2013
2211
|
return mcpError(`${prefix}window.__sdkCall이 주입되지 않았습니다 (dog-food 빌드가 아닙니다). dog-food 채널(intoss-private)로 재배포 후 QR을 다시 스캔하세요: \`ait build && aitcc app deploy\`.`);
|
|
2014
2212
|
}
|
|
2015
2213
|
/**
|
|
2016
|
-
* relay-live 환경에서 side-effect 도구(`call_sdk`, `evaluate`)를 `confirm: true`
|
|
2017
|
-
* 없이 호출했을 때 반환하는 거부 메시지.
|
|
2018
|
-
*
|
|
2019
|
-
* 다음 행동을 두 가지로 제시한다:
|
|
2020
|
-
* 1. 같은 호출에 `confirm: true` 인자를 추가해 재시도.
|
|
2021
|
-
* 2. 읽기 전용 환경(relay-dev, mock)으로 전환.
|
|
2022
|
-
*/
|
|
2023
|
-
function liveGuardError(toolName) {
|
|
2024
|
-
return mcpError(`[LIVE relay guard] ${toolName}은 현재 relay-live(실 출시 런타임) 세션에서 side-effect 호출입니다. 실유저에게 영향을 줄 수 있어 명시적 동의가 필요합니다.
|
|
2025
|
-
|
|
2026
|
-
다음 중 하나를 선택하세요:
|
|
2027
|
-
1. \`confirm: true\` 인자를 추가해 재호출: ${toolName}(…, confirm: true)\n 2. 읽기 전용 도구(list_pages, list_console_messages, take_screenshot 등)를 사용하세요.
|
|
2028
|
-
3. dog-food 빌드(relay-dev 환경)에서 먼저 검증 후 live에 적용하세요.
|
|
2029
|
-
|
|
2030
|
-
live-guard: MCP_ENV=relay-live + confirm: true missing`);
|
|
2031
|
-
}
|
|
2032
|
-
/**
|
|
2033
2214
|
* relay WebSocket 연결이 끊겼을 때 — 크래시가 아닌 네트워크/프로세스 종료.
|
|
2034
2215
|
*/
|
|
2035
2216
|
function relayDisconnectError(toolName) {
|
|
@@ -2539,7 +2720,7 @@ const en = {
|
|
|
2539
2720
|
"dashboard.tunnel.up": "Connected",
|
|
2540
2721
|
"dashboard.tunnel.down": "Disconnected",
|
|
2541
2722
|
"dashboard.attach.section": "Attach QR",
|
|
2542
|
-
"dashboard.attach.hint": "Call the
|
|
2723
|
+
"dashboard.attach.hint": "Call the start_attach MCP tool to show the QR here.",
|
|
2543
2724
|
"dashboard.attach.tunnelDown": "Relay disconnected — this QR is no longer valid. Restart the relay, then regenerate the QR.",
|
|
2544
2725
|
"dashboard.pages.section": "Connected Pages",
|
|
2545
2726
|
"dashboard.pages.empty": "No attached pages",
|
|
@@ -2557,7 +2738,6 @@ const en = {
|
|
|
2557
2738
|
"attach.url.section": "URL (fallback)",
|
|
2558
2739
|
"attach.mode.sandbox": "env 2 — AITC Sandbox App (PWA)",
|
|
2559
2740
|
"attach.mode.intossDev": "env 3 — intoss-private relay dev",
|
|
2560
|
-
"attach.mode.intossLive": "env 4 — intoss live relay debug",
|
|
2561
2741
|
"attach.sandbox.step1": "Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).",
|
|
2562
2742
|
"attach.sandbox.step2": "Scan this QR code with <strong>\"Scan QR with camera\"</strong> inside the launcher.",
|
|
2563
2743
|
"attach.sandbox.step3": "The mini-app opens fullscreen and the debug session attaches automatically.",
|
|
@@ -2573,7 +2753,6 @@ const en = {
|
|
|
2573
2753
|
"attach.intoss.faq.prepare": "<strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter",
|
|
2574
2754
|
"attach.intoss.faq.chii": "<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import",
|
|
2575
2755
|
"attach.intoss.faq.totp": "<strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server",
|
|
2576
|
-
"attach.intoss.faq.liveReadOnly": "<strong>LIVE session is read-only</strong> — <code>call_sdk</code>/<code>evaluate</code> require an explicit <code>confirm</code>",
|
|
2577
2756
|
"launcher.title": "AITC DevTools Launcher",
|
|
2578
2757
|
"launcher.description": "Scan the terminal QR code or paste the tunnel URL.",
|
|
2579
2758
|
"launcher.installCta": "Install launcher to your phone",
|
|
@@ -2776,7 +2955,7 @@ const tables = {
|
|
|
2776
2955
|
"dashboard.tunnel.up": "연결됨",
|
|
2777
2956
|
"dashboard.tunnel.down": "끊어짐",
|
|
2778
2957
|
"dashboard.attach.section": "Attach QR",
|
|
2779
|
-
"dashboard.attach.hint": "
|
|
2958
|
+
"dashboard.attach.hint": "start_attach MCP tool을 호출하면 QR이 여기에 표시됩니다.",
|
|
2780
2959
|
"dashboard.attach.tunnelDown": "relay 연결이 끊겼습니다 — 이 QR은 더 이상 유효하지 않습니다. relay를 재시작한 뒤 QR을 다시 생성하세요.",
|
|
2781
2960
|
"dashboard.pages.section": "연결된 Pages",
|
|
2782
2961
|
"dashboard.pages.empty": "attach된 페이지 없음",
|
|
@@ -2794,7 +2973,6 @@ const tables = {
|
|
|
2794
2973
|
"attach.url.section": "URL (fallback)",
|
|
2795
2974
|
"attach.mode.sandbox": "환경 2 — AITC Sandbox App (PWA)",
|
|
2796
2975
|
"attach.mode.intossDev": "환경 3 — intoss-private relay dev",
|
|
2797
|
-
"attach.mode.intossLive": "환경 4 — intoss live relay debug",
|
|
2798
2976
|
"attach.sandbox.step1": "홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).",
|
|
2799
2977
|
"attach.sandbox.step2": "launcher 안의 <strong>\"QR 카메라로 스캔\"</strong>으로 이 QR 코드를 스캔하세요.",
|
|
2800
2978
|
"attach.sandbox.step3": "미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.",
|
|
@@ -2810,7 +2988,6 @@ const tables = {
|
|
|
2810
2988
|
"attach.intoss.faq.prepare": "<strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인",
|
|
2811
2989
|
"attach.intoss.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
|
|
2812
2990
|
"attach.intoss.faq.totp": "<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인",
|
|
2813
|
-
"attach.intoss.faq.liveReadOnly": "<strong>LIVE 세션은 read-only입니다</strong> — <code>call_sdk</code>/<code>evaluate</code> 실행에는 명시적 <code>confirm</code>이 필요합니다",
|
|
2814
2991
|
"launcher.title": "AITC DevTools Launcher",
|
|
2815
2992
|
"launcher.description": "터미널 QR을 스캔하거나 URL을 입력하세요.",
|
|
2816
2993
|
"launcher.installCta": "폰에 런처 설치하기",
|
|
@@ -3069,7 +3246,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
3069
3246
|
}
|
|
3070
3247
|
.inspector-link:hover { background: #388bfd; }
|
|
3071
3248
|
.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
|
|
3072
|
-
</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
|
|
3249
|
+
</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></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>`;
|
|
3073
3250
|
const dashboardChromeHtmlEn = `<!DOCTYPE html>
|
|
3074
3251
|
<html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>AIT Debug Dashboard</title><style>
|
|
3075
3252
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -3250,7 +3427,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
3250
3427
|
}
|
|
3251
3428
|
.inspector-link:hover { background: #388bfd; }
|
|
3252
3429
|
.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }
|
|
3253
|
-
</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
|
|
3430
|
+
</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></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>`;
|
|
3254
3431
|
/** Map from Locale to the precompiled dashboard chrome string. */
|
|
3255
3432
|
const dashboardChromeByLocale = {
|
|
3256
3433
|
ko: dashboardChromeHtmlKo,
|
|
@@ -3287,9 +3464,6 @@ function buildModeLabel(mode, s) {
|
|
|
3287
3464
|
case "relay-dev":
|
|
3288
3465
|
label = s("attach.mode.intossDev");
|
|
3289
3466
|
break;
|
|
3290
|
-
case "relay-live":
|
|
3291
|
-
label = s("attach.mode.intossLive");
|
|
3292
|
-
break;
|
|
3293
3467
|
case "mock":
|
|
3294
3468
|
case void 0: return "";
|
|
3295
3469
|
}
|
|
@@ -3563,12 +3737,11 @@ function buildSseScript(strings) {
|
|
|
3563
3737
|
* - __SAFE_LABEL__ : HTML-escaped deploymentId label (intoss family에만 존재)
|
|
3564
3738
|
* - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)
|
|
3565
3739
|
* - __MODE_LABEL__ : 환경 배지 (`<p class="mode-label">…</p>` 또는 빈 문자열, #468)
|
|
3566
|
-
* - __LIVE_FAQ__ : 환경 4 LIVE read-only `<li>` 또는 빈 문자열 (intoss family에만 존재)
|
|
3567
3740
|
* - __INSPECTOR_SECTION__ : "디버그 툴 열기" 버튼 또는 대기 힌트 (#544)
|
|
3568
3741
|
*
|
|
3569
3742
|
* mode-aware 분기 (#468): mode가 `relay-mobile`이면 sandbox family chrome(launcher
|
|
3570
|
-
* PWA 절차), 그 외는 intoss family chrome(토스 앱 절차)을 선택한다.
|
|
3571
|
-
*
|
|
3743
|
+
* PWA 절차), 그 외는 intoss family chrome(토스 앱 절차)을 선택한다.
|
|
3744
|
+
* relay-live (env 4) 제거 (#665) — positive-allowlist kill-switch.
|
|
3572
3745
|
*
|
|
3573
3746
|
* SSE 스크립트도 주입 — `#attach-section` hook이 있으면 `/events` push 때 QR이
|
|
3574
3747
|
* `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#inspector-link`도 SSE push로
|
|
@@ -3581,11 +3754,10 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/a
|
|
|
3581
3754
|
const s = resolveLocaleStrings(locale);
|
|
3582
3755
|
const langSwitcher = buildLangSwitcher(path, params, locale, s);
|
|
3583
3756
|
const family = attachFamilyForMode(mode);
|
|
3584
|
-
const liveFaq = mode === "relay-live" ? `<li>${s("attach.intoss.faq.liveReadOnly")}</li>` : "";
|
|
3585
3757
|
let inspectorSection;
|
|
3586
3758
|
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>`;
|
|
3587
3759
|
else inspectorSection = `<span class="inspector-hint" id="inspector-link">${escapeHtml(s("dashboard.inspector.waiting"))}</span>`;
|
|
3588
|
-
const filled = attachChromeByLocale[locale][family].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__MODE_LABEL__", buildModeLabel(mode, s)).replaceAll("
|
|
3760
|
+
const filled = attachChromeByLocale[locale][family].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__MODE_LABEL__", buildModeLabel(mode, s)).replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl).replaceAll("__INSPECTOR_SECTION__", inspectorSection);
|
|
3589
3761
|
const sseScript = buildSseScript({
|
|
3590
3762
|
tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
|
|
3591
3763
|
tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
|
|
@@ -4212,22 +4384,27 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4212
4384
|
availableIn: "both"
|
|
4213
4385
|
},
|
|
4214
4386
|
{
|
|
4215
|
-
name: "
|
|
4216
|
-
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.
|
|
4387
|
+
name: "start_attach",
|
|
4388
|
+
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. Single entry point to attach a real device: switches the debug mode (if `mode` is given), builds the self-attaching deep-link QR for the active relay environment, and waits for the phone to attach — all in one call (replaces the old attach-URL + start_debug two-step). 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).\n\nmode (optional): pass \"relay-sandbox\" (env 2) or \"relay-staging\" (env 3) to switch the active environment first. When omitted, the current relay environment is used as-is (no switch). Passing \"local-browser\" returns an error — start_attach is relay-only (env 2/3). When the session is already in the requested mode, the switch is skipped.\n\nEnvironment-specific behaviour:\n • env 3 / 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.\n • env 2 / 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 added as name= so the launcher partner bar shows it.\n\nWaits for a page to attach by default (up to wait_timeout_seconds, default 60 s). The server automatically opens the QR dashboard in the OS default browser when running on a local GUI machine — headless/remote environments fall back to the text QR in the tool output.\n\nTOTP auto re-mint: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the attachUrl carries a one-time code (at=<code>) valid for ~3 minutes (the relay gate accepts ±6 TOTP steps). While waiting, start_attach AUTOMATICALLY re-mints a fresh code before the current one expires and refreshes the dashboard QR in place (no browser re-open). You do NOT need to re-call start_attach every time the code would expire — a single call covers the whole wait window. The response includes a `totp` field with `expiresAt` and a `reminted` count of how many fresh codes were issued during the wait. 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 (relay-staging) — passing selfdebug=true there returns an error.",
|
|
4217
4389
|
inputSchema: {
|
|
4218
4390
|
type: "object",
|
|
4219
4391
|
properties: {
|
|
4392
|
+
mode: {
|
|
4393
|
+
type: "string",
|
|
4394
|
+
enum: [
|
|
4395
|
+
"local-browser",
|
|
4396
|
+
"relay-sandbox",
|
|
4397
|
+
"relay-staging"
|
|
4398
|
+
],
|
|
4399
|
+
description: "Optional debug mode to switch into before attaching. \"relay-sandbox\" = env 2 (launcher PWA), \"relay-staging\" = env 3 (intoss-private dog-food). \"local-browser\" returns an error (start_attach is relay-only). Omit to keep the current relay environment."
|
|
4400
|
+
},
|
|
4220
4401
|
scheme_url: {
|
|
4221
4402
|
type: "string",
|
|
4222
4403
|
description: "The intoss-private:// scheme URL from `ait deploy --scheme-only` (must carry _deploymentId). Required for env 3/relay-staging mode. Not used in env 2/relay-sandbox mode (use AIT_TUNNEL_BASE_URL instead). The authority (host) must be the app name (e.g. intoss-private://aitc-sdk-example?_deploymentId=…). Generic values like \"web\" or an empty host indicate a malformed URL."
|
|
4223
4404
|
},
|
|
4224
|
-
wait_for_attach: {
|
|
4225
|
-
type: "boolean",
|
|
4226
|
-
description: "If true, block after returning the QR until a page attaches to the relay (polls listTargets ~1 s interval, default 60 s). On attach, the response includes the attached page list. On timeout, call build_attach_url again to resume polling."
|
|
4227
|
-
},
|
|
4228
4405
|
wait_timeout_seconds: {
|
|
4229
4406
|
type: "number",
|
|
4230
|
-
description: "Maximum seconds to wait
|
|
4407
|
+
description: "Maximum seconds to wait for a page to attach (default 60, range 1–600). Values outside the range or invalid inputs (0, negative, NaN) fall back to the default silently. During the wait the TOTP code is auto re-minted as needed, so a single call covers the whole window."
|
|
4231
4408
|
},
|
|
4232
4409
|
projectRoot: {
|
|
4233
4410
|
type: "string",
|
|
@@ -4235,7 +4412,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4235
4412
|
},
|
|
4236
4413
|
selfdebug: {
|
|
4237
4414
|
type: "boolean",
|
|
4238
|
-
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
|
|
4415
|
+
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 (relay-staging) returns an error. Default: false (omitted)."
|
|
4239
4416
|
}
|
|
4240
4417
|
},
|
|
4241
4418
|
required: []
|
|
@@ -4274,7 +4451,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4274
4451
|
},
|
|
4275
4452
|
{
|
|
4276
4453
|
name: "measure_safe_area",
|
|
4277
|
-
description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel page with window.__ait state) and `relay` (real-device WebView with window.__sdk). The result includes a `source: \"mock\" | \"relay-dev\" | \"relay-
|
|
4454
|
+
description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel page with window.__ait state) and `relay` (real-device WebView with window.__sdk). The result includes a `source: \"mock\" | \"relay-dev\" | \"relay-mobile\"` field so consumers can identify provenance without inspecting payload values. (`relay-mobile` = env 2 real-device PWA over an external relay; `relay-dev` = env 3 dog-food WebView; relay-live/env 4 removed #665.) Use in a relay session (phone attached) to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires a page to be attached — call list_pages first.",
|
|
4278
4455
|
inputSchema: {
|
|
4279
4456
|
type: "object",
|
|
4280
4457
|
properties: {},
|
|
@@ -4284,19 +4461,13 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4284
4461
|
},
|
|
4285
4462
|
{
|
|
4286
4463
|
name: "evaluate",
|
|
4287
|
-
description: "Evaluates an arbitrary JavaScript expression on the attached mini-app page via CDP Runtime.evaluate (returnByValue: true) and returns the result. NOT read-only — the expression can have side effects (DOM mutations, SDK calls, state changes). Requires the relay to be attached — call list_pages first. Throws if the evaluation throws an exception on the page.\n\nSECURITY: expression and result are not redacted — never include secrets or auth tokens in the expression.\n\
|
|
4464
|
+
description: "Evaluates an arbitrary JavaScript expression on the attached mini-app page via CDP Runtime.evaluate (returnByValue: true) and returns the result. NOT read-only — the expression can have side effects (DOM mutations, SDK calls, state changes). Requires the relay to be attached — call list_pages first. Throws if the evaluation throws an exception on the page.\n\nSECURITY: expression and result are not redacted — never include secrets or auth tokens in the expression.\n\nPositive-allowlist kill-switch (#665): this tool is blocked when the attached page is on a non-debug host (apps.tossmini.com / env 4). Only localhost, *.trycloudflare.com, and *.private-apps.tossmini.com are allowed. relay-live (env 4) and the LIVE confirm guard are removed.",
|
|
4288
4465
|
inputSchema: {
|
|
4289
4466
|
type: "object",
|
|
4290
|
-
properties: {
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
},
|
|
4295
|
-
confirm: {
|
|
4296
|
-
type: "boolean",
|
|
4297
|
-
description: "Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge that this expression may have side effects on real/live users. Omitting this in a relay-live session results in a structured rejection error. Has no effect in mock or relay-dev sessions."
|
|
4298
|
-
}
|
|
4299
|
-
},
|
|
4467
|
+
properties: { expression: {
|
|
4468
|
+
type: "string",
|
|
4469
|
+
description: "JavaScript expression to evaluate in the page context."
|
|
4470
|
+
} },
|
|
4300
4471
|
required: ["expression"]
|
|
4301
4472
|
},
|
|
4302
4473
|
availableIn: "both"
|
|
@@ -4316,7 +4487,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4316
4487
|
},
|
|
4317
4488
|
{
|
|
4318
4489
|
name: "call_sdk",
|
|
4319
|
-
description: "Calls a dog-food SDK method via the window.__sdkCall bridge (exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). On env 3/4 (real device relay) this hits the real SDK; on env 1 (local mock) and env 2 (PWA relay — real WebKit, mock SDK) it hits the mock SDK. Requires the relay to be attached — call list_pages first. Returns {ok: true, value} on success or {ok: false, error} on failure. If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], the result also includes `recentException` for crash triage. Returns a clear error if window.__sdkCall is not available — on relay (env 3/4) that means a non-dog-food bundle (redeploy via `ait build && aitcc app deploy`); on local (--target=local, env 1) it means the dev bridge is not installed (start the dev server with `pnpm dev`).\n\nSECURITY: method name, args, and result value are not redacted — never include secrets.\n\
|
|
4490
|
+
description: "Calls a dog-food SDK method via the window.__sdkCall bridge (exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). On env 3/4 (real device relay) this hits the real SDK; on env 1 (local mock) and env 2 (PWA relay — real WebKit, mock SDK) it hits the mock SDK. Requires the relay to be attached — call list_pages first. Returns {ok: true, value} on success or {ok: false, error} on failure. If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], the result also includes `recentException` for crash triage. Returns a clear error if window.__sdkCall is not available — on relay (env 3/4) that means a non-dog-food bundle (redeploy via `ait build && aitcc app deploy`); on local (--target=local, env 1) it means the dev bridge is not installed (start the dev server with `pnpm dev`).\n\nSECURITY: method name, args, and result value are not redacted — never include secrets.\n\nPositive-allowlist kill-switch (#665): blocked when the attached page is on a non-debug host (apps.tossmini.com / env 4). relay-live and the LIVE guard removed.\n\nIMPORTANT — 인자 시그니처 (잘못된 인자로 호출하면 토스 앱 crash 위험):\n setDeviceOrientation: call_sdk(\"setDeviceOrientation\", [{ type: \"landscape\" }]) // NOT \"landscape\"\n setIosSwipeGestureEnabled: call_sdk(\"setIosSwipeGestureEnabled\", [{ isEnabled: false }])\n setSecureScreen: call_sdk(\"setSecureScreen\", [{ enabled: true }])\n setScreenAwakeMode: call_sdk(\"setScreenAwakeMode\", [{ enabled: true }])\n getOperationalEnvironment: call_sdk(\"getOperationalEnvironment\", [])\n getPlatformOS: call_sdk(\"getPlatformOS\", [])\n getDeviceId: call_sdk(\"getDeviceId\", [])\n getLocale: call_sdk(\"getLocale\", [])\n getNetworkStatus: call_sdk(\"getNetworkStatus\", [])\n getSchemeUri: call_sdk(\"getSchemeUri\", [])\n requestReview: call_sdk(\"requestReview\", [])\n closeView: call_sdk(\"closeView\", [])",
|
|
4320
4491
|
inputSchema: {
|
|
4321
4492
|
type: "object",
|
|
4322
4493
|
properties: {
|
|
@@ -4328,10 +4499,6 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4328
4499
|
type: "array",
|
|
4329
4500
|
description: "Arguments to pass to the SDK method (optional, default []).",
|
|
4330
4501
|
items: {}
|
|
4331
|
-
},
|
|
4332
|
-
confirm: {
|
|
4333
|
-
type: "boolean",
|
|
4334
|
-
description: "Required when MCP_ENV=relay-live. Set to `true` to explicitly acknowledge that this SDK call may have side effects on real/live users. Omitting this in a relay-live session results in a structured rejection error. Has no effect in mock or relay-dev sessions."
|
|
4335
4502
|
}
|
|
4336
4503
|
},
|
|
4337
4504
|
required: ["name"]
|
|
@@ -4370,7 +4537,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4370
4537
|
},
|
|
4371
4538
|
{
|
|
4372
4539
|
name: "start_debug",
|
|
4373
|
-
description: "Switches the active debug environment in-place (issue #348) — no Claude Code restart and no MCP re-handshake. One daemon holds both a local (env 1, mock SDK in a Chromium) and a relay (env 3
|
|
4540
|
+
description: "Switches the active debug environment in-place (issue #348) — no Claude Code restart and no MCP re-handshake. One daemon holds both a local (env 1, mock SDK in a Chromium) and a relay (env 2/3, real-device over the Chii relay + cloudflared tunnel) connection at once; this tool flips which one every other tool reads from, lazily booting the requested family's infra on first use and keeping the inactive one warm so an existing attach survives the switch. After switching it emits notifications/tools/list_changed — call tools/list again to see the updated tool surface for the new environment.\n\nPositive-allowlist kill-switch (#665): relay sessions on apps.tossmini.com (env 4, released production) are silently blocked at both the in-app gate and this MCP layer — relay-live and the LIVE guard have been removed. Only localhost/loopback (env 1), *.trycloudflare.com (env 2), and *.private-apps.tossmini.com (env 3) are allowed.\n\nmodes:\n local-browser — env 1: desktop Chromium with the mock SDK and a local CDP attach. Side-effect tools (call_sdk/evaluate) run unguarded against the mock; nothing touches a real device or real users. No prerequisites — the default, always-available environment for state/contract and visual-layout work.\n relay-sandbox — env 2: a real-device PWA (real WebKit engine, mock SDK) over an external Chii relay. CDP covers real-device WebKit DOM, console, exceptions, and safe-area observation; call_sdk still hits the mock (SDK fidelity needs relay-staging). Side-effect tools run unguarded against the mock. Only the dual-connection daemon can enter relay-sandbox in-place; a single-connection session rejects it with \"동적 전환할 수 없습니다 … relay-sandbox 모드로 재시작하세요\" — follow that hint and restart the MCP server in relay-sandbox mode rather than retrying. Prerequisites: both AIT_RELAY_BASE_URL (the relay base the unplugin emits when started with tunnel:{cdp:true}, used for the CDP attach) and AIT_TUNNEL_BASE_URL (the dev-server tunnel host, required by start_attach to render the launcher QR) must be set before the MCP server starts — the unplugin does not auto-forward either; set them explicitly. Both carry relay/tunnel hosts (secret-class) — keep them out of logs.\n relay-staging — env 3: a real-device Toss WebView dog-food build with the REAL SDK over the intoss-private relay. The first environment where call_sdk exercises the genuine native bridge. Side-effect tools run unguarded (dog-food, not released to real users). Prerequisite: a dog-food candidate bundle built with `RELEASE_CHANNEL=dogfood ait build`, then uploaded with `ait deploy` (add `--scheme-only` to print the resulting intoss-private://…?_deploymentId=… deep-link); open that deep-link/QR on the device to cold-load the bundle with the relay injected. Unlike env 2, env 3 is NOT a dev-server tunnel — it is a deployed bundle reached via the intoss-private scheme, so `pnpm dev` plays no part here.\n\nFor a relay mode (relay-sandbox/relay-staging), also pass projectRoot — the absolute mini-app project root — so the daemon can read the relay auth secret from <projectRoot>/.ait_relay (read-only; the daemon never mints it). Omit it for local-browser.",
|
|
4374
4541
|
inputSchema: {
|
|
4375
4542
|
type: "object",
|
|
4376
4543
|
properties: {
|
|
@@ -4379,18 +4546,13 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4379
4546
|
enum: [
|
|
4380
4547
|
"local-browser",
|
|
4381
4548
|
"relay-sandbox",
|
|
4382
|
-
"relay-staging"
|
|
4383
|
-
"relay-live"
|
|
4549
|
+
"relay-staging"
|
|
4384
4550
|
],
|
|
4385
|
-
description: "Target environment to switch to.
|
|
4386
|
-
},
|
|
4387
|
-
confirm: {
|
|
4388
|
-
type: "boolean",
|
|
4389
|
-
description: "Required when mode=relay-live — set true to acknowledge entering LIVE (env 4) debugging that can affect real users. Ignored for the other modes."
|
|
4551
|
+
description: "Target environment to switch to. relay-live (env 4) has been removed (#665) — use relay-staging (env 3) for dog-food debugging."
|
|
4390
4552
|
},
|
|
4391
4553
|
projectRoot: {
|
|
4392
4554
|
type: "string",
|
|
4393
|
-
description: "Absolute path to the mini-app project root (the directory containing its package.json and .ait_relay). The daemon reads the relay auth secret from <projectRoot>/.ait_relay (read-only) when switching to a relay environment (relay-staging/relay-
|
|
4555
|
+
description: "Absolute path to the mini-app project root (the directory containing its package.json and .ait_relay). The daemon reads the relay auth secret from <projectRoot>/.ait_relay (read-only) when switching to a relay environment (relay-staging/relay-sandbox). Pass this because the daemon's own cwd is fixed at launch and may not be the project being debugged. Omit for mode=local-browser (no secret needed)."
|
|
4394
4556
|
}
|
|
4395
4557
|
},
|
|
4396
4558
|
required: ["mode"]
|
|
@@ -4399,7 +4561,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4399
4561
|
},
|
|
4400
4562
|
{
|
|
4401
4563
|
name: "get_debug_status",
|
|
4402
|
-
description: "Reports the current debug session state — which environment/mode is active, whether a page is attached, and a full diagnostic snapshot — in one call. Use this any time to answer \"what mode am I in right now?\" or \"why is this not working?\" without chaining tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), authRejects ({count, lastAt} — relay TOTP 401 rejections, secret-free; count > 0 with empty pages means the phone reached the relay but its code was rejected), environment (kind: mock|relay-dev|relay-
|
|
4564
|
+
description: "Reports the current debug session state — which environment/mode is active, whether a page is attached, and a full diagnostic snapshot — in one call. Use this any time to answer \"what mode am I in right now?\" or \"why is this not working?\" without chaining tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), authRejects ({count, lastAt} — relay TOTP 401 rejections, secret-free; count > 0 with empty pages means the phone reached the relay but its code was rejected), environment (kind: mock|relay-dev|relay-mobile, env: mock|relay backward-compat, reason, liveGuardActive: always false — relay-live and LIVE guard removed (#665); start_debug mode→kind mapping: relay-sandbox→relay-mobile, relay-staging→relay-dev, local-browser→mock), serverLockHolder (pid + startedAt from the lock file, or null), nextRecommendedAction ({tool, reason} or null — the single next tool to call; in local-target mode tunnel.up=false is normal so \"restart\" is never recommended). All fields are nullable — missing data is null, not an error. debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. Tier C (both mock and relay).",
|
|
4403
4565
|
inputSchema: {
|
|
4404
4566
|
type: "object",
|
|
4405
4567
|
properties: { recent_errors_limit: {
|
|
@@ -4412,7 +4574,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4412
4574
|
},
|
|
4413
4575
|
{
|
|
4414
4576
|
name: "run_tests",
|
|
4415
|
-
description: "Runs mini-app test files on the attached page over CDP (Runtime.evaluate). Each matched file is bundled with esbuild (SDK imports redirected to the live mock/SDK), injected into the attached WebView, and executed; returns per-file results plus flattened totals (passed/failed/skipped/total). Requires an attached page — call list_pages first to confirm one is attached. Files run SEQUENTIALLY (single-attach model: the relay/local target serves one page), and one run_tests call runs at a time (a concurrent call is rejected). Test verification (assert/snapshot) is delegated to the in-page Vitest runtime; this tool is the transport + report. The per-file results array is the progress record — on partial failure you see exactly which files passed/failed/timed-out.
|
|
4577
|
+
description: "Runs mini-app test files on the attached page over CDP (Runtime.evaluate). Each matched file is bundled with esbuild (SDK imports redirected to the live mock/SDK), injected into the attached WebView, and executed; returns per-file results plus flattened totals (passed/failed/skipped/total). Requires an attached page — call list_pages first to confirm one is attached. Files run SEQUENTIALLY (single-attach model: the relay/local target serves one page), and one run_tests call runs at a time (a concurrent call is rejected). Test verification (assert/snapshot) is delegated to the in-page Vitest runtime; this tool is the transport + report. The per-file results array is the progress record — on partial failure you see exactly which files passed/failed/timed-out. Positive-allowlist kill-switch (#665): blocked when the attached page is on a non-debug host. debug-mode only — dev-mode (--mode=dev) has no CDP. Tier C (both mock/local and relay). The devtools-test CLI shares this run core and file discovery, but its standalone relay attach is not wired yet — run via this tool for now.",
|
|
4416
4578
|
inputSchema: {
|
|
4417
4579
|
type: "object",
|
|
4418
4580
|
properties: {
|
|
@@ -4428,10 +4590,6 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
4428
4590
|
timeout_ms: {
|
|
4429
4591
|
type: "number",
|
|
4430
4592
|
description: "Per-file evaluate timeout in ms (default 30000, range 1000–600000). Out-of-range/invalid values fall back to the default."
|
|
4431
|
-
},
|
|
4432
|
-
confirm: {
|
|
4433
|
-
type: "boolean",
|
|
4434
|
-
description: "Required (true) to run in a relay-live session — test injection mutates page state. Ignored in mock/local/relay-dev/relay-mobile sessions."
|
|
4435
4593
|
}
|
|
4436
4594
|
},
|
|
4437
4595
|
required: ["files"]
|
|
@@ -4457,8 +4615,9 @@ function getToolAvailability(name) {
|
|
|
4457
4615
|
* Unknown tools return `false` — callers should reject them as unknown rather
|
|
4458
4616
|
* than as env-mismatched.
|
|
4459
4617
|
*
|
|
4460
|
-
* Relay variants (`relay-dev`, `relay-
|
|
4618
|
+
* Relay variants (`relay-dev`, `relay-mobile`) all satisfy the
|
|
4461
4619
|
* `'relay'` availability tier — `isRelayEnv()` is used for the check.
|
|
4620
|
+
* (`relay-live` removed #665.)
|
|
4462
4621
|
*/
|
|
4463
4622
|
function isToolAvailableIn(name, env) {
|
|
4464
4623
|
const availability = getToolAvailability(name);
|
|
@@ -4472,8 +4631,8 @@ function isToolAvailableIn(name, env) {
|
|
|
4472
4631
|
* matches the given env. Pure — preserves order; both Tier C ("both") and the
|
|
4473
4632
|
* matching single-env tier pass through.
|
|
4474
4633
|
*
|
|
4475
|
-
* Relay variants (`relay-dev`, `relay-
|
|
4476
|
-
* `'relay'` tier.
|
|
4634
|
+
* Relay variants (`relay-dev`, `relay-mobile`) all satisfy the
|
|
4635
|
+
* `'relay'` tier. (`relay-live` removed #665.)
|
|
4477
4636
|
*/
|
|
4478
4637
|
function filterToolsByEnvironment(tools, env) {
|
|
4479
4638
|
return tools.filter((t) => t.availableIn === "both" || t.availableIn === "relay" && isRelayEnv(env) || t.availableIn === env);
|
|
@@ -4481,14 +4640,14 @@ function filterToolsByEnvironment(tools, env) {
|
|
|
4481
4640
|
/**
|
|
4482
4641
|
* Tool names that are available before any page attaches (bootstrap tier).
|
|
4483
4642
|
*
|
|
4484
|
-
* `
|
|
4485
|
-
* `list_pages`
|
|
4643
|
+
* `start_attach` — mode switch + QR synthesis + attach wait, no prior attach needed.
|
|
4644
|
+
* `list_pages` — reports tunnel status + empty pages even pre-attach.
|
|
4486
4645
|
*
|
|
4487
4646
|
* All other tools require an attached page (`enableDomains` must succeed) and
|
|
4488
4647
|
* are only advertised in `tools/list` once a target appears.
|
|
4489
4648
|
*/
|
|
4490
4649
|
const BOOTSTRAP_TOOL_NAMES = new Set([
|
|
4491
|
-
"
|
|
4650
|
+
"start_attach",
|
|
4492
4651
|
"get_debug_status",
|
|
4493
4652
|
"list_pages",
|
|
4494
4653
|
"start_debug"
|
|
@@ -4592,57 +4751,6 @@ function listPages(connection, tunnel) {
|
|
|
4592
4751
|
};
|
|
4593
4752
|
}
|
|
4594
4753
|
/**
|
|
4595
|
-
* Builds a self-attaching dog-food deep-link from an `ait deploy --scheme-only`
|
|
4596
|
-
* URL plus this session's live relay. Throws if the tunnel is not up yet (no
|
|
4597
|
-
* relay URL to splice in) — the caller surfaces that as a tool error.
|
|
4598
|
-
*
|
|
4599
|
-
* When `AIT_DEBUG_TOTP_SECRET` is set, generates the current TOTP code and
|
|
4600
|
-
* splices it as `at=<code>` into the attach URL. The code is valid for ~3
|
|
4601
|
-
* minutes (the relay gate uses {@link RELAY_VERIFY_SKEW_STEPS}=6, accepting
|
|
4602
|
-
* past 6 steps = 180–210 s backwards from issuance). If the scan happens after
|
|
4603
|
-
* `totp.expiresAt`, call `build_attach_url` again to get a fresh code (#490).
|
|
4604
|
-
*
|
|
4605
|
-
* Also validates the scheme URL's authority. A suspicious authority (empty,
|
|
4606
|
-
* "web", "localhost", etc.) is surfaced as a non-fatal `authorityWarning` on
|
|
4607
|
-
* the result so the caller can show a helpful hint without blocking the link
|
|
4608
|
-
* generation (the warning is consistent with how other validation in
|
|
4609
|
-
* `buildDeepLinkAttachUrl` works — hard errors for relay, soft warning for
|
|
4610
|
-
* the scheme authority which is in the caller's input, not ours to own).
|
|
4611
|
-
*
|
|
4612
|
-
* SECRET-HANDLING: `totpSecret` (if provided) is used only to compute a code
|
|
4613
|
-
* and must never appear in any log, error message, or output outside of the
|
|
4614
|
-
* spliced `at=` param in `attachUrl`.
|
|
4615
|
-
*
|
|
4616
|
-
* @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL.
|
|
4617
|
-
* @param tunnel - Current tunnel status from the running debug server.
|
|
4618
|
-
* @param totpSecret - Optional hex-encoded TOTP secret (from
|
|
4619
|
-
* `AIT_DEBUG_TOTP_SECRET`). When provided, the current code is spliced into
|
|
4620
|
-
* the attach URL as `at=<code>`.
|
|
4621
|
-
*/
|
|
4622
|
-
function buildAttachUrl(schemeUrl, tunnel, totpSecret) {
|
|
4623
|
-
if (!tunnel.up || tunnel.wssUrl === null) throw new Error("tunnel-down: cloudflared 터널이 안 떠 있습니다. MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.");
|
|
4624
|
-
const authorityWarning = validateSchemeAuthority(schemeUrl) ?? void 0;
|
|
4625
|
-
let totpCode;
|
|
4626
|
-
let totpMeta;
|
|
4627
|
-
if (totpSecret !== void 0 && totpSecret !== "") {
|
|
4628
|
-
const now = Date.now();
|
|
4629
|
-
totpCode = generateTotp(totpSecret, now);
|
|
4630
|
-
const STEP_SECONDS = 30;
|
|
4631
|
-
const expiresAtMs = now + 6 * STEP_SECONDS * 1e3;
|
|
4632
|
-
totpMeta = {
|
|
4633
|
-
enabled: true,
|
|
4634
|
-
ttlSeconds: 6 * STEP_SECONDS,
|
|
4635
|
-
expiresAt: new Date(expiresAtMs).toISOString()
|
|
4636
|
-
};
|
|
4637
|
-
}
|
|
4638
|
-
return {
|
|
4639
|
-
attachUrl: buildDeepLinkAttachUrl(schemeUrl, tunnel.wssUrl, totpCode),
|
|
4640
|
-
relayUrl: tunnel.wssUrl,
|
|
4641
|
-
...authorityWarning !== void 0 ? { authorityWarning } : {},
|
|
4642
|
-
...totpMeta !== void 0 ? { totp: totpMeta } : {}
|
|
4643
|
-
};
|
|
4644
|
-
}
|
|
4645
|
-
/**
|
|
4646
4754
|
* Heuristic: can this process open a GUI browser?
|
|
4647
4755
|
*
|
|
4648
4756
|
* Returns `true` when we think a GUI is available:
|
|
@@ -5259,7 +5367,7 @@ async function readMcpSdkVersion() {
|
|
|
5259
5367
|
* some test environments that skip the build step).
|
|
5260
5368
|
*/
|
|
5261
5369
|
function readDevtoolsVersion() {
|
|
5262
|
-
return "0.1.
|
|
5370
|
+
return "0.1.110";
|
|
5263
5371
|
}
|
|
5264
5372
|
/**
|
|
5265
5373
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -5268,10 +5376,10 @@ function readDevtoolsVersion() {
|
|
|
5268
5376
|
* 0. tunnel.droppedAt non-null → restart (permanent tunnel drop — highest priority)
|
|
5269
5377
|
* 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)
|
|
5270
5378
|
* 1b. tunnel.up === false AND env is mock → wait_for_page (local target: tunnel-less is normal)
|
|
5271
|
-
* 2a. authRejects.count > 0 AND pages empty →
|
|
5379
|
+
* 2a. authRejects.count > 0 AND pages empty → start_attach (relay TOTP 거부 관측 — QR 재스캔
|
|
5272
5380
|
* 또는 target-side `at` 전달 확인. 일반 rule 2보다 구체적이므로 먼저 평가 — issue #467)
|
|
5273
|
-
* 2. tunnel.up, pages empty, env === relay →
|
|
5274
|
-
* 3. pages has entry + crashDetectedAt non-null →
|
|
5381
|
+
* 2. tunnel.up, pages empty, env === relay → start_attach (start attach)
|
|
5382
|
+
* 3. pages has entry + crashDetectedAt non-null → start_attach (re-attach after crash)
|
|
5275
5383
|
* 4. otherwise → null (session looks healthy)
|
|
5276
5384
|
*
|
|
5277
5385
|
* Pure — does not throw; receives the final assembled snapshot fields.
|
|
@@ -5294,16 +5402,16 @@ function computeNextRecommendedAction(tunnel, pages, env, authRejects = null) {
|
|
|
5294
5402
|
reason: "tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart"
|
|
5295
5403
|
};
|
|
5296
5404
|
if (authRejects !== null && authRejects.count > 0 && pages !== null && pages.pages.length === 0) return {
|
|
5297
|
-
tool: "
|
|
5405
|
+
tool: "start_attach",
|
|
5298
5406
|
reason: `relay 인증(TOTP) 거부 ${authRejects.count}건 발생 (last ${authRejects.lastAt ?? "unknown"}) — QR을 다시 스캔해 새 코드로 attach하세요(코드는 ~3분마다 만료). 반복되면 폰 페이지 URL에 at 파라미터가 전달되는지(target-side TOTP 전달 경로)를 확인하세요`
|
|
5299
5407
|
};
|
|
5300
5408
|
if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
|
|
5301
|
-
tool: "
|
|
5302
|
-
reason: "tunnel ready, no pages attached — call
|
|
5409
|
+
tool: "start_attach",
|
|
5410
|
+
reason: "tunnel ready, no pages attached — call start_attach to generate the attach QR"
|
|
5303
5411
|
};
|
|
5304
5412
|
if (pages !== null && pages.crashDetectedAt !== null) return {
|
|
5305
|
-
tool: "
|
|
5306
|
-
reason: `page crashed at ${pages.crashDetectedAt} — call
|
|
5413
|
+
tool: "start_attach",
|
|
5414
|
+
reason: `page crashed at ${pages.crashDetectedAt} — call start_attach to re-attach`
|
|
5307
5415
|
};
|
|
5308
5416
|
return null;
|
|
5309
5417
|
}
|
|
@@ -5368,7 +5476,7 @@ async function getDiagnostics(input) {
|
|
|
5368
5476
|
kind: env,
|
|
5369
5477
|
env: toLegacyEnv(env),
|
|
5370
5478
|
reason: envReason,
|
|
5371
|
-
liveGuardActive:
|
|
5479
|
+
liveGuardActive: false
|
|
5372
5480
|
},
|
|
5373
5481
|
serverLockHolder,
|
|
5374
5482
|
process: {
|
|
@@ -5481,7 +5589,7 @@ async function startQuickTunnel(localPort) {
|
|
|
5481
5589
|
* every surface (terminal, VS Code, JetBrains, web) and can be scanned by a
|
|
5482
5590
|
* phone camera when shown verbatim in an agent response.
|
|
5483
5591
|
*
|
|
5484
|
-
* Shared by `renderAttachBanner` (relay wssUrl QR) and the `
|
|
5592
|
+
* Shared by `renderAttachBanner` (relay wssUrl QR) and the `start_attach`
|
|
5485
5593
|
* MCP tool response (attach deep-link QR).
|
|
5486
5594
|
*/
|
|
5487
5595
|
async function renderQr(text) {
|
|
@@ -5507,10 +5615,12 @@ async function renderQr(text) {
|
|
|
5507
5615
|
return `${lines.join("\n")}\n`;
|
|
5508
5616
|
}
|
|
5509
5617
|
/**
|
|
5510
|
-
* Renders the attach banner (relay URL +
|
|
5618
|
+
* Renders the attach banner (relay URL + unicode half-block QR) as a string.
|
|
5511
5619
|
*
|
|
5512
|
-
* The QR
|
|
5513
|
-
*
|
|
5620
|
+
* The QR is produced by `renderQr` (a half-block matrix, not the
|
|
5621
|
+
* `qrcode-terminal` ASCII art used by the unplugin banner) and encodes the
|
|
5622
|
+
* base `wssUrl` only. When `totpEnabled` is true, a note
|
|
5623
|
+
* is added that attach URLs generated by `start_attach` will include a
|
|
5514
5624
|
* live TOTP code (`at=`) appended at call time.
|
|
5515
5625
|
*
|
|
5516
5626
|
* SECRET-HANDLING: no secret value, TOTP code, or intermediate value is
|
|
@@ -5526,9 +5636,9 @@ async function renderAttachBanner(input) {
|
|
|
5526
5636
|
` relay (wss): ${input.wssUrl}`,
|
|
5527
5637
|
authNote,
|
|
5528
5638
|
"",
|
|
5529
|
-
" Use
|
|
5639
|
+
" Use start_attach to generate a deep link with the current TOTP code.",
|
|
5530
5640
|
" Scan the QR to locate the relay (open the dog-food URL separately with",
|
|
5531
|
-
" ?debug=1&relay=<wss>&at=<code> or use the
|
|
5641
|
+
" ?debug=1&relay=<wss>&at=<code> or use the start_attach tool):",
|
|
5532
5642
|
"",
|
|
5533
5643
|
qr
|
|
5534
5644
|
].join("\n");
|
|
@@ -5702,7 +5812,7 @@ function makeTunnelStatus(up, wssUrl, droppedAt = null, reissueAttempts = 0) {
|
|
|
5702
5812
|
* Dynamic tool registration (issue #208):
|
|
5703
5813
|
* The server advertises `listChanged: true` so MCP clients can subscribe to
|
|
5704
5814
|
* `notifications/tools/list_changed`. Before any page attaches, only bootstrap
|
|
5705
|
-
* tools (`
|
|
5815
|
+
* tools (`start_attach`, `list_pages`) are listed. Once a target appears,
|
|
5706
5816
|
* the full attach-dependent tool set is added and a `list_changed` notification
|
|
5707
5817
|
* is sent — without requiring a session restart. `runDebugServer` and
|
|
5708
5818
|
* `runLocalDebugServer` start a polling watcher that detects the 0→N target
|
|
@@ -5715,7 +5825,7 @@ function makeTunnelStatus(up, wssUrl, droppedAt = null, reissueAttempts = 0) {
|
|
|
5715
5825
|
*/
|
|
5716
5826
|
/**
|
|
5717
5827
|
* Maximum age (ms) of a page's `lastSeenAt` before it is treated as a ghost
|
|
5718
|
-
* and excluded from the `wait_for_attach` short-circuit in `
|
|
5828
|
+
* and excluded from the `wait_for_attach` short-circuit in `start_attach`
|
|
5719
5829
|
* (issue #610).
|
|
5720
5830
|
*
|
|
5721
5831
|
* Rationale: the env-2 relay is owned by the dev server (unplugin), so every
|
|
@@ -5730,7 +5840,15 @@ function makeTunnelStatus(up, wssUrl, droppedAt = null, reissueAttempts = 0) {
|
|
|
5730
5840
|
*/
|
|
5731
5841
|
const RELAY_SANDBOX_STALE_PAGE_MS = 300 * 1e3;
|
|
5732
5842
|
/**
|
|
5733
|
-
*
|
|
5843
|
+
* Segment length (ms) of the `start_attach` wait loop (issue #626 — TOTP in-call
|
|
5844
|
+
* re-mint). The single-shot `wait_for_attach` of the old attach tool could
|
|
5845
|
+
* not re-mint a TOTP code mid-wait; `start_attach` decomposes the wait into
|
|
5846
|
+
* SEGMENT_MS slices so it can detect an aging code between slices and re-mint a
|
|
5847
|
+
* fresh one without the agent re-calling the tool. 30 s = one TOTP step.
|
|
5848
|
+
*/
|
|
5849
|
+
const START_ATTACH_SEGMENT_MS = 3e4;
|
|
5850
|
+
/**
|
|
5851
|
+
* Predicate used by `start_attach`'s `wait_for_attach` loop to decide
|
|
5734
5852
|
* whether the relay-sandbox connection has a genuinely fresh page attached.
|
|
5735
5853
|
*
|
|
5736
5854
|
* Stale-ghost gating (issue #610): when the dev server restarts with a new
|
|
@@ -5780,13 +5898,28 @@ function extractDeploymentId(schemeUrl) {
|
|
|
5780
5898
|
}
|
|
5781
5899
|
}
|
|
5782
5900
|
/**
|
|
5783
|
-
* Returns `true` when the mode routes to a relay connection (`relay-sandbox
|
|
5784
|
-
* `relay-staging
|
|
5785
|
-
* `relay-staging`/`relay-live` are intoss-private relays — but all three surface
|
|
5786
|
-
* the Tier B / relay-only tool set.
|
|
5901
|
+
* Returns `true` when the mode routes to a relay connection (`relay-sandbox` or
|
|
5902
|
+
* `relay-staging`). Both surface the Tier B / relay-only tool set.
|
|
5787
5903
|
*/
|
|
5788
5904
|
function isRelayMode(mode) {
|
|
5789
|
-
return mode === "relay-sandbox" || mode === "relay-staging"
|
|
5905
|
+
return mode === "relay-sandbox" || mode === "relay-staging";
|
|
5906
|
+
}
|
|
5907
|
+
/**
|
|
5908
|
+
* Maps a `StartDebugMode` to the `McpEnvironment` it routes to (issue #626).
|
|
5909
|
+
* Used by `start_attach`'s mode prologue to decide whether a `switchMode` is
|
|
5910
|
+
* needed: when the active env already equals `envForMode(mode)`, the switch is
|
|
5911
|
+
* skipped (no `tools/list_changed` churn).
|
|
5912
|
+
*
|
|
5913
|
+
* - `local-browser` → `mock`
|
|
5914
|
+
* - `relay-sandbox` → `relay-mobile` (env 2 external-PWA relay)
|
|
5915
|
+
* - `relay-staging` → `relay-dev` (env 3 intoss-private relay)
|
|
5916
|
+
*/
|
|
5917
|
+
function envForMode(mode) {
|
|
5918
|
+
switch (mode) {
|
|
5919
|
+
case "local-browser": return "mock";
|
|
5920
|
+
case "relay-sandbox": return "relay-mobile";
|
|
5921
|
+
case "relay-staging": return "relay-dev";
|
|
5922
|
+
}
|
|
5790
5923
|
}
|
|
5791
5924
|
/**
|
|
5792
5925
|
* Single-attach guard for `run_tests` (#646). Two concurrent runs injecting
|
|
@@ -5809,6 +5942,12 @@ let runTestsInFlight = false;
|
|
|
5809
5942
|
* to resolve before the relay had observed the first inbound CDP message from
|
|
5810
5943
|
* the phone.
|
|
5811
5944
|
*
|
|
5945
|
+
* Timeout note: callers (e.g. the `start_attach` path) always pass an
|
|
5946
|
+
* explicit `timeoutMs`, sourced from the factory's `waitForAttachTimeoutMs`
|
|
5947
|
+
* (default 60 000). That value is forwarded to `waitForFirstTarget`, so it
|
|
5948
|
+
* overrides that method's own 90 000 signature default — the effective
|
|
5949
|
+
* wait on the tool path is 60 s, not 90 s.
|
|
5950
|
+
*
|
|
5812
5951
|
* @param connection - The CDP connection (production or fake).
|
|
5813
5952
|
* @param filterFn - Resolves when this predicate is satisfied.
|
|
5814
5953
|
* @param timeoutMs - Maximum wait time in ms.
|
|
@@ -5845,7 +5984,7 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
|
|
|
5845
5984
|
* tunnel, which is what makes the tool surface unit-testable.
|
|
5846
5985
|
*
|
|
5847
5986
|
* `tools/list` is two-tiered (issue #208):
|
|
5848
|
-
* - bootstrap (always): `
|
|
5987
|
+
* - bootstrap (always): `start_attach`, `list_pages`
|
|
5849
5988
|
* - attach-dependent (after `connection.listTargets().length > 0`): all others
|
|
5850
5989
|
*
|
|
5851
5990
|
* `CallTool` is NOT tiered — hidden tools still execute (attach errors surface
|
|
@@ -5856,12 +5995,299 @@ function createDebugServer(deps) {
|
|
|
5856
5995
|
const getTotpSecret = deps.getTotpSecret ?? (() => totpSecret);
|
|
5857
5996
|
const readLockFn = readLockDep ?? readServerLock;
|
|
5858
5997
|
const router = routerDep ?? makeSingleConnectionRouter(connection);
|
|
5859
|
-
const resolveEnvironment = getEnvDep ?? (() => deriveEnvironment(router.active.kind,
|
|
5860
|
-
const resolveEnvironmentReason = getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},
|
|
5998
|
+
const resolveEnvironment = getEnvDep ?? (() => deriveEnvironment(router.active.kind, router.activeRelayOrigin));
|
|
5999
|
+
const resolveEnvironmentReason = getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},relayOrigin=${router.activeRelayOrigin ?? "none"}`);
|
|
5861
6000
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
6001
|
+
/**
|
|
6002
|
+
* Synthesizes an attach URL from stored components with a FRESHLY-minted TOTP
|
|
6003
|
+
* code (issue #626 §3/§4 — the single mint point). Reads the late-bound secret
|
|
6004
|
+
* via `getTotpSecret()` so the project-local `.ait_relay` secret loaded by
|
|
6005
|
+
* `switchMode` is visible. SECRET-HANDLING: the minted code rides inside the
|
|
6006
|
+
* URL's `at=` param only — never logged or returned separately.
|
|
6007
|
+
*/
|
|
6008
|
+
function mintAttachUrl(parts) {
|
|
6009
|
+
const secret = getTotpSecret();
|
|
6010
|
+
const code = secret ? generateTotp(secret) : void 0;
|
|
6011
|
+
return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, {
|
|
6012
|
+
name: parts.appName,
|
|
6013
|
+
...parts.selfdebug ? { selfdebug: true } : {}
|
|
6014
|
+
}) : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);
|
|
6015
|
+
}
|
|
6016
|
+
/** Builds the fresh TOTP metadata (expiresAt window) for a tool result. */
|
|
6017
|
+
function buildTotpMeta() {
|
|
6018
|
+
const secret = getTotpSecret();
|
|
6019
|
+
if (secret === void 0 || secret === "") return void 0;
|
|
6020
|
+
const STEP_SECONDS = 30;
|
|
6021
|
+
const expiresAtMs = nowMs() + 6 * STEP_SECONDS * 1e3;
|
|
6022
|
+
return {
|
|
6023
|
+
enabled: true,
|
|
6024
|
+
ttlSeconds: 6 * STEP_SECONDS,
|
|
6025
|
+
expiresAt: new Date(expiresAtMs).toISOString()
|
|
6026
|
+
};
|
|
6027
|
+
}
|
|
6028
|
+
/**
|
|
6029
|
+
* Env-specific validation + component bundle for `start_attach` (issue #626).
|
|
6030
|
+
* Branches on `env`: `relay-mobile` reads AIT_TUNNEL_BASE_URL + builds launcher
|
|
6031
|
+
* parts; `relay-dev` requires scheme_url + builds scheme parts. Returns
|
|
6032
|
+
* `{ ok: false, error }` with a ready McpResult on any failure.
|
|
6033
|
+
*/
|
|
6034
|
+
async function prepareAttach(env, args, conn) {
|
|
6035
|
+
const selfdebug = args?.selfdebug === true;
|
|
6036
|
+
if (selfdebug && env !== "relay-mobile") return {
|
|
6037
|
+
ok: false,
|
|
6038
|
+
error: mcpError("start_attach: selfdebug=true는 env 2 / relay-sandbox 전용 기능입니다. 현재 환경(env 3)에서는 launcher가 없어 self-target 모드를 지원하지 않습니다. launcher self-target이 필요하다면 relay-sandbox 모드로 전환하세요.")
|
|
6039
|
+
};
|
|
6040
|
+
if (env === "relay-mobile") {
|
|
6041
|
+
const rawProjectRoot = args?.projectRoot;
|
|
6042
|
+
const buildProjectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
6043
|
+
let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
|
|
6044
|
+
if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
|
|
6045
|
+
const { readRelayUrls } = await import("../relay-url-store-CwKT7i04.js");
|
|
6046
|
+
tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
|
|
6047
|
+
}
|
|
6048
|
+
if (tunnelHttpUrl === "") return {
|
|
6049
|
+
ok: false,
|
|
6050
|
+
error: mcpError("start_attach(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. 자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.")
|
|
6051
|
+
};
|
|
6052
|
+
const tunnelStatus = getTunnelStatus();
|
|
6053
|
+
if (!tunnelStatus.up || tunnelStatus.wssUrl === null) return {
|
|
6054
|
+
ok: false,
|
|
6055
|
+
error: mcpError("start_attach(mobile): relay wssUrl이 아직 설정되지 않았습니다. unplugin tunnel:{cdp:true}가 relay를 완전히 기동할 때까지 잠시 후 다시 시도하세요.")
|
|
6056
|
+
};
|
|
6057
|
+
const secret = getTotpSecret();
|
|
6058
|
+
if (secret === void 0 || secret === "") return {
|
|
6059
|
+
ok: false,
|
|
6060
|
+
error: mcpError("start_attach(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.")
|
|
6061
|
+
};
|
|
6062
|
+
let launcherAppName;
|
|
6063
|
+
if (buildProjectRoot !== void 0) try {
|
|
6064
|
+
const { readFileSync } = await import("node:fs");
|
|
6065
|
+
const pkgRaw = readFileSync(`${buildProjectRoot}/package.json`, "utf8");
|
|
6066
|
+
const pkg = JSON.parse(pkgRaw);
|
|
6067
|
+
const rawName = typeof pkg.name === "string" ? pkg.name : "";
|
|
6068
|
+
launcherAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
|
|
6069
|
+
} catch {}
|
|
6070
|
+
const parts = {
|
|
6071
|
+
kind: "launcher",
|
|
6072
|
+
tunnelHttpUrl,
|
|
6073
|
+
wssUrl: tunnelStatus.wssUrl,
|
|
6074
|
+
appName: launcherAppName,
|
|
6075
|
+
...selfdebug ? { selfdebug: true } : {}
|
|
6076
|
+
};
|
|
6077
|
+
const connAsAny = conn;
|
|
6078
|
+
const getLastSeenAt = typeof connAsAny.getTargetLastSeenAt === "function" ? (id) => connAsAny.getTargetLastSeenAt(id) : null;
|
|
6079
|
+
const callNow = nowMs();
|
|
6080
|
+
const isMatchingPage = (pages) => isSandboxPageFresh(pages, getLastSeenAt, callNow, stalePageThresholdMs);
|
|
6081
|
+
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
6082
|
+
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
6083
|
+
return `${baseText}\n\nNo page attached within ${timeoutSec}s${observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : ""} — launcher QR을 폰 카메라로 스캔한 뒤 call list_pages를 다시 호출하세요.`;
|
|
6084
|
+
};
|
|
6085
|
+
return {
|
|
6086
|
+
ok: true,
|
|
6087
|
+
parts,
|
|
6088
|
+
isMatchingPage,
|
|
6089
|
+
buildTimeoutError,
|
|
6090
|
+
authorityWarning: void 0,
|
|
6091
|
+
totpMeta: buildTotpMeta()
|
|
6092
|
+
};
|
|
6093
|
+
}
|
|
6094
|
+
const schemeUrl = args?.scheme_url;
|
|
6095
|
+
if (typeof schemeUrl !== "string" || schemeUrl === "") return {
|
|
6096
|
+
ok: false,
|
|
6097
|
+
error: mcpError("start_attach: scheme_url이 비어 있습니다. `ait deploy --scheme-only`가 출력하는 intoss-private:// URL을 인자로 전달하세요. 환경 2(mobile)라면 scheme_url 대신 AIT_TUNNEL_BASE_URL을 설정하세요.")
|
|
6098
|
+
};
|
|
6099
|
+
{
|
|
6100
|
+
const relaySecret = getTotpSecret();
|
|
6101
|
+
if (relaySecret === void 0 || relaySecret === "") return {
|
|
6102
|
+
ok: false,
|
|
6103
|
+
error: mcpError("start_attach(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.")
|
|
6104
|
+
};
|
|
6105
|
+
}
|
|
6106
|
+
const tunnelForBuild = getTunnelStatus();
|
|
6107
|
+
if (!tunnelForBuild.up || tunnelForBuild.wssUrl === null) return {
|
|
6108
|
+
ok: false,
|
|
6109
|
+
error: classifyToolError(/* @__PURE__ */ new Error("tunnel-down:"), "start_attach")
|
|
6110
|
+
};
|
|
6111
|
+
const authorityWarning = validateSchemeAuthority(schemeUrl) ?? void 0;
|
|
6112
|
+
const parts = {
|
|
6113
|
+
kind: "scheme",
|
|
6114
|
+
schemeUrl,
|
|
6115
|
+
wssUrl: tunnelForBuild.wssUrl
|
|
6116
|
+
};
|
|
6117
|
+
const deploymentId = extractDeploymentId(schemeUrl);
|
|
6118
|
+
if (!deploymentId) logInfo("tool.call", {
|
|
6119
|
+
tool: "start_attach",
|
|
6120
|
+
msg: "no _deploymentId in scheme_url; matching on presence only"
|
|
6121
|
+
});
|
|
6122
|
+
const isMatchingPage = (pages) => {
|
|
6123
|
+
if (pages.length === 0) return false;
|
|
6124
|
+
if (deploymentId === null) return true;
|
|
6125
|
+
return pages.some((p) => p.url.includes(deploymentId));
|
|
6126
|
+
};
|
|
6127
|
+
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
6128
|
+
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
6129
|
+
const observedNote = observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : "";
|
|
6130
|
+
return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
|
|
6131
|
+
};
|
|
6132
|
+
return {
|
|
6133
|
+
ok: true,
|
|
6134
|
+
parts,
|
|
6135
|
+
isMatchingPage,
|
|
6136
|
+
buildTimeoutError,
|
|
6137
|
+
authorityWarning,
|
|
6138
|
+
totpMeta: buildTotpMeta()
|
|
6139
|
+
};
|
|
6140
|
+
}
|
|
6141
|
+
/**
|
|
6142
|
+
* QR render + browser open + segmented attach wait with in-call TOTP re-mint
|
|
6143
|
+
* (issue #626 §3). Shared by env-2 and env-3 (4 render paths:
|
|
6144
|
+
* headless / browser-opened / browser-open-failed / no-http-server).
|
|
6145
|
+
*
|
|
6146
|
+
* The wait is decomposed into `START_ATTACH_SEGMENT_MS` slices. Between slices,
|
|
6147
|
+
* if the current TOTP code has aged past `START_ATTACH_REMINT_THRESHOLD_MS`,
|
|
6148
|
+
* a fresh URL is minted via `mintAttachUrl` and pushed to the dashboard via
|
|
6149
|
+
* `onAttachUrlBuilt` (SSE refresh — NO browser re-open). The `reminted` count
|
|
6150
|
+
* rides in the success/timeout result.
|
|
6151
|
+
*
|
|
6152
|
+
* SECRET-HANDLING: attachUrl encodes tunnel/scheme host + the TOTP `at=` code
|
|
6153
|
+
* in the QR payload only. The browser is opened on a 127.0.0.1 URL only. The
|
|
6154
|
+
* tool result carries `totp.expiresAt` + `reminted` count — never the code.
|
|
6155
|
+
*/
|
|
6156
|
+
async function renderAndMaybeWait(prep, waitForAttach, callTimeoutMs, conn) {
|
|
6157
|
+
const { parts, isMatchingPage, buildTimeoutError, authorityWarning, totpMeta } = prep;
|
|
6158
|
+
let attachUrl = mintAttachUrl(parts);
|
|
6159
|
+
onAttachUrlBuilt?.(parts);
|
|
6160
|
+
let totpIssuedAt = nowMs();
|
|
6161
|
+
let reminted = 0;
|
|
6162
|
+
const relayUrl = parts.wssUrl;
|
|
6163
|
+
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).";
|
|
6164
|
+
const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
|
|
6165
|
+
const guiAvailable = canOpenBrowser();
|
|
6166
|
+
/** Builds the totp object surfaced in results (fresh expiresAt + reminted). */
|
|
6167
|
+
const totpResult = () => {
|
|
6168
|
+
if (!totpMeta) return void 0;
|
|
6169
|
+
const expiresAtMs = totpIssuedAt + 180 * 1e3;
|
|
6170
|
+
return {
|
|
6171
|
+
enabled: true,
|
|
6172
|
+
ttlSeconds: totpMeta.ttlSeconds,
|
|
6173
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
6174
|
+
...reminted > 0 ? { reminted } : {}
|
|
6175
|
+
};
|
|
6176
|
+
};
|
|
6177
|
+
/**
|
|
6178
|
+
* Segmented wait with TOTP re-mint (issue #626 §3). Resolves with the
|
|
6179
|
+
* attached page list, or rejects on timeout. Between SEGMENT_MS slices it
|
|
6180
|
+
* re-mints when the code has aged past the threshold (max ~4 re-mints over
|
|
6181
|
+
* 600 s). Returns immediately once a matching page attaches (no re-mint).
|
|
6182
|
+
*/
|
|
6183
|
+
async function waitWithRemint() {
|
|
6184
|
+
const deadline = nowMs() + callTimeoutMs;
|
|
6185
|
+
if (isMatchingPage(conn.listTargets())) return conn.listTargets();
|
|
6186
|
+
for (;;) {
|
|
6187
|
+
const remaining = deadline - nowMs();
|
|
6188
|
+
if (remaining <= 0) throw new Error(`start_attach: 타임아웃 (${callTimeoutMs}ms)`);
|
|
6189
|
+
const segmentMs = Math.min(START_ATTACH_SEGMENT_MS, remaining);
|
|
6190
|
+
try {
|
|
6191
|
+
return await waitForAttachWithEvents(conn, isMatchingPage, segmentMs);
|
|
6192
|
+
} catch {
|
|
6193
|
+
if (totpMeta && nowMs() - totpIssuedAt >= 15e4) {
|
|
6194
|
+
attachUrl = mintAttachUrl(parts);
|
|
6195
|
+
onAttachUrlBuilt?.(parts);
|
|
6196
|
+
totpIssuedAt = nowMs();
|
|
6197
|
+
reminted += 1;
|
|
6198
|
+
}
|
|
6199
|
+
}
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6202
|
+
/**
|
|
6203
|
+
* Assembles the success result after a page attaches. `baseText` carries the
|
|
6204
|
+
* QR + pre-wait JSON block (the QR the user already scanned). The attach
|
|
6205
|
+
* itself ends the wait, so the QR is moot — what matters now is the final
|
|
6206
|
+
* TOTP state. If the segmented wait re-minted (issue #626 §3), surface the
|
|
6207
|
+
* post-wait `totp` block (fresh `expiresAt` + `reminted` count) so the result
|
|
6208
|
+
* reflects how many times the code rotated during the wait. SECRET-HANDLING:
|
|
6209
|
+
* the totp block carries expiresAt + reminted only — never the code value.
|
|
6210
|
+
*/
|
|
6211
|
+
const successResult = (baseText) => {
|
|
6212
|
+
const pagesResult = listPages(conn, getTunnelStatus());
|
|
6213
|
+
const finalTotp = totpResult();
|
|
6214
|
+
const remintNote = finalTotp && reminted > 0 ? `\n\n${JSON.stringify({ totp: finalTotp }, null, 2)}` : "";
|
|
6215
|
+
return { content: [{
|
|
6216
|
+
type: "text",
|
|
6217
|
+
text: `${baseText}\n\n${JSON.stringify(pagesResult, null, 2)}${remintNote}`
|
|
6218
|
+
}] };
|
|
6219
|
+
};
|
|
6220
|
+
/** Runs the wait (when requested) and returns success/timeout result. */
|
|
6221
|
+
const runWait = async (baseText) => {
|
|
6222
|
+
if (!waitForAttach) return { content: [{
|
|
6223
|
+
type: "text",
|
|
6224
|
+
text: baseText
|
|
6225
|
+
}] };
|
|
6226
|
+
try {
|
|
6227
|
+
await waitWithRemint();
|
|
6228
|
+
} catch {
|
|
6229
|
+
const observed = conn.listTargets();
|
|
6230
|
+
return {
|
|
6231
|
+
content: [{
|
|
6232
|
+
type: "text",
|
|
6233
|
+
text: buildTimeoutError(baseText, callTimeoutMs / 1e3, observed)
|
|
6234
|
+
}],
|
|
6235
|
+
isError: true
|
|
6236
|
+
};
|
|
6237
|
+
}
|
|
6238
|
+
return successResult(baseText);
|
|
6239
|
+
};
|
|
6240
|
+
if (!guiAvailable) {
|
|
6241
|
+
const headlessNote = "GUI 환경이 감지되지 않았습니다 (headless/remote 환경). 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
|
|
6242
|
+
const qr = await renderQr(attachUrl);
|
|
6243
|
+
return runWait(`${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
|
|
6244
|
+
attachUrl,
|
|
6245
|
+
relayUrl,
|
|
6246
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6247
|
+
}, null, 2)}\n\n${qr}`);
|
|
6248
|
+
}
|
|
6249
|
+
if (guiAvailable && qrHttpServer) {
|
|
6250
|
+
const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
|
|
6251
|
+
if (browserResult.opened) {
|
|
6252
|
+
const retriedNote = browserResult.retried ? " (1회 retry 후 성공)" : "";
|
|
6253
|
+
const openResult = {
|
|
6254
|
+
attempted: true,
|
|
6255
|
+
succeeded: true,
|
|
6256
|
+
...browserResult.retried ? { retried: true } : {}
|
|
6257
|
+
};
|
|
6258
|
+
return runWait(`${warningPrefix}${header}\n${JSON.stringify({
|
|
6259
|
+
relayUrl,
|
|
6260
|
+
openResult,
|
|
6261
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6262
|
+
}, null, 2)}\n\n브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`);
|
|
6263
|
+
}
|
|
6264
|
+
const openResult = {
|
|
6265
|
+
attempted: true,
|
|
6266
|
+
succeeded: false,
|
|
6267
|
+
failureReason: browserResult.error ?? "브라우저 실행 후보 모두 실패",
|
|
6268
|
+
pngUrl: browserResult.pngUrl,
|
|
6269
|
+
...browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}
|
|
6270
|
+
};
|
|
6271
|
+
const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
|
|
6272
|
+
const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
|
|
6273
|
+
const qr = await renderQr(attachUrl);
|
|
6274
|
+
return runWait(`${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
|
|
6275
|
+
attachUrl,
|
|
6276
|
+
relayUrl,
|
|
6277
|
+
openResult,
|
|
6278
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6279
|
+
}, null, 2)}\n\n${qr}`);
|
|
6280
|
+
}
|
|
6281
|
+
const qr = await renderQr(attachUrl);
|
|
6282
|
+
return runWait(`${warningPrefix}${header}\n${JSON.stringify({
|
|
6283
|
+
attachUrl,
|
|
6284
|
+
relayUrl,
|
|
6285
|
+
...totpResult() ? { totp: totpResult() } : {}
|
|
6286
|
+
}, null, 2)}\n\n${qr}`);
|
|
6287
|
+
}
|
|
5862
6288
|
const server = new Server({
|
|
5863
6289
|
name: "ait-debug",
|
|
5864
|
-
version: "0.1.
|
|
6290
|
+
version: "0.1.110"
|
|
5865
6291
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
5866
6292
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
5867
6293
|
const conn = router.active;
|
|
@@ -5883,12 +6309,47 @@ function createDebugServer(deps) {
|
|
|
5883
6309
|
if (name === "start_debug") {
|
|
5884
6310
|
const rawMode = request.params.arguments?.mode;
|
|
5885
6311
|
const mode = normalizeStartDebugMode(rawMode);
|
|
5886
|
-
if (mode === null) return mcpError("start_debug: mode가 올바르지 않습니다. 'local-browser' | 'relay-sandbox' | 'relay-staging'
|
|
5887
|
-
const confirm = request.params.arguments?.confirm === true;
|
|
6312
|
+
if (mode === null) return mcpError("start_debug: mode가 올바르지 않습니다. 'local-browser' | 'relay-sandbox' | 'relay-staging' 중 하나를 전달하세요. (relay-live / env 4는 #665에서 제거됐습니다.)");
|
|
5888
6313
|
const rawProjectRoot = request.params.arguments?.projectRoot;
|
|
5889
6314
|
const projectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
5890
6315
|
try {
|
|
5891
|
-
return jsonResult$1(await router.switchMode(mode,
|
|
6316
|
+
return jsonResult$1(await router.switchMode(mode, projectRoot));
|
|
6317
|
+
} catch (err) {
|
|
6318
|
+
return errorResult(err, name);
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
if (name === "start_attach") {
|
|
6322
|
+
const args = request.params.arguments;
|
|
6323
|
+
let attachConn = conn;
|
|
6324
|
+
const rawMode = args?.mode;
|
|
6325
|
+
if (rawMode !== void 0) {
|
|
6326
|
+
const mode = normalizeStartDebugMode(rawMode);
|
|
6327
|
+
if (mode === null || mode === "local-browser") return mcpError("start_attach: mode가 올바르지 않습니다. 'relay-sandbox' | 'relay-staging' 중 하나를 전달하세요 (local-browser는 QR attach가 없어 start_attach에서 지원하지 않습니다).");
|
|
6328
|
+
const targetEnv = envForMode(mode);
|
|
6329
|
+
if (resolveEnvironment() !== targetEnv) {
|
|
6330
|
+
const rawProjectRoot = args?.projectRoot;
|
|
6331
|
+
const projectRoot = typeof rawProjectRoot === "string" ? rawProjectRoot : void 0;
|
|
6332
|
+
try {
|
|
6333
|
+
await router.switchMode(mode, projectRoot);
|
|
6334
|
+
} catch (err) {
|
|
6335
|
+
return errorResult(err, name);
|
|
6336
|
+
}
|
|
6337
|
+
attachConn = router.active;
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
const attachEnv = resolveEnvironment();
|
|
6341
|
+
if (!isRelayEnv(attachEnv)) return mcpError("start_attach: relay 전용 tool입니다 (env 2 / relay-sandbox 또는 env 3 / relay-staging). 현재 환경은 'local-browser'(mock)입니다 — mode 인자로 'relay-sandbox' 또는 'relay-staging'을 전달하거나, 먼저 relay 모드로 전환하세요.");
|
|
6342
|
+
const waitForAttach = true;
|
|
6343
|
+
const rawWaitTimeout = args?.wait_timeout_seconds;
|
|
6344
|
+
const callTimeoutMs = (() => {
|
|
6345
|
+
if (typeof rawWaitTimeout !== "number" || !Number.isFinite(rawWaitTimeout)) return waitForAttachTimeoutMs;
|
|
6346
|
+
if (rawWaitTimeout <= 0) return waitForAttachTimeoutMs;
|
|
6347
|
+
return Math.round(Math.max(1, Math.min(600, rawWaitTimeout))) * 1e3;
|
|
6348
|
+
})();
|
|
6349
|
+
try {
|
|
6350
|
+
const prep = await prepareAttach(attachEnv, args, attachConn);
|
|
6351
|
+
if (!prep.ok) return prep.error;
|
|
6352
|
+
return await renderAndMaybeWait(prep, waitForAttach, callTimeoutMs, attachConn);
|
|
5892
6353
|
} catch (err) {
|
|
5893
6354
|
return errorResult(err, name);
|
|
5894
6355
|
}
|
|
@@ -5933,386 +6394,6 @@ function createDebugServer(deps) {
|
|
|
5933
6394
|
} catch (err) {
|
|
5934
6395
|
return errorResult(err, name);
|
|
5935
6396
|
}
|
|
5936
|
-
if (name === "build_attach_url") {
|
|
5937
|
-
const waitForAttach = request.params.arguments?.wait_for_attach === true;
|
|
5938
|
-
const selfdebug = request.params.arguments?.selfdebug === true;
|
|
5939
|
-
const rawWaitTimeout = request.params.arguments?.wait_timeout_seconds;
|
|
5940
|
-
const callTimeoutMs = (() => {
|
|
5941
|
-
if (typeof rawWaitTimeout !== "number" || !Number.isFinite(rawWaitTimeout)) return waitForAttachTimeoutMs;
|
|
5942
|
-
const clamped = Math.max(1, Math.min(600, rawWaitTimeout));
|
|
5943
|
-
if (rawWaitTimeout <= 0) return waitForAttachTimeoutMs;
|
|
5944
|
-
return Math.round(clamped) * 1e3;
|
|
5945
|
-
})();
|
|
5946
|
-
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 모드로 재시작하세요.");
|
|
5947
|
-
if (env === "relay-mobile") {
|
|
5948
|
-
const rawBuildProjectRoot = request.params.arguments?.projectRoot;
|
|
5949
|
-
const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
|
|
5950
|
-
let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
|
|
5951
|
-
if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
|
|
5952
|
-
const { readRelayUrls } = await import("../relay-url-store-CwKT7i04.js");
|
|
5953
|
-
tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
|
|
5954
|
-
}
|
|
5955
|
-
if (tunnelHttpUrl === "") return mcpError("build_attach_url(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. 자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.");
|
|
5956
|
-
const tunnelStatus = getTunnelStatus();
|
|
5957
|
-
if (!tunnelStatus.up || tunnelStatus.wssUrl === null) return mcpError("build_attach_url(mobile): relay wssUrl이 아직 설정되지 않았습니다. unplugin tunnel:{cdp:true}가 relay를 완전히 기동할 때까지 잠시 후 다시 시도하세요.");
|
|
5958
|
-
const secret = getTotpSecret();
|
|
5959
|
-
if (secret === void 0 || secret === "") return mcpError("build_attach_url(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.");
|
|
5960
|
-
let totpCode;
|
|
5961
|
-
let totpMeta;
|
|
5962
|
-
{
|
|
5963
|
-
const now = Date.now();
|
|
5964
|
-
totpCode = generateTotp(secret, now);
|
|
5965
|
-
const STEP_SECONDS = 30;
|
|
5966
|
-
const expiresAtMs = now + 6 * STEP_SECONDS * 1e3;
|
|
5967
|
-
totpMeta = {
|
|
5968
|
-
enabled: true,
|
|
5969
|
-
ttlSeconds: 6 * STEP_SECONDS,
|
|
5970
|
-
expiresAt: new Date(expiresAtMs).toISOString()
|
|
5971
|
-
};
|
|
5972
|
-
}
|
|
5973
|
-
let launcherAppName;
|
|
5974
|
-
if (buildProjectRoot !== void 0) try {
|
|
5975
|
-
const { readFileSync } = await import("node:fs");
|
|
5976
|
-
const pkgRaw = readFileSync(`${buildProjectRoot}/package.json`, "utf8");
|
|
5977
|
-
const pkg = JSON.parse(pkgRaw);
|
|
5978
|
-
const rawName = typeof pkg.name === "string" ? pkg.name : "";
|
|
5979
|
-
launcherAppName = (rawName.includes("/") ? rawName.slice(rawName.indexOf("/") + 1) : rawName).trim() || void 0;
|
|
5980
|
-
} catch {}
|
|
5981
|
-
const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode, {
|
|
5982
|
-
name: launcherAppName,
|
|
5983
|
-
...selfdebug ? { selfdebug: true } : {}
|
|
5984
|
-
});
|
|
5985
|
-
onAttachUrlBuilt?.({
|
|
5986
|
-
kind: "launcher",
|
|
5987
|
-
tunnelHttpUrl,
|
|
5988
|
-
wssUrl: tunnelStatus.wssUrl,
|
|
5989
|
-
appName: launcherAppName
|
|
5990
|
-
});
|
|
5991
|
-
const relayUrl = tunnelStatus.wssUrl;
|
|
5992
|
-
const totp = totpMeta;
|
|
5993
|
-
const connAsAny = conn;
|
|
5994
|
-
const getLastSeenAt = typeof connAsAny.getTargetLastSeenAt === "function" ? (id) => connAsAny.getTargetLastSeenAt(id) : null;
|
|
5995
|
-
const callNow = nowMs();
|
|
5996
|
-
const isMatchingPage = (pages) => isSandboxPageFresh(pages, getLastSeenAt, callNow, stalePageThresholdMs);
|
|
5997
|
-
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
5998
|
-
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
5999
|
-
return `${baseText}\n\nNo page attached within ${timeoutSec}s${observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : ""} — launcher QR을 폰 카메라로 스캔한 뒤 call list_pages를 다시 호출하세요.`;
|
|
6000
|
-
};
|
|
6001
|
-
return await (async () => {
|
|
6002
|
-
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).";
|
|
6003
|
-
const warningPrefix = "";
|
|
6004
|
-
const guiAvailable = canOpenBrowser();
|
|
6005
|
-
if (!guiAvailable) {
|
|
6006
|
-
const headlessNote = "GUI 환경이 감지되지 않았습니다 (headless/remote 환경). 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
|
|
6007
|
-
const qrHeadless = await renderQr(attachUrl);
|
|
6008
|
-
const headlessText = `${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
|
|
6009
|
-
attachUrl,
|
|
6010
|
-
relayUrl,
|
|
6011
|
-
...totp ? { totp } : {}
|
|
6012
|
-
}, null, 2)}\n\n${qrHeadless}`;
|
|
6013
|
-
if (!waitForAttach) return { content: [{
|
|
6014
|
-
type: "text",
|
|
6015
|
-
text: headlessText
|
|
6016
|
-
}] };
|
|
6017
|
-
let attachedPagesHl = [];
|
|
6018
|
-
try {
|
|
6019
|
-
attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6020
|
-
} catch {
|
|
6021
|
-
attachedPagesHl = conn.listTargets();
|
|
6022
|
-
return {
|
|
6023
|
-
content: [{
|
|
6024
|
-
type: "text",
|
|
6025
|
-
text: buildTimeoutError(headlessText, callTimeoutMs / 1e3, attachedPagesHl)
|
|
6026
|
-
}],
|
|
6027
|
-
isError: true
|
|
6028
|
-
};
|
|
6029
|
-
}
|
|
6030
|
-
const pagesResultHl = listPages(conn, getTunnelStatus());
|
|
6031
|
-
return { content: [{
|
|
6032
|
-
type: "text",
|
|
6033
|
-
text: `${headlessText}\n\n${JSON.stringify(pagesResultHl, null, 2)}`
|
|
6034
|
-
}] };
|
|
6035
|
-
}
|
|
6036
|
-
if (guiAvailable && qrHttpServer) {
|
|
6037
|
-
const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
|
|
6038
|
-
if (browserResult.opened) {
|
|
6039
|
-
const retriedNote = browserResult.retried ? " (1회 retry 후 성공)" : "";
|
|
6040
|
-
const openResult = {
|
|
6041
|
-
attempted: true,
|
|
6042
|
-
succeeded: true,
|
|
6043
|
-
...browserResult.retried ? { retried: true } : {}
|
|
6044
|
-
};
|
|
6045
|
-
const shortText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
6046
|
-
relayUrl,
|
|
6047
|
-
openResult,
|
|
6048
|
-
...totp ? { totp } : {}
|
|
6049
|
-
}, null, 2)}\n\n브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`;
|
|
6050
|
-
if (!waitForAttach) return { content: [{
|
|
6051
|
-
type: "text",
|
|
6052
|
-
text: shortText
|
|
6053
|
-
}] };
|
|
6054
|
-
let attachedPages = [];
|
|
6055
|
-
try {
|
|
6056
|
-
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6057
|
-
} catch {
|
|
6058
|
-
attachedPages = conn.listTargets();
|
|
6059
|
-
return {
|
|
6060
|
-
content: [{
|
|
6061
|
-
type: "text",
|
|
6062
|
-
text: buildTimeoutError(shortText, callTimeoutMs / 1e3, attachedPages)
|
|
6063
|
-
}],
|
|
6064
|
-
isError: true
|
|
6065
|
-
};
|
|
6066
|
-
}
|
|
6067
|
-
const pagesResult = listPages(conn, getTunnelStatus());
|
|
6068
|
-
return { content: [{
|
|
6069
|
-
type: "text",
|
|
6070
|
-
text: `${shortText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
6071
|
-
}] };
|
|
6072
|
-
}
|
|
6073
|
-
const openResult = {
|
|
6074
|
-
attempted: true,
|
|
6075
|
-
succeeded: false,
|
|
6076
|
-
failureReason: browserResult.error ?? "브라우저 실행 후보 모두 실패",
|
|
6077
|
-
pngUrl: browserResult.pngUrl,
|
|
6078
|
-
...browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}
|
|
6079
|
-
};
|
|
6080
|
-
const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
|
|
6081
|
-
const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:\n${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
|
|
6082
|
-
const qr = await renderQr(attachUrl);
|
|
6083
|
-
const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
|
|
6084
|
-
attachUrl,
|
|
6085
|
-
relayUrl,
|
|
6086
|
-
openResult,
|
|
6087
|
-
...totp ? { totp } : {}
|
|
6088
|
-
}, null, 2)}\n\n${qr}`;
|
|
6089
|
-
if (!waitForAttach) return { content: [{
|
|
6090
|
-
type: "text",
|
|
6091
|
-
text: baseText
|
|
6092
|
-
}] };
|
|
6093
|
-
let attachedPagesFb = [];
|
|
6094
|
-
try {
|
|
6095
|
-
attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6096
|
-
} catch {
|
|
6097
|
-
attachedPagesFb = conn.listTargets();
|
|
6098
|
-
return {
|
|
6099
|
-
content: [{
|
|
6100
|
-
type: "text",
|
|
6101
|
-
text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPagesFb)
|
|
6102
|
-
}],
|
|
6103
|
-
isError: true
|
|
6104
|
-
};
|
|
6105
|
-
}
|
|
6106
|
-
const pagesResultFb = listPages(conn, getTunnelStatus());
|
|
6107
|
-
return { content: [{
|
|
6108
|
-
type: "text",
|
|
6109
|
-
text: `${baseText}\n\n${JSON.stringify(pagesResultFb, null, 2)}`
|
|
6110
|
-
}] };
|
|
6111
|
-
}
|
|
6112
|
-
const qr = await renderQr(attachUrl);
|
|
6113
|
-
const baseText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
6114
|
-
attachUrl,
|
|
6115
|
-
relayUrl,
|
|
6116
|
-
...totp ? { totp } : {}
|
|
6117
|
-
}, null, 2)}\n\n${qr}`;
|
|
6118
|
-
if (!waitForAttach) return { content: [{
|
|
6119
|
-
type: "text",
|
|
6120
|
-
text: baseText
|
|
6121
|
-
}] };
|
|
6122
|
-
let attachedPages = [];
|
|
6123
|
-
try {
|
|
6124
|
-
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6125
|
-
} catch {
|
|
6126
|
-
attachedPages = conn.listTargets();
|
|
6127
|
-
return {
|
|
6128
|
-
content: [{
|
|
6129
|
-
type: "text",
|
|
6130
|
-
text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPages)
|
|
6131
|
-
}],
|
|
6132
|
-
isError: true
|
|
6133
|
-
};
|
|
6134
|
-
}
|
|
6135
|
-
const pagesResult = listPages(conn, getTunnelStatus());
|
|
6136
|
-
return { content: [{
|
|
6137
|
-
type: "text",
|
|
6138
|
-
text: `${baseText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
6139
|
-
}] };
|
|
6140
|
-
})();
|
|
6141
|
-
}
|
|
6142
|
-
const schemeUrl = request.params.arguments?.scheme_url;
|
|
6143
|
-
if (typeof schemeUrl !== "string" || schemeUrl === "") return mcpError("build_attach_url: scheme_url이 비어 있습니다. `ait deploy --scheme-only`가 출력하는 intoss-private:// URL을 인자로 전달하세요. 환경 2(mobile)라면 scheme_url 대신 AIT_TUNNEL_BASE_URL을 설정하세요.");
|
|
6144
|
-
const deploymentId = extractDeploymentId(schemeUrl);
|
|
6145
|
-
if (!deploymentId) logInfo("tool.call", {
|
|
6146
|
-
tool: "build_attach_url",
|
|
6147
|
-
msg: "no _deploymentId in scheme_url; matching on presence only"
|
|
6148
|
-
});
|
|
6149
|
-
/** Returns true when the page list satisfies the attach condition. */
|
|
6150
|
-
const isMatchingPage = (pages) => {
|
|
6151
|
-
if (pages.length === 0) return false;
|
|
6152
|
-
if (deploymentId === null) return true;
|
|
6153
|
-
return pages.some((p) => p.url.includes(deploymentId));
|
|
6154
|
-
};
|
|
6155
|
-
/** Builds a timeout error message with diagnostic context. */
|
|
6156
|
-
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
6157
|
-
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
6158
|
-
const observedNote = observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : "";
|
|
6159
|
-
return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
|
|
6160
|
-
};
|
|
6161
|
-
{
|
|
6162
|
-
const relaySecret = getTotpSecret();
|
|
6163
|
-
if (relaySecret === void 0 || relaySecret === "") return mcpError("build_attach_url(relay): TOTP secret(AIT_DEBUG_TOTP_SECRET)이 설정되지 않았습니다. relay 환경은 TOTP 인증이 필수입니다 — relay를 secret과 함께 재기동하세요.");
|
|
6164
|
-
}
|
|
6165
|
-
try {
|
|
6166
|
-
const tunnelForBuild = getTunnelStatus();
|
|
6167
|
-
const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(schemeUrl, tunnelForBuild, getTotpSecret());
|
|
6168
|
-
if (tunnelForBuild.wssUrl !== null) onAttachUrlBuilt?.({
|
|
6169
|
-
kind: "scheme",
|
|
6170
|
-
schemeUrl,
|
|
6171
|
-
wssUrl: tunnelForBuild.wssUrl
|
|
6172
|
-
});
|
|
6173
|
-
const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
|
|
6174
|
-
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).";
|
|
6175
|
-
const guiAvailable = canOpenBrowser();
|
|
6176
|
-
if (!guiAvailable) {
|
|
6177
|
-
const headlessNote = "GUI 환경이 감지되지 않았습니다 (headless/remote 환경). 텍스트 QR을 폰 카메라로 스캔하거나, 로컬 GUI 환경에서 실행하세요.\n\n";
|
|
6178
|
-
const qrHeadless = await renderQr(attachUrl);
|
|
6179
|
-
const headlessText = `${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
|
|
6180
|
-
attachUrl,
|
|
6181
|
-
relayUrl,
|
|
6182
|
-
...totp ? { totp } : {}
|
|
6183
|
-
}, null, 2)}\n\n${qrHeadless}`;
|
|
6184
|
-
if (!waitForAttach) return { content: [{
|
|
6185
|
-
type: "text",
|
|
6186
|
-
text: headlessText
|
|
6187
|
-
}] };
|
|
6188
|
-
let attachedPagesHl = [];
|
|
6189
|
-
try {
|
|
6190
|
-
attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6191
|
-
} catch {
|
|
6192
|
-
attachedPagesHl = conn.listTargets();
|
|
6193
|
-
return {
|
|
6194
|
-
content: [{
|
|
6195
|
-
type: "text",
|
|
6196
|
-
text: buildTimeoutError(headlessText, callTimeoutMs / 1e3, attachedPagesHl)
|
|
6197
|
-
}],
|
|
6198
|
-
isError: true
|
|
6199
|
-
};
|
|
6200
|
-
}
|
|
6201
|
-
const pagesResultHl = listPages(conn, getTunnelStatus());
|
|
6202
|
-
return { content: [{
|
|
6203
|
-
type: "text",
|
|
6204
|
-
text: `${headlessText}\n\n${JSON.stringify(pagesResultHl, null, 2)}`
|
|
6205
|
-
}] };
|
|
6206
|
-
}
|
|
6207
|
-
if (guiAvailable && qrHttpServer) {
|
|
6208
|
-
const browserResult = await openQrInBrowser(qrHttpServer.buildAttachPageUrl(attachUrl), `http://127.0.0.1:${qrHttpServer.port}/qr.png?u=${encodeURIComponent(attachUrl)}`);
|
|
6209
|
-
if (browserResult.opened) {
|
|
6210
|
-
const retriedNote = browserResult.retried ? " (1회 retry 후 성공)" : "";
|
|
6211
|
-
const openResult = {
|
|
6212
|
-
attempted: true,
|
|
6213
|
-
succeeded: true,
|
|
6214
|
-
...browserResult.retried ? { retried: true } : {}
|
|
6215
|
-
};
|
|
6216
|
-
const shortText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
6217
|
-
relayUrl,
|
|
6218
|
-
openResult,
|
|
6219
|
-
...totp ? { totp } : {}
|
|
6220
|
-
}, null, 2)}\n\n브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`;
|
|
6221
|
-
if (!waitForAttach) return { content: [{
|
|
6222
|
-
type: "text",
|
|
6223
|
-
text: shortText
|
|
6224
|
-
}] };
|
|
6225
|
-
let attachedPages = [];
|
|
6226
|
-
try {
|
|
6227
|
-
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6228
|
-
} catch {
|
|
6229
|
-
attachedPages = conn.listTargets();
|
|
6230
|
-
return {
|
|
6231
|
-
content: [{
|
|
6232
|
-
type: "text",
|
|
6233
|
-
text: buildTimeoutError(shortText, callTimeoutMs / 1e3, attachedPages)
|
|
6234
|
-
}],
|
|
6235
|
-
isError: true
|
|
6236
|
-
};
|
|
6237
|
-
}
|
|
6238
|
-
const pagesResult = listPages(conn, getTunnelStatus());
|
|
6239
|
-
return { content: [{
|
|
6240
|
-
type: "text",
|
|
6241
|
-
text: `${shortText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
6242
|
-
}] };
|
|
6243
|
-
}
|
|
6244
|
-
const openResult = {
|
|
6245
|
-
attempted: true,
|
|
6246
|
-
succeeded: false,
|
|
6247
|
-
failureReason: browserResult.error ?? "브라우저 실행 후보 모두 실패",
|
|
6248
|
-
pngUrl: browserResult.pngUrl,
|
|
6249
|
-
...browserResult.stderrSummary ? { stderrSummary: browserResult.stderrSummary } : {}
|
|
6250
|
-
};
|
|
6251
|
-
const stderrNote = browserResult.stderrSummary ? `\nstderr: ${browserResult.stderrSummary}` : "";
|
|
6252
|
-
const fallbackNote = `브라우저 자동 열기에 실패했습니다. 다음 URL을 직접 브라우저에서 여세요:
|
|
6253
|
-
${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stderrNote + "\n\n";
|
|
6254
|
-
const qr = await renderQr(attachUrl);
|
|
6255
|
-
const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
|
|
6256
|
-
attachUrl,
|
|
6257
|
-
relayUrl,
|
|
6258
|
-
openResult,
|
|
6259
|
-
...totp ? { totp } : {}
|
|
6260
|
-
}, null, 2)}\n\n${qr}`;
|
|
6261
|
-
if (!waitForAttach) return { content: [{
|
|
6262
|
-
type: "text",
|
|
6263
|
-
text: baseText
|
|
6264
|
-
}] };
|
|
6265
|
-
let attachedPagesFb = [];
|
|
6266
|
-
try {
|
|
6267
|
-
attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6268
|
-
} catch {
|
|
6269
|
-
attachedPagesFb = conn.listTargets();
|
|
6270
|
-
return {
|
|
6271
|
-
content: [{
|
|
6272
|
-
type: "text",
|
|
6273
|
-
text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPagesFb)
|
|
6274
|
-
}],
|
|
6275
|
-
isError: true
|
|
6276
|
-
};
|
|
6277
|
-
}
|
|
6278
|
-
const pagesResultFb = listPages(conn, getTunnelStatus());
|
|
6279
|
-
return { content: [{
|
|
6280
|
-
type: "text",
|
|
6281
|
-
text: `${baseText}\n\n${JSON.stringify(pagesResultFb, null, 2)}`
|
|
6282
|
-
}] };
|
|
6283
|
-
}
|
|
6284
|
-
const qr = await renderQr(attachUrl);
|
|
6285
|
-
const baseText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
6286
|
-
attachUrl,
|
|
6287
|
-
relayUrl,
|
|
6288
|
-
...totp ? { totp } : {}
|
|
6289
|
-
}, null, 2)}\n\n${qr}`;
|
|
6290
|
-
if (!waitForAttach) return { content: [{
|
|
6291
|
-
type: "text",
|
|
6292
|
-
text: baseText
|
|
6293
|
-
}] };
|
|
6294
|
-
let attachedPages = [];
|
|
6295
|
-
try {
|
|
6296
|
-
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, callTimeoutMs);
|
|
6297
|
-
} catch {
|
|
6298
|
-
attachedPages = conn.listTargets();
|
|
6299
|
-
return {
|
|
6300
|
-
content: [{
|
|
6301
|
-
type: "text",
|
|
6302
|
-
text: buildTimeoutError(baseText, callTimeoutMs / 1e3, attachedPages)
|
|
6303
|
-
}],
|
|
6304
|
-
isError: true
|
|
6305
|
-
};
|
|
6306
|
-
}
|
|
6307
|
-
const pagesResult = listPages(conn, getTunnelStatus());
|
|
6308
|
-
return { content: [{
|
|
6309
|
-
type: "text",
|
|
6310
|
-
text: `${baseText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
6311
|
-
}] };
|
|
6312
|
-
} catch (err) {
|
|
6313
|
-
return errorResult(err, name);
|
|
6314
|
-
}
|
|
6315
|
-
}
|
|
6316
6397
|
try {
|
|
6317
6398
|
await conn.enableDomains();
|
|
6318
6399
|
} catch (err) {
|
|
@@ -6351,7 +6432,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6351
6432
|
case "evaluate": {
|
|
6352
6433
|
const expression = request.params.arguments?.expression;
|
|
6353
6434
|
if (typeof expression !== "string" || expression === "") return mcpError("evaluate: expression 인자가 비어 있습니다. 평가할 JavaScript 표현식을 전달하세요.");
|
|
6354
|
-
if (conn
|
|
6435
|
+
if (!connectionHostsAllowed(conn)) return mcpError("evaluate: 현재 연결된 페이지는 debug 허용 호스트가 아닙니다 (#665). 허용 호스트: localhost, *.trycloudflare.com, *.private-apps.tossmini.com.");
|
|
6355
6436
|
return jsonResult$1(await evaluate(conn, expression));
|
|
6356
6437
|
}
|
|
6357
6438
|
case "call_sdk": {
|
|
@@ -6359,7 +6440,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6359
6440
|
if (typeof sdkName !== "string" || sdkName === "") return mcpError("call_sdk: name 인자가 비어 있습니다. 호출할 SDK 메서드 이름을 전달하세요.");
|
|
6360
6441
|
const rawArgs = request.params.arguments?.args;
|
|
6361
6442
|
const sdkArgs = Array.isArray(rawArgs) ? rawArgs : [];
|
|
6362
|
-
if (conn
|
|
6443
|
+
if (!connectionHostsAllowed(conn)) return mcpError("call_sdk: 현재 연결된 페이지는 debug 허용 호스트가 아닙니다 (#665). 허용 호스트: localhost, *.trycloudflare.com, *.private-apps.tossmini.com.");
|
|
6363
6444
|
const sdkResult = await callSdk(conn, sdkName, sdkArgs);
|
|
6364
6445
|
if (!sdkResult.ok && typeof sdkResult.error === "string" && sdkResult.error.startsWith("sdk-absent:")) return sdkAbsentError("call_sdk", conn.kind === "local");
|
|
6365
6446
|
return envelopeResult$1(sdkResult, name, env, conn.listTargets().length > 0);
|
|
@@ -6373,7 +6454,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6373
6454
|
const projectRoot = typeof rawRoot === "string" ? rawRoot : process.cwd();
|
|
6374
6455
|
const rawTimeout = request.params.arguments?.timeout_ms;
|
|
6375
6456
|
const timeoutMs = typeof rawTimeout === "number" && rawTimeout >= 1e3 && rawTimeout <= 6e5 ? rawTimeout : void 0;
|
|
6376
|
-
if (conn
|
|
6457
|
+
if (!connectionHostsAllowed(conn)) return mcpError("run_tests: 현재 연결된 페이지는 debug 허용 호스트가 아닙니다 (#665). 허용 호스트: localhost, *.trycloudflare.com, *.private-apps.tossmini.com.");
|
|
6377
6458
|
if (runTestsInFlight) return mcpError("run_tests: 이미 다른 테스트 실행이 진행 중입니다 (single-attach 모델: 페이지는 한 번에 하나의 실행만 처리). 완료 후 다시 시도하세요.");
|
|
6378
6459
|
runTestsInFlight = true;
|
|
6379
6460
|
try {
|
|
@@ -6403,24 +6484,52 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
6403
6484
|
}
|
|
6404
6485
|
/**
|
|
6405
6486
|
* Normalizes a raw `start_debug` `mode` argument to a `StartDebugMode`, or
|
|
6406
|
-
* `null` when the value is not one of the
|
|
6407
|
-
* 'local-browser' | 'relay-sandbox' | 'relay-staging'
|
|
6487
|
+
* `null` when the value is not one of the three accepted modes:
|
|
6488
|
+
* 'local-browser' | 'relay-sandbox' | 'relay-staging'
|
|
6408
6489
|
*
|
|
6409
6490
|
* Hard rename (issue #398): the older `local`/`mobile`/`staging`/`live` names
|
|
6410
6491
|
* and their aliases are no longer accepted — pre-1.0, no back-compat.
|
|
6492
|
+
* `relay-live` (env 4) removed in #665.
|
|
6411
6493
|
*/
|
|
6412
6494
|
function normalizeStartDebugMode(raw) {
|
|
6413
|
-
if (raw === "local-browser" || raw === "relay-sandbox" || raw === "relay-staging"
|
|
6495
|
+
if (raw === "local-browser" || raw === "relay-sandbox" || raw === "relay-staging") return raw;
|
|
6414
6496
|
return null;
|
|
6415
6497
|
}
|
|
6416
6498
|
/**
|
|
6499
|
+
* Positive-allowlist kill-switch for side-effect MCP tools (#665).
|
|
6500
|
+
*
|
|
6501
|
+
* Returns `true` when the connection's attached targets are all on allowed
|
|
6502
|
+
* debug hosts (localhost / trycloudflare / private-apps). Returns `false` when
|
|
6503
|
+
* any target's page URL is on a non-allowed host (e.g. `apps.tossmini.com`).
|
|
6504
|
+
*
|
|
6505
|
+
* For local connections this always returns `true` — the local Chromium is
|
|
6506
|
+
* always on localhost. For relay connections without any pages it returns
|
|
6507
|
+
* `true` (no pages = nothing to block; the caller's page-missing guard fires
|
|
6508
|
+
* first).
|
|
6509
|
+
*
|
|
6510
|
+
* SECRET-HANDLING: hostnames are NEVER logged here — only the boolean result
|
|
6511
|
+
* is returned to the caller.
|
|
6512
|
+
*/
|
|
6513
|
+
function connectionHostsAllowed(conn) {
|
|
6514
|
+
if (conn.kind === "local") return true;
|
|
6515
|
+
const pages = conn.listTargets();
|
|
6516
|
+
if (pages.length === 0) return true;
|
|
6517
|
+
return pages.every((p) => {
|
|
6518
|
+
try {
|
|
6519
|
+
return isDebugAllowedHost(new URL(p.url ?? "").hostname);
|
|
6520
|
+
} catch {
|
|
6521
|
+
return false;
|
|
6522
|
+
}
|
|
6523
|
+
});
|
|
6524
|
+
}
|
|
6525
|
+
/**
|
|
6417
6526
|
* Builds a trivial `ConnectionRouter` pinned to a single connection (issue
|
|
6418
6527
|
* #348). Used by `createDebugServer` when no real dual router is injected —
|
|
6419
6528
|
* every existing single-connection test and the `local`-only / `relay`-only
|
|
6420
6529
|
* boot path. `switchMode` here cannot lazily boot another family, so it only
|
|
6421
|
-
* honors a request that matches the connection's own kind
|
|
6422
|
-
*
|
|
6423
|
-
*
|
|
6530
|
+
* honors a request that matches the connection's own kind; any cross-family
|
|
6531
|
+
* request is rejected with a clear "dynamic switch unavailable in this session"
|
|
6532
|
+
* error. `confirm` parameter and `relay-live` gate removed (#665).
|
|
6424
6533
|
*/
|
|
6425
6534
|
function makeSingleConnectionRouter(connection) {
|
|
6426
6535
|
return {
|
|
@@ -6428,18 +6537,15 @@ function makeSingleConnectionRouter(connection) {
|
|
|
6428
6537
|
return connection;
|
|
6429
6538
|
},
|
|
6430
6539
|
activeRelayOrigin: void 0,
|
|
6431
|
-
switchMode(mode,
|
|
6540
|
+
switchMode(mode, _projectRoot) {
|
|
6432
6541
|
if (mode === "relay-sandbox") return Promise.reject(/* @__PURE__ */ new Error("start_debug: 이 세션은 단일 연결만 보유합니다 — 'relay-sandbox'(환경 2 PWA, 외부 relay)로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). MCP 서버를 relay-sandbox 모드로 재시작하세요."));
|
|
6433
6542
|
if (isRelayMode(mode) !== (connection.kind === "relay")) return Promise.reject(/* @__PURE__ */ new Error(`start_debug: 이 세션은 단일 ${connection.kind} 연결만 보유합니다 — '${mode}'로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). MCP 서버를 원하는 모드로 재시작하세요.`));
|
|
6434
|
-
|
|
6435
|
-
setLiveIntent(mode === "relay-live");
|
|
6436
|
-
const environment = deriveEnvironment(connection.kind, getLiveIntent());
|
|
6543
|
+
const environment = deriveEnvironment(connection.kind);
|
|
6437
6544
|
return Promise.resolve({
|
|
6438
6545
|
mode,
|
|
6439
6546
|
environment,
|
|
6440
6547
|
kind: connection.kind,
|
|
6441
|
-
|
|
6442
|
-
nextStep: connection.kind === "relay" ? "build_attach_url로 attach QR을 생성하세요." : "list_pages로 로컬 페이지 attach를 확인하세요."
|
|
6548
|
+
nextStep: connection.kind === "relay" ? "start_attach로 attach QR 생성 + 폰 attach까지 한 번에 진행하세요." : "list_pages로 로컬 페이지 attach를 확인하세요."
|
|
6443
6549
|
});
|
|
6444
6550
|
}
|
|
6445
6551
|
};
|
|
@@ -6454,7 +6560,10 @@ function makeSingleConnectionRouter(connection) {
|
|
|
6454
6560
|
function rebuildAttachUrl(parts) {
|
|
6455
6561
|
const secret = process.env.AIT_DEBUG_TOTP_SECRET;
|
|
6456
6562
|
const code = secret ? generateTotp(secret) : void 0;
|
|
6457
|
-
return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, {
|
|
6563
|
+
return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code, {
|
|
6564
|
+
name: parts.appName,
|
|
6565
|
+
...parts.selfdebug ? { selfdebug: true } : {}
|
|
6566
|
+
}) : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);
|
|
6458
6567
|
}
|
|
6459
6568
|
function jsonResult$1(value) {
|
|
6460
6569
|
return { content: [{
|
|
@@ -6495,6 +6604,7 @@ function toRunTestsResult(report) {
|
|
|
6495
6604
|
error: f.result.error
|
|
6496
6605
|
} : {
|
|
6497
6606
|
file: f.file,
|
|
6607
|
+
duration: f.result.duration,
|
|
6498
6608
|
passed: f.result.passed,
|
|
6499
6609
|
failed: f.result.failed,
|
|
6500
6610
|
skipped: f.result.skipped,
|
|
@@ -6646,8 +6756,9 @@ async function bootLocalFamily() {
|
|
|
6646
6756
|
*
|
|
6647
6757
|
* Booted lazily via the dual router's `bootLazyFor('relay-intoss')` callback
|
|
6648
6758
|
* (symmetry with {@link bootLocalFamily}), at most once on the first
|
|
6649
|
-
* `start_debug({ mode: 'relay-staging'
|
|
6650
|
-
*
|
|
6759
|
+
* `start_debug({ mode: 'relay-staging' })` (all-lazy, #396 — every relay boot now
|
|
6760
|
+
* flows through `switchMode` after the project-local secret load). `relay-live`
|
|
6761
|
+
* removed (#665).
|
|
6651
6762
|
*
|
|
6652
6763
|
* The relay base URL is only known after `startChiiRelay()` resolves, so the
|
|
6653
6764
|
* `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside
|
|
@@ -6734,7 +6845,7 @@ async function bootRelayFamily(options = {}) {
|
|
|
6734
6845
|
* we did not start.
|
|
6735
6846
|
*
|
|
6736
6847
|
* `getTunnelStatus()` reports `up: true` with a `wssUrl` derived from
|
|
6737
|
-
* `relayBaseUrl` (http→ws, https→wss) so the `
|
|
6848
|
+
* `relayBaseUrl` (http→ws, https→wss) so the `start_attach` gate
|
|
6738
6849
|
* (`up: true && wssUrl !== null`) is satisfied even though we never opened a
|
|
6739
6850
|
* cloudflared tunnel ourselves.
|
|
6740
6851
|
*
|
|
@@ -6779,14 +6890,14 @@ async function bootExternalRelayFamily(relayBaseUrl, relayLocalUrl) {
|
|
|
6779
6890
|
/**
|
|
6780
6891
|
* Maps a `StartDebugMode` to the {@link FamilyKey} that serves it (issue #378).
|
|
6781
6892
|
* local-browser → 'local-browser'; relay-sandbox → 'relay-sandbox';
|
|
6782
|
-
* relay-staging
|
|
6893
|
+
* relay-staging → 'relay-intoss' (the intoss-private relay slot).
|
|
6894
|
+
* `relay-live` removed (#665).
|
|
6783
6895
|
*/
|
|
6784
6896
|
function familyKeyForMode(mode) {
|
|
6785
6897
|
switch (mode) {
|
|
6786
6898
|
case "local-browser": return "local-browser";
|
|
6787
6899
|
case "relay-sandbox": return "relay-sandbox";
|
|
6788
|
-
case "relay-staging":
|
|
6789
|
-
case "relay-live": return "relay-intoss";
|
|
6900
|
+
case "relay-staging": return "relay-intoss";
|
|
6790
6901
|
}
|
|
6791
6902
|
}
|
|
6792
6903
|
/** The error thrown / surfaced when entering `mobile` without AIT_RELAY_BASE_URL. */
|
|
@@ -6845,12 +6956,11 @@ const NULL_CDP_CONNECTION = {
|
|
|
6845
6956
|
* restarting the MCP server.
|
|
6846
6957
|
*
|
|
6847
6958
|
* Why a KEYED map and not a single lazy slot (#378): `relay-sandbox` (env-2
|
|
6848
|
-
* external relay) and `relay-staging
|
|
6849
|
-
*
|
|
6850
|
-
*
|
|
6851
|
-
*
|
|
6852
|
-
*
|
|
6853
|
-
* `relay-intoss` slot (wire-identical, distinguished only by `liveIntent`).
|
|
6959
|
+
* external relay) and `relay-staging` (intoss relay) are BOTH `kind: 'relay'`.
|
|
6960
|
+
* A single "opposite-kind" slot could not warm-keep both at once — they would
|
|
6961
|
+
* collide. The three `FamilyKey`s (`local-browser` / `relay-intoss` /
|
|
6962
|
+
* `relay-sandbox`) give each its own warm slot. `relay-live` (env 4) removed
|
|
6963
|
+
* (#665) — `relay-intoss` slot now maps only to `relay-staging`.
|
|
6854
6964
|
*
|
|
6855
6965
|
* Why all-lazy (#396): the relay TOTP secret now lives in a project-local
|
|
6856
6966
|
* `.ait_relay` file loaded read-only by `switchMode` BEFORE a relay family boots.
|
|
@@ -6860,15 +6970,14 @@ const NULL_CDP_CONNECTION = {
|
|
|
6860
6970
|
* `buildRelayVerifyAuth()` run at the boot site.
|
|
6861
6971
|
*
|
|
6862
6972
|
* `switchMode`:
|
|
6863
|
-
* 1. rejects re-entrant swaps (`swapInFlight`)
|
|
6973
|
+
* 1. rejects re-entrant swaps (`swapInFlight`);
|
|
6864
6974
|
* 2. resolves the requested mode's `FamilyKey`:
|
|
6865
6975
|
* `lazyFamilies.get(key) ?? (boot via bootLazyFor(key), store)`;
|
|
6866
6976
|
* 3. flips `active` (the MCP `Server` never re-handshakes — it reads through
|
|
6867
6977
|
* `active` per request);
|
|
6868
|
-
* 4.
|
|
6869
|
-
* 5. stops the old attach watcher and re-arms one on the new connection
|
|
6978
|
+
* 4. stops the old attach watcher and re-arms one on the new connection
|
|
6870
6979
|
* (the watcher self-clears, so re-arm is mandatory);
|
|
6871
|
-
*
|
|
6980
|
+
* 5. emits `tools/list_changed`.
|
|
6872
6981
|
*
|
|
6873
6982
|
* Inactive infra is left WARM — teardown happens only at process exit (the
|
|
6874
6983
|
* unified shutdown in the run functions), which is what keeps a phone attach
|
|
@@ -6914,10 +7023,10 @@ var DualConnectionRouter = class {
|
|
|
6914
7023
|
/**
|
|
6915
7024
|
* Live tunnel status of the active relay family (issues #356, #378). Reads
|
|
6916
7025
|
* the ACTIVE family's tunnel when it has one (so `relay-sandbox` surfaces the
|
|
6917
|
-
* external relay wss and `relay-staging
|
|
7026
|
+
* external relay wss and `relay-staging` the intoss relay wss); otherwise
|
|
6918
7027
|
* falls back to the first booted family that has a tunnel. Returns "down"
|
|
6919
7028
|
* until any relay family is booted (any session before the first relay
|
|
6920
|
-
* start_debug) — the correct signal for `
|
|
7029
|
+
* start_debug) — the correct signal for `start_attach` (no tunnel yet).
|
|
6921
7030
|
*/
|
|
6922
7031
|
relayTunnelStatus() {
|
|
6923
7032
|
if (this.activeFamily?.getTunnelStatus) return this.activeFamily.getTunnelStatus();
|
|
@@ -6952,7 +7061,7 @@ var DualConnectionRouter = class {
|
|
|
6952
7061
|
this.deps.onPageAttach?.();
|
|
6953
7062
|
if (activeFamily.connection.kind === "relay") {
|
|
6954
7063
|
const firstTarget = activeFamily.connection.listTargets()[0];
|
|
6955
|
-
const env = deriveEnvironment(activeFamily.connection.kind,
|
|
7064
|
+
const env = deriveEnvironment(activeFamily.connection.kind, activeFamily.relayOrigin);
|
|
6956
7065
|
const inspectorStableUrl = this.deps.getInspectorStableUrl?.() ?? null;
|
|
6957
7066
|
this.deps.devtoolsOpener.open({
|
|
6958
7067
|
inspectorStableUrl,
|
|
@@ -7010,25 +7119,22 @@ var DualConnectionRouter = class {
|
|
|
7010
7119
|
this.lazyFamilies.set(key, booted);
|
|
7011
7120
|
return booted;
|
|
7012
7121
|
}
|
|
7013
|
-
async switchMode(mode,
|
|
7122
|
+
async switchMode(mode, projectRoot) {
|
|
7014
7123
|
if (this.swapInFlight) throw new Error("start_debug: 이전 전환이 아직 진행 중입니다 — 잠시 후 다시 호출하세요.");
|
|
7015
|
-
if (mode === "relay-live" && !confirm) throw new Error("start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — 실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요.");
|
|
7016
7124
|
this.swapInFlight = true;
|
|
7017
7125
|
try {
|
|
7018
7126
|
if (isRelayMode(mode)) await loadRelaySecretReadOnly({ projectRoot });
|
|
7019
7127
|
const target = await this.familyFor(familyKeyForMode(mode), projectRoot);
|
|
7020
7128
|
this.activeFamily = target;
|
|
7021
|
-
setLiveIntent(mode === "relay-live");
|
|
7022
7129
|
this.stopWatcher();
|
|
7023
7130
|
this.armWatcher();
|
|
7024
7131
|
this.server?.sendToolListChanged();
|
|
7025
7132
|
const wantRelay = isRelayMode(mode);
|
|
7026
7133
|
return {
|
|
7027
7134
|
mode,
|
|
7028
|
-
environment: deriveEnvironment(target.connection.kind,
|
|
7135
|
+
environment: deriveEnvironment(target.connection.kind, target.relayOrigin),
|
|
7029
7136
|
kind: target.connection.kind,
|
|
7030
|
-
|
|
7031
|
-
nextStep: wantRelay ? "build_attach_url로 attach QR을 생성하세요 (relay 세션)." : "list_pages로 로컬 Chromium 페이지 attach를 확인하세요."
|
|
7137
|
+
nextStep: wantRelay ? "start_attach로 attach QR 생성 + 폰 attach까지 한 번에 진행하세요 (relay 세션)." : "list_pages로 로컬 Chromium 페이지 attach를 확인하세요."
|
|
7032
7138
|
};
|
|
7033
7139
|
} finally {
|
|
7034
7140
|
this.swapInFlight = false;
|
|
@@ -7087,7 +7193,7 @@ async function runDebugServer(options = {}) {
|
|
|
7087
7193
|
})),
|
|
7088
7194
|
attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
|
|
7089
7195
|
inspectorUrl,
|
|
7090
|
-
mode: deriveEnvironment(router.active.kind,
|
|
7196
|
+
mode: deriveEnvironment(router.active.kind, router.activeRelayOrigin)
|
|
7091
7197
|
};
|
|
7092
7198
|
};
|
|
7093
7199
|
const getDirectInspectorUrl = () => {
|
|
@@ -7219,7 +7325,7 @@ async function runDebugServer(options = {}) {
|
|
|
7219
7325
|
* 1. `start_debug({ mode: 'local-browser' })` launches a local Chromium with
|
|
7220
7326
|
* `--remote-debugging-port=<port>` and attaches a `LocalCdpConnection`;
|
|
7221
7327
|
* 2. the intoss/external relay families lazy-boot on the first
|
|
7222
|
-
* `start_debug({ mode: 'relay-staging' | 'relay-
|
|
7328
|
+
* `start_debug({ mode: 'relay-staging' | 'relay-sandbox' })` (#665: relay-live removed);
|
|
7223
7329
|
* 3. all of this runs through the SAME direction-neutral
|
|
7224
7330
|
* `DualConnectionRouter` that `runDebugServer` uses (issue #356).
|
|
7225
7331
|
*
|
|
@@ -7231,7 +7337,7 @@ async function runDebugServer(options = {}) {
|
|
|
7231
7337
|
* env 1 (local), then env 3 (intoss-private) in ONE session, no restart" — now
|
|
7232
7338
|
* works from either entry point.
|
|
7233
7339
|
*
|
|
7234
|
-
* `
|
|
7340
|
+
* `start_attach` (relay-specific) stays effectively hidden / non-applicable
|
|
7235
7341
|
* until the relay family is booted: before the first relay switch the env
|
|
7236
7342
|
* derives to `mock` and `relayTunnelStatus()` reports "down", so the tool fails
|
|
7237
7343
|
* with a clear "tunnel not up" message. After a relay switch the relay tunnel
|
|
@@ -7442,10 +7548,9 @@ async function runLocalDebugServer(options = {}) {
|
|
|
7442
7548
|
* Symmetry with `runDebugServer` / `runLocalDebugServer` (#356, #378, #396): all
|
|
7443
7549
|
* three families are lazy-booted — the env-2 external relay on the first
|
|
7444
7550
|
* `start_debug({ mode: 'relay-sandbox' })`, the local family on `local-browser`,
|
|
7445
|
-
* the intoss relay on `relay-staging
|
|
7446
|
-
* session can hot-switch
|
|
7447
|
-
*
|
|
7448
|
-
* origin, liveIntent off).
|
|
7551
|
+
* the intoss relay on `relay-staging` (#665: relay-live removed) — so a
|
|
7552
|
+
* `--target=mobile` session can hot-switch without a restart. The active env
|
|
7553
|
+
* derives to `relay-mobile` (external-PWA origin).
|
|
7449
7554
|
*
|
|
7450
7555
|
* SECRET-HANDLING: `AIT_RELAY_BASE_URL` is read once here via
|
|
7451
7556
|
* {@link readMobileRelayBaseUrl}; when unset it throws
|
|
@@ -7826,25 +7931,30 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
7826
7931
|
availableIn: "both"
|
|
7827
7932
|
},
|
|
7828
7933
|
{
|
|
7829
|
-
name: "
|
|
7830
|
-
description: "
|
|
7934
|
+
name: "start_attach",
|
|
7935
|
+
description: "Switches into a relay mode (if given), builds a self-attaching deep-link QR for a real device, and waits for the phone to attach — all in one call. NOT available in dev-mode — requires a live cloudflared relay (Tier B, relay-only). To use this tool: restart the MCP server with `--mode=debug` (or omit --mode) and set MCP_ENV=relay, then call start_attach to generate the QR for phone scanning. See: https://docs.aitc.dev/guides/debug-relay",
|
|
7831
7936
|
inputSchema: {
|
|
7832
7937
|
type: "object",
|
|
7833
7938
|
properties: {
|
|
7834
|
-
|
|
7939
|
+
mode: {
|
|
7835
7940
|
type: "string",
|
|
7836
|
-
|
|
7941
|
+
enum: [
|
|
7942
|
+
"local-browser",
|
|
7943
|
+
"relay-sandbox",
|
|
7944
|
+
"relay-staging"
|
|
7945
|
+
],
|
|
7946
|
+
description: "Optional relay mode to switch into before attaching."
|
|
7837
7947
|
},
|
|
7838
|
-
|
|
7839
|
-
type: "
|
|
7840
|
-
description: "
|
|
7948
|
+
scheme_url: {
|
|
7949
|
+
type: "string",
|
|
7950
|
+
description: "The intoss-private:// URL from `ait deploy --scheme-only` (env 3/relay-staging)."
|
|
7841
7951
|
},
|
|
7842
7952
|
wait_timeout_seconds: {
|
|
7843
7953
|
type: "number",
|
|
7844
|
-
description: "Maximum seconds to wait
|
|
7954
|
+
description: "Maximum seconds to wait for a page to attach (default 60, range 1–600). Invalid inputs fall back to default."
|
|
7845
7955
|
}
|
|
7846
7956
|
},
|
|
7847
|
-
required: [
|
|
7957
|
+
required: []
|
|
7848
7958
|
},
|
|
7849
7959
|
availableIn: "relay"
|
|
7850
7960
|
},
|
|
@@ -7942,10 +8052,6 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
7942
8052
|
timeout_ms: {
|
|
7943
8053
|
type: "number",
|
|
7944
8054
|
description: "Per-file evaluate timeout in ms."
|
|
7945
|
-
},
|
|
7946
|
-
confirm: {
|
|
7947
|
-
type: "boolean",
|
|
7948
|
-
description: "Required in relay-live sessions."
|
|
7949
8055
|
}
|
|
7950
8056
|
},
|
|
7951
8057
|
required: ["files"]
|
|
@@ -7971,7 +8077,7 @@ const CDP_ONLY_TOOL_NAMES = new Set([
|
|
|
7971
8077
|
* Listed in dev-mode tool surface (issue #323) so agents get a hand-off hint
|
|
7972
8078
|
* toward `--mode=debug` instead of "Unknown tool".
|
|
7973
8079
|
*/
|
|
7974
|
-
const TIER_B_TOOL_NAMES = new Set(["
|
|
8080
|
+
const TIER_B_TOOL_NAMES = new Set(["start_attach"]);
|
|
7975
8081
|
/**
|
|
7976
8082
|
* Builds the `list_pages` dev-mode shim response.
|
|
7977
8083
|
* Returns the Vite dev URL as a single-entry page list with `devMode: true`.
|
|
@@ -8081,7 +8187,7 @@ function createDevServer(deps = {}) {
|
|
|
8081
8187
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
8082
8188
|
const server = new Server({
|
|
8083
8189
|
name: "ait-devtools",
|
|
8084
|
-
version: "0.1.
|
|
8190
|
+
version: "0.1.110"
|
|
8085
8191
|
}, { capabilities: { tools: {} } });
|
|
8086
8192
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
8087
8193
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -8156,7 +8262,7 @@ async function runDevServer() {
|
|
|
8156
8262
|
*
|
|
8157
8263
|
* --mode=debug (default)
|
|
8158
8264
|
* --target=relay (default) — CDP/Chii relay + cloudflared quick tunnel.
|
|
8159
|
-
* Attach a running mini-app (real Toss WebView, env 3
|
|
8265
|
+
* Attach a running mini-app (real Toss WebView, env 3) and read its
|
|
8160
8266
|
* console + network over CDP without a human watching a phone.
|
|
8161
8267
|
* --target=local — CDP direct-attach to a local Chromium launched by the
|
|
8162
8268
|
* MCP server (env 1). No relay or tunnel; the browser is launched
|
|
@@ -8172,26 +8278,12 @@ async function runDevServer() {
|
|
|
8172
8278
|
* Back-compat (issue #348): the legacy `--mode`/`--target` flags and `MCP_ENV`
|
|
8173
8279
|
* still work. `--target=relay`/`local` select the initial active connection;
|
|
8174
8280
|
* the in-session `start_debug(mode)` MCP tool can then flip between them with no
|
|
8175
|
-
* restart. `MCP_ENV
|
|
8176
|
-
* `
|
|
8177
|
-
* (the active connection's `kind` is authoritative).
|
|
8281
|
+
* restart. `MCP_ENV` values are accepted and ignored (the active connection's
|
|
8282
|
+
* `kind` is authoritative; `relay-live` and `liveIntent` are removed, #665).
|
|
8178
8283
|
*
|
|
8179
8284
|
* Node-only stdio process.
|
|
8180
8285
|
*/
|
|
8181
8286
|
/**
|
|
8182
|
-
* Seeds the module-level `liveIntent` bit from the deprecated `MCP_ENV` alias
|
|
8183
|
-
* (issue #348). `MCP_ENV=relay-live` is the only value that matters now — it
|
|
8184
|
-
* arms LIVE intent at boot so a session launched straight into env 4 has the
|
|
8185
|
-
* guard active without a `start_debug({ mode: 'relay-live' })` round-trip. All
|
|
8186
|
-
* other `MCP_ENV` values (`mock`, `relay`, `relay-dev`) are accepted-and-ignored
|
|
8187
|
-
* for env derivation — the active connection's `kind` is authoritative.
|
|
8188
|
-
*
|
|
8189
|
-
* SECRET-HANDLING: reads only the env-var string; never logs a secret.
|
|
8190
|
-
*/
|
|
8191
|
-
function seedLiveIntentFromEnv(env = process.env) {
|
|
8192
|
-
if (env.MCP_ENV === "relay-live") setLiveIntent(true);
|
|
8193
|
-
}
|
|
8194
|
-
/**
|
|
8195
8287
|
* Returns `true` when `--force` or `--takeover` is present in argv.
|
|
8196
8288
|
*
|
|
8197
8289
|
* Both flags are accepted as aliases — `--force` is the short form listed in
|
|
@@ -8248,7 +8340,6 @@ function normalizeTarget(value) {
|
|
|
8248
8340
|
}
|
|
8249
8341
|
async function main() {
|
|
8250
8342
|
const args = process.argv.slice(2);
|
|
8251
|
-
seedLiveIntentFromEnv();
|
|
8252
8343
|
if (parseMode(args) === "dev") await runDevServer();
|
|
8253
8344
|
else {
|
|
8254
8345
|
const target = parseTarget(args);
|
|
@@ -8283,6 +8374,6 @@ if (isEntrypoint()) main().catch((err) => {
|
|
|
8283
8374
|
process.exitCode = 1;
|
|
8284
8375
|
});
|
|
8285
8376
|
//#endregion
|
|
8286
|
-
export { parseForce, parseMode, parseTarget
|
|
8377
|
+
export { parseForce, parseMode, parseTarget };
|
|
8287
8378
|
|
|
8288
8379
|
//# sourceMappingURL=cli.js.map
|