@ait-co/devtools 0.1.51 → 0.1.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/cli.d.ts +19 -1
- package/dist/mcp/cli.d.ts.map +1 -1
- package/dist/mcp/cli.js +479 -217
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +28 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +2 -2
- package/package.json +1 -1
package/dist/mcp/cli.js
CHANGED
|
@@ -213,6 +213,8 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 3e4;
|
|
|
213
213
|
* opens a client websocket to it, enables Phase 1 domains, and buffers events.
|
|
214
214
|
*/
|
|
215
215
|
var ChiiCdpConnection = class {
|
|
216
|
+
/** Authoritative connection kind (issue #348) — relay-backed. */
|
|
217
|
+
kind = "relay";
|
|
216
218
|
relayBaseUrl;
|
|
217
219
|
bufferSize;
|
|
218
220
|
commandTimeoutMs;
|
|
@@ -909,7 +911,7 @@ function isLiveRelayEnv(env) {
|
|
|
909
911
|
return env === "relay-live";
|
|
910
912
|
}
|
|
911
913
|
/**
|
|
912
|
-
* Maps the
|
|
914
|
+
* Maps the `McpEnvironment` union to the legacy two-value union
|
|
913
915
|
* (`'mock' | 'relay'`) for backward-compatible fields in diagnostics output.
|
|
914
916
|
*/
|
|
915
917
|
function toLegacyEnv(env) {
|
|
@@ -917,93 +919,42 @@ function toLegacyEnv(env) {
|
|
|
917
919
|
return "relay";
|
|
918
920
|
}
|
|
919
921
|
/**
|
|
920
|
-
*
|
|
922
|
+
* Reconstructs the three-value `McpEnvironment` output string from the two
|
|
923
|
+
* orthogonal signals (issue #348):
|
|
921
924
|
*
|
|
922
|
-
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
* the relay transport. A target whose URL is on that host is, by construction,
|
|
926
|
-
* reached over the relay.
|
|
925
|
+
* - `kind === 'local'` → `'mock'`
|
|
926
|
+
* - `kind === 'relay'` && liveIntent → `'relay-live'`
|
|
927
|
+
* - `kind === 'relay'` && !liveIntent → `'relay-dev'`
|
|
927
928
|
*
|
|
928
|
-
*
|
|
929
|
+
* Pure — used at every output boundary (envelope `meta.env`, `get_diagnostics`,
|
|
930
|
+
* `measure_safe_area` provenance) so the surface never sniffs a URL again.
|
|
929
931
|
*/
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
* over the Chii relay. Used for `getEnvironment()` precedence step 2.
|
|
934
|
-
*/
|
|
935
|
-
function isRelayUrl(url) {
|
|
936
|
-
if (typeof url !== "string" || url.length === 0) return false;
|
|
937
|
-
return RELAY_URL_PATTERNS.some((p) => p.test(url));
|
|
932
|
+
function deriveEnvironment(kind, liveIntent) {
|
|
933
|
+
if (kind === "local") return "mock";
|
|
934
|
+
return liveIntent ? "relay-live" : "relay-dev";
|
|
938
935
|
}
|
|
939
936
|
/**
|
|
940
|
-
*
|
|
941
|
-
* regardless of env vars or connection state. Cleared with `null`.
|
|
942
|
-
*/
|
|
943
|
-
let envOverride = null;
|
|
944
|
-
/**
|
|
945
|
-
* Parses the `MCP_ENV` env var into a `McpEnvironment` if valid.
|
|
937
|
+
* Module-level `relay-dev` vs `relay-live` intent bit (issue #348).
|
|
946
938
|
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
951
|
-
* - `relay` → `relay-dev` (backward-compat alias — resolves to relay-dev)
|
|
939
|
+
* Armed by `start_debug({ mode: 'relay-live' })` (and seeded at boot by the
|
|
940
|
+
* deprecated `MCP_ENV=relay-live` alias). Disarming is implicit: when the
|
|
941
|
+
* active connection becomes local, the LIVE guard reads
|
|
942
|
+
* `connection.kind === 'relay' && liveIntent`, so a stale `true` bit is inert.
|
|
952
943
|
*
|
|
953
|
-
*
|
|
944
|
+
* SECRET-HANDLING: this is a boolean — never a secret. Safe to read in logs.
|
|
954
945
|
*/
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
if (raw === "relay-live") return "relay-live";
|
|
960
|
-
if (raw === "relay") return "relay-dev";
|
|
961
|
-
}
|
|
962
|
-
/**
|
|
963
|
-
* Returns the current MCP environment, applying the precedence rules:
|
|
964
|
-
* 1. test override (if set)
|
|
965
|
-
* 2. `MCP_ENV` env var
|
|
966
|
-
* 3. CDP target URL pattern match → `relay-dev` (conservative — LIVE
|
|
967
|
-
* requires explicit MCP_ENV=relay-live opt-in)
|
|
968
|
-
* 4. caller-stated `defaultEnv` (intent hint from the CLI mode)
|
|
969
|
-
* 5. baked-in default `mock`
|
|
970
|
-
*/
|
|
971
|
-
function getEnvironment(input = {}) {
|
|
972
|
-
if (envOverride !== null) return envOverride;
|
|
973
|
-
const fromEnv = readEnvVar();
|
|
974
|
-
if (fromEnv !== void 0) return fromEnv;
|
|
975
|
-
const { connection, defaultEnv } = input;
|
|
976
|
-
if (connection !== void 0) {
|
|
977
|
-
const targets = connection.listTargets();
|
|
978
|
-
for (const t of targets) if (isRelayUrl(t.url)) return "relay-dev";
|
|
979
|
-
}
|
|
980
|
-
return defaultEnv ?? "mock";
|
|
946
|
+
let liveIntent = false;
|
|
947
|
+
/** Returns the current `liveIntent` bit. */
|
|
948
|
+
function getLiveIntent() {
|
|
949
|
+
return liveIntent;
|
|
981
950
|
}
|
|
982
951
|
/**
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
* secret value is ever returned.
|
|
952
|
+
* Sets the `liveIntent` bit. Called by `start_debug` (true for `relay-live`,
|
|
953
|
+
* false for every other mode) and once at boot by the `MCP_ENV=relay-live`
|
|
954
|
+
* deprecated alias.
|
|
987
955
|
*/
|
|
988
|
-
function
|
|
989
|
-
|
|
990
|
-
if (envOverride === "mock") return "env-var-mock";
|
|
991
|
-
if (envOverride === "relay-live") return "env-var-relay-live";
|
|
992
|
-
return "env-var-relay-dev";
|
|
993
|
-
}
|
|
994
|
-
const rawVar = process.env.MCP_ENV;
|
|
995
|
-
const fromEnv = readEnvVar();
|
|
996
|
-
if (fromEnv === "mock") return "env-var-mock";
|
|
997
|
-
if (fromEnv === "relay-live") return "env-var-relay-live";
|
|
998
|
-
if (fromEnv === "relay-dev") return rawVar === "relay" ? "env-var-relay-compat" : "env-var-relay-dev";
|
|
999
|
-
const { connection, defaultEnv } = input;
|
|
1000
|
-
if (connection !== void 0) {
|
|
1001
|
-
const targets = connection.listTargets();
|
|
1002
|
-
for (const t of targets) if (isRelayUrl(t.url)) return "cdp-target-url-relay-pattern";
|
|
1003
|
-
}
|
|
1004
|
-
if (defaultEnv === "relay-live") return "default-relay-live";
|
|
1005
|
-
if (defaultEnv === "relay-dev") return "default-relay-dev";
|
|
1006
|
-
return "default-mock";
|
|
956
|
+
function setLiveIntent(value) {
|
|
957
|
+
liveIntent = value;
|
|
1007
958
|
}
|
|
1008
959
|
//#endregion
|
|
1009
960
|
//#region src/mcp/errors.ts
|
|
@@ -1027,7 +978,8 @@ function mcpError(message) {
|
|
|
1027
978
|
* @param toolName - 거부된 tool 이름.
|
|
1028
979
|
* @param requiredEnv - 해당 tool이 요구하는 환경 ('mock' | 'relay').
|
|
1029
980
|
* @param currentEnv - 현재 세션 환경.
|
|
1030
|
-
* @param reason - 환경이 결정된
|
|
981
|
+
* @param reason - 환경이 결정된 근거를 나타내는 파생 문자열
|
|
982
|
+
* (예: `derived:kind=relay,liveIntent=true`).
|
|
1031
983
|
*/
|
|
1032
984
|
function tierRejectionError(toolName, requiredEnv, currentEnv, reason) {
|
|
1033
985
|
return mcpError(`${`${toolName}은 ${requiredEnv === "relay" ? "relay (실기기 연결)" : "mock (로컬 브라우저)"} 환경에서만 사용할 수 있습니다. 현재 환경: ${currentEnv === "relay" ? "relay" : "mock"} (${reason}). ${requiredEnv === "relay" ? "relay로 전환하려면 MCP_ENV=relay 설정 후 서버를 재시작하고 build_attach_url → QR 스캔으로 실기기를 attach하세요." : "mock으로 전환하려면 MCP_ENV=mock 설정 후 서버를 재시작하세요."}`}\n\n${`tool ${toolName} is available only in ${requiredEnv}. Current environment is ${currentEnv} (${reason}).`}`);
|
|
@@ -1162,6 +1114,8 @@ const PHASE_1_EVENTS = [
|
|
|
1162
1114
|
* `about:blank`, `about:newtab`, or a devtools:// URL.
|
|
1163
1115
|
*/
|
|
1164
1116
|
var LocalCdpConnection = class {
|
|
1117
|
+
/** Authoritative connection kind (issue #348) — local Chromium CDP. */
|
|
1118
|
+
kind = "local";
|
|
1165
1119
|
devtoolsHttpUrl;
|
|
1166
1120
|
bufferSize;
|
|
1167
1121
|
emitter = new EventEmitter();
|
|
@@ -2317,6 +2271,31 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
2317
2271
|
},
|
|
2318
2272
|
availableIn: "both"
|
|
2319
2273
|
},
|
|
2274
|
+
{
|
|
2275
|
+
name: "start_debug",
|
|
2276
|
+
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/4, real-device Toss WebView 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\nmodes:\n local-browser-dev / local-browser-cdp — local Chromium CDP attach (env 1, mock). Both route to the local connection; the names preserve dev-vs-cdp intent.\n relay-dev — real-device dogfood relay (env 3). Side-effect tools run unguarded.\n relay-live — real-device live/production relay (env 4, read-only debugging). Arms the LIVE guard: call_sdk/evaluate then require confirm: true. Entering relay-live ALSO requires confirm: true on this call to acknowledge LIVE intent.\n\nSwitching back to a local mode automatically disarms the LIVE guard.",
|
|
2277
|
+
inputSchema: {
|
|
2278
|
+
type: "object",
|
|
2279
|
+
properties: {
|
|
2280
|
+
mode: {
|
|
2281
|
+
type: "string",
|
|
2282
|
+
enum: [
|
|
2283
|
+
"local-browser-dev",
|
|
2284
|
+
"local-browser-cdp",
|
|
2285
|
+
"relay-dev",
|
|
2286
|
+
"relay-live"
|
|
2287
|
+
],
|
|
2288
|
+
description: "Target environment to switch to. relay-live additionally requires confirm: true."
|
|
2289
|
+
},
|
|
2290
|
+
confirm: {
|
|
2291
|
+
type: "boolean",
|
|
2292
|
+
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."
|
|
2293
|
+
}
|
|
2294
|
+
},
|
|
2295
|
+
required: ["mode"]
|
|
2296
|
+
},
|
|
2297
|
+
availableIn: "both"
|
|
2298
|
+
},
|
|
2320
2299
|
{
|
|
2321
2300
|
name: "get_diagnostics",
|
|
2322
2301
|
description: "Returns a single-call server status snapshot so the agent can diagnose \"why is this not working?\" without calling multiple 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), environment (kind: mock|relay-dev|relay-live, env: mock|relay backward-compat, reason, liveGuardActive: true when relay-live LIVE guard is active), 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). Call this first when debugging session state.",
|
|
@@ -2381,7 +2360,8 @@ function filterToolsByEnvironment(tools, env) {
|
|
|
2381
2360
|
const BOOTSTRAP_TOOL_NAMES = new Set([
|
|
2382
2361
|
"build_attach_url",
|
|
2383
2362
|
"get_diagnostics",
|
|
2384
|
-
"list_pages"
|
|
2363
|
+
"list_pages",
|
|
2364
|
+
"start_debug"
|
|
2385
2365
|
]);
|
|
2386
2366
|
/** Renders a CDP `RemoteObject` console arg to a stable display string. */
|
|
2387
2367
|
function renderRemoteObject(arg) {
|
|
@@ -3112,19 +3092,26 @@ var InMemoryDiagnosticsCollector = class {
|
|
|
3112
3092
|
}
|
|
3113
3093
|
};
|
|
3114
3094
|
/**
|
|
3115
|
-
*
|
|
3116
|
-
*
|
|
3117
|
-
*
|
|
3118
|
-
*
|
|
3119
|
-
*
|
|
3120
|
-
*
|
|
3095
|
+
* Returns the `@modelcontextprotocol/sdk` version baked in at build time via
|
|
3096
|
+
* the `__MCP_SDK_VERSION__` define (see `tsdown.config.ts`). Returns `null`
|
|
3097
|
+
* when the define is absent (unbundled test runs) and the runtime fallback
|
|
3098
|
+
* below also fails — diagnostics must never throw.
|
|
3099
|
+
*
|
|
3100
|
+
* The old implementation resolved `@modelcontextprotocol/sdk/package.json` at
|
|
3101
|
+
* runtime, but that subpath is NOT in the SDK's `exports` map, so the resolve
|
|
3102
|
+
* threw `ERR_PACKAGE_PATH_NOT_EXPORTED` and this always returned `null` in a
|
|
3103
|
+
* real bundle (issue #361). The build-time define sidesteps the exports gate.
|
|
3104
|
+
*
|
|
3105
|
+
* Kept `async` for call-site compatibility (`Promise.all` at the caller); the
|
|
3106
|
+
* body is synchronous apart from the best-effort fallback.
|
|
3121
3107
|
*/
|
|
3122
3108
|
async function readMcpSdkVersion() {
|
|
3123
3109
|
try {
|
|
3124
3110
|
const { createRequire } = await import("node:module");
|
|
3125
|
-
const
|
|
3111
|
+
const entry = createRequire(import.meta.url).resolve("@modelcontextprotocol/sdk");
|
|
3112
|
+
const root = entry.slice(0, entry.indexOf("@modelcontextprotocol/sdk") + 25);
|
|
3126
3113
|
const { readFileSync } = await import("node:fs");
|
|
3127
|
-
const raw = readFileSync(
|
|
3114
|
+
const raw = readFileSync(`${root}/package.json`, "utf8");
|
|
3128
3115
|
const parsed = JSON.parse(raw);
|
|
3129
3116
|
return typeof parsed.version === "string" ? parsed.version : null;
|
|
3130
3117
|
} catch {
|
|
@@ -3137,12 +3124,7 @@ async function readMcpSdkVersion() {
|
|
|
3137
3124
|
* some test environments that skip the build step).
|
|
3138
3125
|
*/
|
|
3139
3126
|
function readDevtoolsVersion() {
|
|
3140
|
-
|
|
3141
|
-
const v = globalThis.__VERSION__;
|
|
3142
|
-
return typeof v === "string" && v.length > 0 ? v : null;
|
|
3143
|
-
} catch {
|
|
3144
|
-
return null;
|
|
3145
|
-
}
|
|
3127
|
+
return "0.1.53";
|
|
3146
3128
|
}
|
|
3147
3129
|
/**
|
|
3148
3130
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -3559,6 +3541,10 @@ function extractDeploymentId(schemeUrl) {
|
|
|
3559
3541
|
return null;
|
|
3560
3542
|
}
|
|
3561
3543
|
}
|
|
3544
|
+
/** Returns `true` when the mode routes to a relay connection. */
|
|
3545
|
+
function isRelayMode(mode) {
|
|
3546
|
+
return mode === "relay-dev" || mode === "relay-live";
|
|
3547
|
+
}
|
|
3562
3548
|
/**
|
|
3563
3549
|
* Waits for the first target matching `filterFn` to attach, using the
|
|
3564
3550
|
* event-driven `waitForFirstTarget()` when the connection supports it
|
|
@@ -3613,23 +3599,19 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
|
|
|
3613
3599
|
* naturally via `enableDomains`). The tier only controls visibility.
|
|
3614
3600
|
*/
|
|
3615
3601
|
function createDebugServer(deps) {
|
|
3616
|
-
const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep,
|
|
3617
|
-
const
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
}));
|
|
3621
|
-
const resolveEnvironmentReason = getEnvReasonDep ?? (() => getEnvironmentReason({
|
|
3622
|
-
connection,
|
|
3623
|
-
defaultEnv
|
|
3624
|
-
}));
|
|
3602
|
+
const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret } = deps;
|
|
3603
|
+
const router = routerDep ?? makeSingleConnectionRouter(connection);
|
|
3604
|
+
const resolveEnvironment = getEnvDep ?? (() => deriveEnvironment(router.active.kind, getLiveIntent()));
|
|
3605
|
+
const resolveEnvironmentReason = getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},liveIntent=${getLiveIntent()}`);
|
|
3625
3606
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
3626
3607
|
const server = new Server({
|
|
3627
3608
|
name: "ait-debug",
|
|
3628
|
-
version: "0.1.
|
|
3609
|
+
version: "0.1.53"
|
|
3629
3610
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
3630
3611
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
3612
|
+
const conn = router.active;
|
|
3631
3613
|
const env = resolveEnvironment();
|
|
3632
|
-
const attached =
|
|
3614
|
+
const attached = conn.listTargets().length > 0;
|
|
3633
3615
|
const envFiltered = filterToolsByEnvironment(DEBUG_TOOL_DEFINITIONS, env);
|
|
3634
3616
|
return { tools: attached ? envFiltered.map((tool) => ({ ...tool })) : envFiltered.filter((tool) => BOOTSTRAP_TOOL_NAMES.has(tool.name)).map((tool) => ({ ...tool })) };
|
|
3635
3617
|
});
|
|
@@ -3642,10 +3624,22 @@ function createDebugServer(deps) {
|
|
|
3642
3624
|
}],
|
|
3643
3625
|
isError: true
|
|
3644
3626
|
};
|
|
3627
|
+
const conn = router.active;
|
|
3628
|
+
if (name === "start_debug") {
|
|
3629
|
+
const rawMode = request.params.arguments?.mode;
|
|
3630
|
+
const mode = normalizeStartDebugMode(rawMode);
|
|
3631
|
+
if (mode === null) return mcpError("start_debug: mode가 올바르지 않습니다. 'local-browser-dev' | 'local-browser-cdp' | 'relay-dev' | 'relay-live' 중 하나를 전달하세요.");
|
|
3632
|
+
const confirm = request.params.arguments?.confirm === true;
|
|
3633
|
+
try {
|
|
3634
|
+
return jsonResult$1(await router.switchMode(mode, confirm));
|
|
3635
|
+
} catch (err) {
|
|
3636
|
+
return errorResult(err, name);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3645
3639
|
const env = resolveEnvironment();
|
|
3640
|
+
const envReason = resolveEnvironmentReason();
|
|
3646
3641
|
if (!isToolAvailableIn(name, env)) {
|
|
3647
3642
|
const requiredEnv = getToolAvailability(name) ?? "unknown";
|
|
3648
|
-
const envReason = resolveEnvironmentReason();
|
|
3649
3643
|
logWarn("tool.error", {
|
|
3650
3644
|
tool: name,
|
|
3651
3645
|
errorKind: "tier-filter",
|
|
@@ -3656,7 +3650,7 @@ function createDebugServer(deps) {
|
|
|
3656
3650
|
return tierRejectionError(name, requiredEnv, env, envReason);
|
|
3657
3651
|
}
|
|
3658
3652
|
if (isAitToolName(name)) try {
|
|
3659
|
-
await
|
|
3653
|
+
await conn.enableDomains();
|
|
3660
3654
|
switch (name) {
|
|
3661
3655
|
case "AIT.getSdkCallHistory": return jsonResult$1(await getSdkCallHistory(aitSource));
|
|
3662
3656
|
case "AIT.getMockState": return jsonResult$1(await getMockState(aitSource));
|
|
@@ -3669,17 +3663,15 @@ function createDebugServer(deps) {
|
|
|
3669
3663
|
if (name === "get_diagnostics") try {
|
|
3670
3664
|
const rawLimit = request.params.arguments?.recent_errors_limit;
|
|
3671
3665
|
const recentErrorsLimit = typeof rawLimit === "number" && rawLimit > 0 ? rawLimit : 10;
|
|
3672
|
-
|
|
3666
|
+
return envelopeResult$1(await getDiagnostics({
|
|
3673
3667
|
tunnel: getTunnelStatus(),
|
|
3674
|
-
connection,
|
|
3675
|
-
env
|
|
3676
|
-
envReason
|
|
3668
|
+
connection: conn,
|
|
3669
|
+
env,
|
|
3670
|
+
envReason,
|
|
3677
3671
|
collector,
|
|
3678
3672
|
readLock: readServerLock,
|
|
3679
3673
|
recentErrorsLimit
|
|
3680
|
-
});
|
|
3681
|
-
const attached = connection.listTargets().length > 0;
|
|
3682
|
-
return envelopeResult$1(result, name, resolveEnvironment(), attached);
|
|
3674
|
+
}), name, env, conn.listTargets().length > 0);
|
|
3683
3675
|
} catch (err) {
|
|
3684
3676
|
return errorResult(err, name);
|
|
3685
3677
|
}
|
|
@@ -3724,9 +3716,9 @@ function createDebugServer(deps) {
|
|
|
3724
3716
|
}] };
|
|
3725
3717
|
let attachedPagesHl = [];
|
|
3726
3718
|
try {
|
|
3727
|
-
attachedPagesHl = await waitForAttachWithEvents(
|
|
3719
|
+
attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
|
|
3728
3720
|
} catch {
|
|
3729
|
-
attachedPagesHl =
|
|
3721
|
+
attachedPagesHl = conn.listTargets();
|
|
3730
3722
|
return {
|
|
3731
3723
|
content: [{
|
|
3732
3724
|
type: "text",
|
|
@@ -3735,7 +3727,7 @@ function createDebugServer(deps) {
|
|
|
3735
3727
|
isError: true
|
|
3736
3728
|
};
|
|
3737
3729
|
}
|
|
3738
|
-
const pagesResultHl = listPages(
|
|
3730
|
+
const pagesResultHl = listPages(conn, getTunnelStatus());
|
|
3739
3731
|
return { content: [{
|
|
3740
3732
|
type: "text",
|
|
3741
3733
|
text: `${headlessText}\n\n${JSON.stringify(pagesResultHl, null, 2)}`
|
|
@@ -3761,9 +3753,9 @@ function createDebugServer(deps) {
|
|
|
3761
3753
|
}] };
|
|
3762
3754
|
let attachedPages = [];
|
|
3763
3755
|
try {
|
|
3764
|
-
attachedPages = await waitForAttachWithEvents(
|
|
3756
|
+
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
|
|
3765
3757
|
} catch {
|
|
3766
|
-
attachedPages =
|
|
3758
|
+
attachedPages = conn.listTargets();
|
|
3767
3759
|
return {
|
|
3768
3760
|
content: [{
|
|
3769
3761
|
type: "text",
|
|
@@ -3772,7 +3764,7 @@ function createDebugServer(deps) {
|
|
|
3772
3764
|
isError: true
|
|
3773
3765
|
};
|
|
3774
3766
|
}
|
|
3775
|
-
const pagesResult = listPages(
|
|
3767
|
+
const pagesResult = listPages(conn, getTunnelStatus());
|
|
3776
3768
|
return { content: [{
|
|
3777
3769
|
type: "text",
|
|
3778
3770
|
text: `${shortText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
@@ -3801,9 +3793,9 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3801
3793
|
}] };
|
|
3802
3794
|
let attachedPagesFb = [];
|
|
3803
3795
|
try {
|
|
3804
|
-
attachedPagesFb = await waitForAttachWithEvents(
|
|
3796
|
+
attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
|
|
3805
3797
|
} catch {
|
|
3806
|
-
attachedPagesFb =
|
|
3798
|
+
attachedPagesFb = conn.listTargets();
|
|
3807
3799
|
return {
|
|
3808
3800
|
content: [{
|
|
3809
3801
|
type: "text",
|
|
@@ -3812,7 +3804,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3812
3804
|
isError: true
|
|
3813
3805
|
};
|
|
3814
3806
|
}
|
|
3815
|
-
const pagesResultFb = listPages(
|
|
3807
|
+
const pagesResultFb = listPages(conn, getTunnelStatus());
|
|
3816
3808
|
return { content: [{
|
|
3817
3809
|
type: "text",
|
|
3818
3810
|
text: `${baseText}\n\n${JSON.stringify(pagesResultFb, null, 2)}`
|
|
@@ -3830,9 +3822,9 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3830
3822
|
}] };
|
|
3831
3823
|
let attachedPages = [];
|
|
3832
3824
|
try {
|
|
3833
|
-
attachedPages = await waitForAttachWithEvents(
|
|
3825
|
+
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
|
|
3834
3826
|
} catch {
|
|
3835
|
-
attachedPages =
|
|
3827
|
+
attachedPages = conn.listTargets();
|
|
3836
3828
|
return {
|
|
3837
3829
|
content: [{
|
|
3838
3830
|
type: "text",
|
|
@@ -3841,7 +3833,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3841
3833
|
isError: true
|
|
3842
3834
|
};
|
|
3843
3835
|
}
|
|
3844
|
-
const pagesResult = listPages(
|
|
3836
|
+
const pagesResult = listPages(conn, getTunnelStatus());
|
|
3845
3837
|
return { content: [{
|
|
3846
3838
|
type: "text",
|
|
3847
3839
|
text: `${baseText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
@@ -3851,65 +3843,55 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3851
3843
|
}
|
|
3852
3844
|
}
|
|
3853
3845
|
try {
|
|
3854
|
-
await
|
|
3846
|
+
await conn.enableDomains();
|
|
3855
3847
|
} catch (err) {
|
|
3856
3848
|
if (name === "list_pages") {
|
|
3857
3849
|
try {
|
|
3858
|
-
await
|
|
3850
|
+
await conn.refreshTargets?.();
|
|
3859
3851
|
} catch {}
|
|
3860
|
-
|
|
3861
|
-
const attached = connection.listTargets().length > 0;
|
|
3862
|
-
return envelopeResult$1(pagesData, name, resolveEnvironment(), attached);
|
|
3852
|
+
return envelopeResult$1(listPages(conn, getTunnelStatus()), name, env, conn.listTargets().length > 0);
|
|
3863
3853
|
}
|
|
3864
3854
|
return classifyEnableDomainError(err, name);
|
|
3865
3855
|
}
|
|
3866
3856
|
try {
|
|
3867
3857
|
switch (name) {
|
|
3868
|
-
case "list_console_messages": return jsonResult$1(listConsoleMessages(
|
|
3858
|
+
case "list_console_messages": return jsonResult$1(listConsoleMessages(conn));
|
|
3869
3859
|
case "list_exceptions": {
|
|
3870
3860
|
const rawLimit = request.params.arguments?.limit;
|
|
3871
|
-
return jsonResult$1({ exceptions: listExceptions(
|
|
3861
|
+
return jsonResult$1({ exceptions: listExceptions(conn, typeof rawLimit === "number" && rawLimit > 0 ? rawLimit : 50) });
|
|
3872
3862
|
}
|
|
3873
|
-
case "list_network_requests": return jsonResult$1(listNetworkRequests(
|
|
3874
|
-
case "list_pages":
|
|
3863
|
+
case "list_network_requests": return jsonResult$1(listNetworkRequests(conn));
|
|
3864
|
+
case "list_pages":
|
|
3875
3865
|
try {
|
|
3876
|
-
await
|
|
3866
|
+
await conn.refreshTargets?.();
|
|
3877
3867
|
} catch {}
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
}
|
|
3882
|
-
case "get_dom_document": return jsonResult$1(await getDomDocument(connection));
|
|
3883
|
-
case "take_snapshot": return jsonResult$1(await takeSnapshot(connection));
|
|
3868
|
+
return envelopeResult$1(listPages(conn, getTunnelStatus()), name, env, conn.listTargets().length > 0);
|
|
3869
|
+
case "get_dom_document": return jsonResult$1(await getDomDocument(conn));
|
|
3870
|
+
case "take_snapshot": return jsonResult$1(await takeSnapshot(conn));
|
|
3884
3871
|
case "take_screenshot": {
|
|
3885
|
-
const shot = await takeScreenshot(
|
|
3872
|
+
const shot = await takeScreenshot(conn);
|
|
3886
3873
|
return { content: [{
|
|
3887
3874
|
type: "image",
|
|
3888
3875
|
data: shot.data,
|
|
3889
3876
|
mimeType: shot.mimeType
|
|
3890
3877
|
}] };
|
|
3891
3878
|
}
|
|
3892
|
-
case "measure_safe_area":
|
|
3893
|
-
const safeAreaData = await measureSafeArea(connection, resolveEnvironment());
|
|
3894
|
-
const safeAreaAttached = connection.listTargets().length > 0;
|
|
3895
|
-
return envelopeResult$1(safeAreaData, name, resolveEnvironment(), safeAreaAttached);
|
|
3896
|
-
}
|
|
3879
|
+
case "measure_safe_area": return envelopeResult$1(await measureSafeArea(conn, env), name, env, conn.listTargets().length > 0);
|
|
3897
3880
|
case "evaluate": {
|
|
3898
3881
|
const expression = request.params.arguments?.expression;
|
|
3899
3882
|
if (typeof expression !== "string" || expression === "") return mcpError("evaluate: expression 인자가 비어 있습니다. 평가할 JavaScript 표현식을 전달하세요.");
|
|
3900
|
-
if (
|
|
3901
|
-
return jsonResult$1(await evaluate(
|
|
3883
|
+
if (conn.kind === "relay" && getLiveIntent() && request.params.arguments?.confirm !== true) return liveGuardError("evaluate");
|
|
3884
|
+
return jsonResult$1(await evaluate(conn, expression));
|
|
3902
3885
|
}
|
|
3903
3886
|
case "call_sdk": {
|
|
3904
3887
|
const sdkName = request.params.arguments?.name;
|
|
3905
3888
|
if (typeof sdkName !== "string" || sdkName === "") return mcpError("call_sdk: name 인자가 비어 있습니다. 호출할 SDK 메서드 이름을 전달하세요.");
|
|
3906
3889
|
const rawArgs = request.params.arguments?.args;
|
|
3907
3890
|
const sdkArgs = Array.isArray(rawArgs) ? rawArgs : [];
|
|
3908
|
-
if (
|
|
3909
|
-
const sdkResult = await callSdk(
|
|
3891
|
+
if (conn.kind === "relay" && getLiveIntent() && request.params.arguments?.confirm !== true) return liveGuardError("call_sdk");
|
|
3892
|
+
const sdkResult = await callSdk(conn, sdkName, sdkArgs);
|
|
3910
3893
|
if (!sdkResult.ok && typeof sdkResult.error === "string" && sdkResult.error.startsWith("sdk-absent:")) return sdkAbsentError("call_sdk");
|
|
3911
|
-
|
|
3912
|
-
return envelopeResult$1(sdkResult, name, resolveEnvironment(), callSdkAttached);
|
|
3894
|
+
return envelopeResult$1(sdkResult, name, env, conn.listTargets().length > 0);
|
|
3913
3895
|
}
|
|
3914
3896
|
default: return unknownTool(name);
|
|
3915
3897
|
}
|
|
@@ -3919,6 +3901,46 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3919
3901
|
});
|
|
3920
3902
|
return server;
|
|
3921
3903
|
}
|
|
3904
|
+
/**
|
|
3905
|
+
* Normalizes a raw `start_debug` `mode` argument to a `StartDebugMode`, or
|
|
3906
|
+
* `null` when the value is not one of the four accepted modes.
|
|
3907
|
+
*/
|
|
3908
|
+
function normalizeStartDebugMode(raw) {
|
|
3909
|
+
if (raw === "local-browser-dev") return "local-browser-dev";
|
|
3910
|
+
if (raw === "local-browser-cdp") return "local-browser-cdp";
|
|
3911
|
+
if (raw === "relay-dev") return "relay-dev";
|
|
3912
|
+
if (raw === "relay-live") return "relay-live";
|
|
3913
|
+
return null;
|
|
3914
|
+
}
|
|
3915
|
+
/**
|
|
3916
|
+
* Builds a trivial `ConnectionRouter` pinned to a single connection (issue
|
|
3917
|
+
* #348). Used by `createDebugServer` when no real dual router is injected —
|
|
3918
|
+
* every existing single-connection test and the `local`-only / `relay`-only
|
|
3919
|
+
* boot path. `switchMode` here cannot lazily boot another family, so it only
|
|
3920
|
+
* honors a request that matches the connection's own kind (and arms/disarms
|
|
3921
|
+
* `liveIntent` accordingly for relay-live); any cross-family request is
|
|
3922
|
+
* rejected with a clear "dynamic switch unavailable in this session" error.
|
|
3923
|
+
*/
|
|
3924
|
+
function makeSingleConnectionRouter(connection) {
|
|
3925
|
+
return {
|
|
3926
|
+
get active() {
|
|
3927
|
+
return connection;
|
|
3928
|
+
},
|
|
3929
|
+
switchMode(mode, confirm) {
|
|
3930
|
+
if (isRelayMode(mode) !== (connection.kind === "relay")) return Promise.reject(/* @__PURE__ */ new Error(`start_debug: 이 세션은 단일 ${connection.kind} 연결만 보유합니다 — '${mode}'로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). MCP 서버를 원하는 모드로 재시작하세요.`));
|
|
3931
|
+
if (mode === "relay-live" && !confirm) return Promise.reject(/* @__PURE__ */ new Error("start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — 실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요."));
|
|
3932
|
+
setLiveIntent(mode === "relay-live");
|
|
3933
|
+
const environment = deriveEnvironment(connection.kind, getLiveIntent());
|
|
3934
|
+
return Promise.resolve({
|
|
3935
|
+
mode,
|
|
3936
|
+
environment,
|
|
3937
|
+
kind: connection.kind,
|
|
3938
|
+
liveGuardActive: connection.kind === "relay" && getLiveIntent(),
|
|
3939
|
+
nextStep: connection.kind === "relay" ? "build_attach_url로 attach QR을 생성하세요." : "list_pages로 로컬 페이지 attach를 확인하세요."
|
|
3940
|
+
});
|
|
3941
|
+
}
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3922
3944
|
function jsonResult$1(value) {
|
|
3923
3945
|
return { content: [{
|
|
3924
3946
|
type: "text",
|
|
@@ -4080,21 +4102,72 @@ function createRelayConnection(relayBaseUrl) {
|
|
|
4080
4102
|
return new ChiiCdpConnection({ relayBaseUrl });
|
|
4081
4103
|
}
|
|
4082
4104
|
/**
|
|
4083
|
-
*
|
|
4084
|
-
*
|
|
4085
|
-
*
|
|
4086
|
-
*
|
|
4087
|
-
*
|
|
4088
|
-
*
|
|
4105
|
+
* AIT source that always forwards over the *currently active* connection
|
|
4106
|
+
* (issue #348). The single-connection `ChiiAitSource` binds one sender at
|
|
4107
|
+
* construction; in the dual-connection daemon the AIT.* domain must follow the
|
|
4108
|
+
* active connection across `start_debug` swaps, so this indirection reads
|
|
4109
|
+
* `getActive()` on every call.
|
|
4110
|
+
*
|
|
4111
|
+
* Both `ChiiCdpConnection` and `LocalCdpConnection` expose `sendCommand`, so
|
|
4112
|
+
* the active connection is a valid `AitCommandSender`.
|
|
4089
4113
|
*/
|
|
4090
|
-
|
|
4091
|
-
|
|
4114
|
+
var RoutingAitSource = class extends ChiiAitSource {
|
|
4115
|
+
constructor(getActive) {
|
|
4116
|
+
super({ sendCommand: (method, params) => getActive().sendCommand(method, params) });
|
|
4117
|
+
}
|
|
4118
|
+
};
|
|
4119
|
+
/**
|
|
4120
|
+
* Boots the local-browser family (issues #348, #356). Launches a Chromium with
|
|
4121
|
+
* `--remote-debugging-port` and returns a `LocalCdpConnection` attached to it,
|
|
4122
|
+
* plus a `stop()` that kills both.
|
|
4123
|
+
*
|
|
4124
|
+
* Used two ways:
|
|
4125
|
+
* - `runDebugServer` (relay-eager): the dual router's lazy callback, booted at
|
|
4126
|
+
* most once on the first `start_debug({ mode: 'local-*' })`.
|
|
4127
|
+
* - `runLocalDebugServer` (local-eager, #356): the eager family booted at
|
|
4128
|
+
* startup.
|
|
4129
|
+
*/
|
|
4130
|
+
async function bootLocalFamily() {
|
|
4131
|
+
const chromium = await launchChromium({
|
|
4132
|
+
port: 0,
|
|
4133
|
+
devUrl: process.env.AIT_DEVTOOLS_URL ?? "http://localhost:5173"
|
|
4134
|
+
});
|
|
4135
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
4136
|
+
const connection = new LocalCdpConnection({ devtoolsHttpUrl: chromium.devtoolsUrl });
|
|
4137
|
+
return {
|
|
4138
|
+
connection,
|
|
4139
|
+
stop() {
|
|
4140
|
+
connection.close();
|
|
4141
|
+
chromium.stop();
|
|
4142
|
+
}
|
|
4143
|
+
};
|
|
4144
|
+
}
|
|
4145
|
+
/**
|
|
4146
|
+
* Boots the relay family (issues #348, #356): starts the Chii relay on an
|
|
4147
|
+
* OS-assigned port (with optional TOTP gate), opens a cloudflared quick tunnel
|
|
4148
|
+
* to the relay's confirmed port in the background, prints the attach banner,
|
|
4149
|
+
* and arms the tunnel health probe. Returns a {@link BootedFamily} whose
|
|
4150
|
+
* `getTunnelStatus()` reflects the live tunnel (it flips up once the background
|
|
4151
|
+
* tunnel resolves and follows reissues).
|
|
4152
|
+
*
|
|
4153
|
+
* Used two ways (symmetry with {@link bootLocalFamily}):
|
|
4154
|
+
* - `runDebugServer` (relay-eager): booted at startup.
|
|
4155
|
+
* - `runLocalDebugServer` (local-eager, #356): the dual router's lazy
|
|
4156
|
+
* callback, booted at most once on the first `start_debug({ mode: 'relay-*' })`.
|
|
4157
|
+
*
|
|
4158
|
+
* The relay base URL is only known after `startChiiRelay()` resolves, so the
|
|
4159
|
+
* `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside
|
|
4160
|
+
* this function, after the relay port is confirmed.
|
|
4161
|
+
*
|
|
4162
|
+
* SECRET-HANDLING: the TOTP secret rides only inside `verifyAuth`; the wssUrl
|
|
4163
|
+
* (relay host) is never logged here directly.
|
|
4164
|
+
*/
|
|
4165
|
+
async function bootRelayFamily(options = {}) {
|
|
4092
4166
|
const relayPort = options.relayPort ?? 0;
|
|
4093
|
-
const
|
|
4094
|
-
const totpEnabled = verifyAuth !== void 0;
|
|
4167
|
+
const totpEnabled = options.verifyAuth !== void 0;
|
|
4095
4168
|
const relay = await startChiiRelay({
|
|
4096
4169
|
port: relayPort,
|
|
4097
|
-
verifyAuth
|
|
4170
|
+
verifyAuth: options.verifyAuth
|
|
4098
4171
|
});
|
|
4099
4172
|
logInfo("server.start", {
|
|
4100
4173
|
port: relay.port,
|
|
@@ -4102,18 +4175,18 @@ async function runDebugServer(options = {}) {
|
|
|
4102
4175
|
});
|
|
4103
4176
|
let tunnel = null;
|
|
4104
4177
|
let tunnelStatus = makeTunnelStatus(false, null);
|
|
4105
|
-
generateAttachToken();
|
|
4106
4178
|
let tunnelProbe = null;
|
|
4179
|
+
generateAttachToken();
|
|
4107
4180
|
startQuickTunnel(relay.port).then((t) => {
|
|
4108
4181
|
tunnel = t;
|
|
4109
4182
|
tunnelStatus = makeTunnelStatus(true, t.wssUrl);
|
|
4110
|
-
|
|
4183
|
+
options.onWssUrl?.(t.wssUrl);
|
|
4111
4184
|
logInfo("tunnel.up", { totpEnabled });
|
|
4112
4185
|
tunnelProbe = startTunnelHealthProbe(t, relay.port, {
|
|
4113
4186
|
onReissue: (newTunnel) => {
|
|
4114
4187
|
tunnel = newTunnel;
|
|
4115
4188
|
tunnelStatus = makeTunnelStatus(true, newTunnel.wssUrl, null, 0);
|
|
4116
|
-
|
|
4189
|
+
options.onWssUrl?.(newTunnel.wssUrl);
|
|
4117
4190
|
printAttachBanner({
|
|
4118
4191
|
wssUrl: newTunnel.wssUrl,
|
|
4119
4192
|
totpEnabled
|
|
@@ -4137,39 +4210,178 @@ async function runDebugServer(options = {}) {
|
|
|
4137
4210
|
logError("tunnel.down", { msg: `Failed to open cloudflared quick tunnel: ${err instanceof Error ? err.message : String(err)}. The relay is up locally; attach over the public URL is unavailable until the tunnel starts.` });
|
|
4138
4211
|
});
|
|
4139
4212
|
const connection = createRelayConnection(relay.baseUrl);
|
|
4140
|
-
|
|
4213
|
+
return {
|
|
4214
|
+
connection,
|
|
4215
|
+
getTunnelStatus: () => tunnelStatus,
|
|
4216
|
+
stop() {
|
|
4217
|
+
tunnelProbe?.stop();
|
|
4218
|
+
tunnel?.stop();
|
|
4219
|
+
connection.close();
|
|
4220
|
+
relay.close();
|
|
4221
|
+
}
|
|
4222
|
+
};
|
|
4223
|
+
}
|
|
4224
|
+
/**
|
|
4225
|
+
* Production `ConnectionRouter` (issues #348, #356 — DUAL-CONNECTION-COEXIST).
|
|
4226
|
+
*
|
|
4227
|
+
* Holds two families — one booted eagerly at startup, the other lazily on the
|
|
4228
|
+
* first cross-family switch — an `active` pointer, and the single attach watcher
|
|
4229
|
+
* armed on the active connection. The router is **direction-neutral** (#356):
|
|
4230
|
+
* either family kind can be the eager one, so a `--target=local` session can
|
|
4231
|
+
* hot-switch into relay (and vice versa) without restarting the MCP server.
|
|
4232
|
+
*
|
|
4233
|
+
* `switchMode`:
|
|
4234
|
+
* 1. rejects re-entrant swaps (`swapInFlight`) and an unconfirmed relay-live;
|
|
4235
|
+
* 2. routes by the requested mode's family kind: same kind as `eager` → reuse
|
|
4236
|
+
* eager; different kind → lazily boot (once) and keep warm;
|
|
4237
|
+
* 3. flips `active` (the MCP `Server` never re-handshakes — it reads through
|
|
4238
|
+
* `active` per request);
|
|
4239
|
+
* 4. sets `liveIntent` (true only for relay-live);
|
|
4240
|
+
* 5. stops the old attach watcher and re-arms one on the new connection
|
|
4241
|
+
* (the watcher self-clears, so re-arm is mandatory);
|
|
4242
|
+
* 6. emits `tools/list_changed`.
|
|
4243
|
+
*
|
|
4244
|
+
* Inactive infra is left WARM — teardown happens only at process exit (the
|
|
4245
|
+
* unified shutdown in the run functions), which is what keeps a phone attach
|
|
4246
|
+
* alive across a local→relay→local round trip.
|
|
4247
|
+
*/
|
|
4248
|
+
var DualConnectionRouter = class {
|
|
4249
|
+
deps;
|
|
4250
|
+
/** The opposite-kind family, booted lazily on the first cross-family switch. */
|
|
4251
|
+
lazyFamily = null;
|
|
4252
|
+
activeFamily;
|
|
4253
|
+
server = null;
|
|
4254
|
+
attachWatcher = null;
|
|
4255
|
+
swapInFlight = false;
|
|
4256
|
+
constructor(deps) {
|
|
4257
|
+
this.deps = deps;
|
|
4258
|
+
this.activeFamily = deps.eager;
|
|
4259
|
+
}
|
|
4260
|
+
get active() {
|
|
4261
|
+
return this.activeFamily.connection;
|
|
4262
|
+
}
|
|
4263
|
+
/** Every booted family (for unified shutdown). */
|
|
4264
|
+
bootedFamilies() {
|
|
4265
|
+
return this.lazyFamily ? [this.deps.eager, this.lazyFamily] : [this.deps.eager];
|
|
4266
|
+
}
|
|
4267
|
+
/**
|
|
4268
|
+
* Live tunnel status of the relay family, regardless of whether relay is the
|
|
4269
|
+
* eager or lazy family (#356). Returns "down" until the relay family has been
|
|
4270
|
+
* booted (local-eager sessions before the first relay switch) — which is the
|
|
4271
|
+
* correct signal for `build_attach_url` (no tunnel exists yet).
|
|
4272
|
+
*/
|
|
4273
|
+
relayTunnelStatus() {
|
|
4274
|
+
for (const family of this.bootedFamilies()) if (family.getTunnelStatus) return family.getTunnelStatus();
|
|
4275
|
+
return {
|
|
4276
|
+
up: false,
|
|
4277
|
+
wssUrl: null
|
|
4278
|
+
};
|
|
4279
|
+
}
|
|
4280
|
+
/**
|
|
4281
|
+
* Binds the MCP `Server` and arms the initial attach watcher on the active
|
|
4282
|
+
* connection. Called once after `createDebugServer` + `connect`.
|
|
4283
|
+
*/
|
|
4284
|
+
start(server) {
|
|
4285
|
+
this.server = server;
|
|
4286
|
+
this.armWatcher();
|
|
4287
|
+
}
|
|
4288
|
+
/** Stops the current attach watcher (for shutdown). */
|
|
4289
|
+
stopWatcher() {
|
|
4290
|
+
this.attachWatcher?.stop();
|
|
4291
|
+
this.attachWatcher = null;
|
|
4292
|
+
}
|
|
4293
|
+
/** Arms a fresh attach watcher on the current active connection. */
|
|
4294
|
+
armWatcher() {
|
|
4295
|
+
const server = this.server;
|
|
4296
|
+
if (!server) return;
|
|
4297
|
+
this.attachWatcher = startAttachWatcher(this.activeFamily.connection, server, this.deps.attachWatcherIntervalMs ?? 1e3, () => {
|
|
4298
|
+
this.deps.diagnosticsCollector.recordAttach();
|
|
4299
|
+
if (this.activeFamily.connection.kind === "relay") this.deps.devtoolsOpener.open(this.relayTunnelStatus().wssUrl, deriveEnvironment(this.activeFamily.connection.kind, getLiveIntent()));
|
|
4300
|
+
});
|
|
4301
|
+
}
|
|
4302
|
+
async switchMode(mode, confirm) {
|
|
4303
|
+
if (this.swapInFlight) throw new Error("start_debug: 이전 전환이 아직 진행 중입니다 — 잠시 후 다시 호출하세요.");
|
|
4304
|
+
if (mode === "relay-live" && !confirm) throw new Error("start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — 실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요.");
|
|
4305
|
+
this.swapInFlight = true;
|
|
4306
|
+
try {
|
|
4307
|
+
const wantRelay = isRelayMode(mode);
|
|
4308
|
+
const wantKind = wantRelay ? "relay" : "local";
|
|
4309
|
+
let target;
|
|
4310
|
+
if (wantKind === this.deps.eager.connection.kind) target = this.deps.eager;
|
|
4311
|
+
else {
|
|
4312
|
+
if (this.lazyFamily === null) this.lazyFamily = await this.deps.bootLazy();
|
|
4313
|
+
target = this.lazyFamily;
|
|
4314
|
+
}
|
|
4315
|
+
this.activeFamily = target;
|
|
4316
|
+
setLiveIntent(mode === "relay-live");
|
|
4317
|
+
this.stopWatcher();
|
|
4318
|
+
this.armWatcher();
|
|
4319
|
+
this.server?.sendToolListChanged();
|
|
4320
|
+
return {
|
|
4321
|
+
mode,
|
|
4322
|
+
environment: deriveEnvironment(target.connection.kind, getLiveIntent()),
|
|
4323
|
+
kind: target.connection.kind,
|
|
4324
|
+
liveGuardActive: target.connection.kind === "relay" && getLiveIntent(),
|
|
4325
|
+
nextStep: wantRelay ? "build_attach_url로 attach QR을 생성하세요 (relay 세션)." : "list_pages로 로컬 Chromium 페이지 attach를 확인하세요."
|
|
4326
|
+
};
|
|
4327
|
+
} finally {
|
|
4328
|
+
this.swapInFlight = false;
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
};
|
|
4332
|
+
/**
|
|
4333
|
+
* Boots the live debug stack and serves it over stdio:
|
|
4334
|
+
* 1. start the Chii relay on an OS-assigned port (with TOTP auth if
|
|
4335
|
+
* AIT_DEBUG_TOTP_SECRET is set),
|
|
4336
|
+
* 2. open a cloudflared quick tunnel to the relay's confirmed port,
|
|
4337
|
+
* 3. print relay URL + attach instructions,
|
|
4338
|
+
* 4. expose the debug tools backed by a `ChiiCdpConnection` + `ChiiAitSource`.
|
|
4339
|
+
*/
|
|
4340
|
+
async function runDebugServer(options = {}) {
|
|
4341
|
+
const lockHandle = acquireLock({ force: options.force ?? false });
|
|
4342
|
+
const verifyAuth = buildRelayVerifyAuth();
|
|
4343
|
+
const relayFamily = await bootRelayFamily({
|
|
4344
|
+
relayPort: options.relayPort,
|
|
4345
|
+
verifyAuth,
|
|
4346
|
+
onWssUrl: (wssUrl) => lockHandle.updateWssUrl(wssUrl)
|
|
4347
|
+
});
|
|
4348
|
+
const devtoolsOpener = new AutoDevtoolsOpener();
|
|
4349
|
+
const diagnosticsCollector = new InMemoryDiagnosticsCollector();
|
|
4350
|
+
const router = new DualConnectionRouter({
|
|
4351
|
+
eager: relayFamily,
|
|
4352
|
+
bootLazy: bootLocalFamily,
|
|
4353
|
+
diagnosticsCollector,
|
|
4354
|
+
devtoolsOpener
|
|
4355
|
+
});
|
|
4356
|
+
const aitSource = new RoutingAitSource(() => {
|
|
4357
|
+
return router.active;
|
|
4358
|
+
});
|
|
4141
4359
|
let qrServer;
|
|
4142
4360
|
try {
|
|
4143
4361
|
qrServer = await startQrHttpServer();
|
|
4144
4362
|
} catch (err) {
|
|
4145
4363
|
logWarn("server.start", { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${err instanceof Error ? err.message : String(err)}` });
|
|
4146
4364
|
}
|
|
4147
|
-
const devtoolsOpener = new AutoDevtoolsOpener();
|
|
4148
|
-
const diagnosticsCollector = new InMemoryDiagnosticsCollector();
|
|
4149
4365
|
const server = createDebugServer({
|
|
4150
|
-
connection,
|
|
4366
|
+
connection: router.active,
|
|
4367
|
+
router,
|
|
4151
4368
|
aitSource,
|
|
4152
|
-
getTunnelStatus: () =>
|
|
4369
|
+
getTunnelStatus: () => router.relayTunnelStatus(),
|
|
4153
4370
|
get qrHttpServer() {
|
|
4154
4371
|
return qrServer;
|
|
4155
4372
|
},
|
|
4156
4373
|
diagnosticsCollector,
|
|
4157
|
-
defaultEnv: "relay-dev",
|
|
4158
4374
|
...process.env.AIT_DEBUG_TOTP_SECRET ? { totpSecret: process.env.AIT_DEBUG_TOTP_SECRET } : {}
|
|
4159
4375
|
});
|
|
4160
4376
|
const transport = new StdioServerTransport();
|
|
4161
4377
|
let closed = false;
|
|
4162
|
-
let attachWatcher = null;
|
|
4163
4378
|
let parentWatcher = null;
|
|
4164
4379
|
const shutdown = () => {
|
|
4165
4380
|
if (closed) return;
|
|
4166
4381
|
closed = true;
|
|
4167
4382
|
parentWatcher?.stop();
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
connection.close();
|
|
4171
|
-
tunnel?.stop();
|
|
4172
|
-
relay.close();
|
|
4383
|
+
router.stopWatcher();
|
|
4384
|
+
for (const family of router.bootedFamilies()) family.stop();
|
|
4173
4385
|
server.close();
|
|
4174
4386
|
qrServer?.close();
|
|
4175
4387
|
lockHandle.release();
|
|
@@ -4181,9 +4393,8 @@ async function runDebugServer(options = {}) {
|
|
|
4181
4393
|
if (!closed) {
|
|
4182
4394
|
closed = true;
|
|
4183
4395
|
parentWatcher?.stop();
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
tunnel?.stop();
|
|
4396
|
+
router.stopWatcher();
|
|
4397
|
+
for (const family of router.bootedFamilies()) family.stop();
|
|
4187
4398
|
lockHandle.release();
|
|
4188
4399
|
}
|
|
4189
4400
|
});
|
|
@@ -4204,13 +4415,7 @@ async function runDebugServer(options = {}) {
|
|
|
4204
4415
|
process.exit(1);
|
|
4205
4416
|
});
|
|
4206
4417
|
await server.connect(transport);
|
|
4207
|
-
|
|
4208
|
-
diagnosticsCollector.recordAttach();
|
|
4209
|
-
devtoolsOpener.open(tunnelStatus.wssUrl, getEnvironment({
|
|
4210
|
-
connection,
|
|
4211
|
-
defaultEnv: "relay-dev"
|
|
4212
|
-
}));
|
|
4213
|
-
});
|
|
4418
|
+
router.start(server);
|
|
4214
4419
|
if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== "1") {
|
|
4215
4420
|
parentWatcher = startParentWatcher(() => {
|
|
4216
4421
|
shutdown();
|
|
@@ -4230,19 +4435,28 @@ async function runDebugServer(options = {}) {
|
|
|
4230
4435
|
* Boots the local-browser debug stack and serves it over stdio:
|
|
4231
4436
|
* 1. launch a local Chromium with `--remote-debugging-port=<port>`,
|
|
4232
4437
|
* 2. attach a `LocalCdpConnection` to the first non-blank page target,
|
|
4233
|
-
* 3. expose the debug tools
|
|
4234
|
-
*
|
|
4235
|
-
*
|
|
4236
|
-
*
|
|
4237
|
-
*
|
|
4238
|
-
*
|
|
4438
|
+
* 3. expose the debug tools through the SAME direction-neutral
|
|
4439
|
+
* `DualConnectionRouter` that `runDebugServer` uses (issue #356) — the
|
|
4440
|
+
* local family is eager, the relay family is lazy-booted on the first
|
|
4441
|
+
* `start_debug({ mode: 'relay-*' })`.
|
|
4442
|
+
*
|
|
4443
|
+
* Symmetry with `runDebugServer` (#356): starting with `--target=local` no
|
|
4444
|
+
* longer pins a single-connection router. A `--target=local` session can
|
|
4445
|
+
* hot-switch into relay (env 1 → env 3) without restarting the MCP server,
|
|
4446
|
+
* closing the asymmetry where only the default (relay-target) entry point had
|
|
4447
|
+
* bidirectional hot-switch. The intended fidelity-ladder flow — "validate in
|
|
4448
|
+
* env 1 (local), then env 3 (intoss-private) in ONE session, no restart" — now
|
|
4449
|
+
* works from either entry point.
|
|
4450
|
+
*
|
|
4451
|
+
* `build_attach_url` (relay-specific) stays effectively hidden / non-applicable
|
|
4452
|
+
* until the relay family is booted: before the first relay switch the env
|
|
4453
|
+
* derives to `mock` and `relayTunnelStatus()` reports "down", so the tool fails
|
|
4454
|
+
* with a clear "tunnel not up" message. After a relay switch the relay tunnel
|
|
4455
|
+
* is live and the tool works.
|
|
4239
4456
|
*
|
|
4240
4457
|
* The AIT.* tools (`AIT.getSdkCallHistory`, `AIT.getMockState`,
|
|
4241
|
-
* `AIT.getOperationalEnvironment`) ride the
|
|
4242
|
-
* `
|
|
4243
|
-
* the sdk-example dev-bridge (`window.__sdkCall` install) lands in sdk-example;
|
|
4244
|
-
* until then they return the sdk-example "bridge absent" message — which is
|
|
4245
|
-
* expected and noted in the PR as an explicit out-of-scope follow-up.
|
|
4458
|
+
* `AIT.getOperationalEnvironment`) ride the *active* connection's CDP channel
|
|
4459
|
+
* via `RoutingAitSource`, so they follow `start_debug` swaps.
|
|
4246
4460
|
*/
|
|
4247
4461
|
async function runLocalDebugServer(options = {}) {
|
|
4248
4462
|
const lockHandle = acquireLock({ force: options.force ?? false });
|
|
@@ -4251,30 +4465,57 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4251
4465
|
devUrl: options.devUrl ?? process.env.AIT_DEVTOOLS_URL ?? "http://localhost:5173"
|
|
4252
4466
|
});
|
|
4253
4467
|
await new Promise((r) => setTimeout(r, 800));
|
|
4254
|
-
const
|
|
4255
|
-
const
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4468
|
+
const localConnection = new LocalCdpConnection({ devtoolsHttpUrl: chromium.devtoolsUrl });
|
|
4469
|
+
const localFamily = {
|
|
4470
|
+
connection: localConnection,
|
|
4471
|
+
stop() {
|
|
4472
|
+
localConnection.close();
|
|
4473
|
+
chromium.stop();
|
|
4474
|
+
}
|
|
4259
4475
|
};
|
|
4476
|
+
const verifyAuth = buildRelayVerifyAuth();
|
|
4477
|
+
const devtoolsOpener = new AutoDevtoolsOpener();
|
|
4478
|
+
const diagnosticsCollector = new InMemoryDiagnosticsCollector();
|
|
4479
|
+
const router = new DualConnectionRouter({
|
|
4480
|
+
eager: localFamily,
|
|
4481
|
+
bootLazy: () => bootRelayFamily({
|
|
4482
|
+
verifyAuth,
|
|
4483
|
+
onWssUrl: (wssUrl) => lockHandle.updateWssUrl(wssUrl)
|
|
4484
|
+
}),
|
|
4485
|
+
diagnosticsCollector,
|
|
4486
|
+
devtoolsOpener
|
|
4487
|
+
});
|
|
4488
|
+
const aitSource = new RoutingAitSource(() => {
|
|
4489
|
+
return router.active;
|
|
4490
|
+
});
|
|
4491
|
+
let qrServer;
|
|
4492
|
+
try {
|
|
4493
|
+
qrServer = await startQrHttpServer();
|
|
4494
|
+
} catch (err) {
|
|
4495
|
+
logWarn("server.start", { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${err instanceof Error ? err.message : String(err)}` });
|
|
4496
|
+
}
|
|
4260
4497
|
const server = createDebugServer({
|
|
4261
|
-
connection,
|
|
4498
|
+
connection: router.active,
|
|
4499
|
+
router,
|
|
4262
4500
|
aitSource,
|
|
4263
|
-
getTunnelStatus: () =>
|
|
4264
|
-
|
|
4501
|
+
getTunnelStatus: () => router.relayTunnelStatus(),
|
|
4502
|
+
get qrHttpServer() {
|
|
4503
|
+
return qrServer;
|
|
4504
|
+
},
|
|
4505
|
+
diagnosticsCollector,
|
|
4506
|
+
...process.env.AIT_DEBUG_TOTP_SECRET ? { totpSecret: process.env.AIT_DEBUG_TOTP_SECRET } : {}
|
|
4265
4507
|
});
|
|
4266
4508
|
const transport = new StdioServerTransport();
|
|
4267
4509
|
let closed = false;
|
|
4268
|
-
let attachWatcher = null;
|
|
4269
4510
|
let parentWatcher = null;
|
|
4270
4511
|
const shutdown = () => {
|
|
4271
4512
|
if (closed) return;
|
|
4272
4513
|
closed = true;
|
|
4273
4514
|
parentWatcher?.stop();
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
chromium.stop();
|
|
4515
|
+
router.stopWatcher();
|
|
4516
|
+
for (const family of router.bootedFamilies()) family.stop();
|
|
4277
4517
|
server.close();
|
|
4518
|
+
qrServer?.close();
|
|
4278
4519
|
lockHandle.release();
|
|
4279
4520
|
};
|
|
4280
4521
|
process.once("SIGINT", shutdown);
|
|
@@ -4284,8 +4525,8 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4284
4525
|
if (!closed) {
|
|
4285
4526
|
closed = true;
|
|
4286
4527
|
parentWatcher?.stop();
|
|
4287
|
-
|
|
4288
|
-
|
|
4528
|
+
router.stopWatcher();
|
|
4529
|
+
for (const family of router.bootedFamilies()) family.stop();
|
|
4289
4530
|
lockHandle.release();
|
|
4290
4531
|
}
|
|
4291
4532
|
});
|
|
@@ -4308,7 +4549,7 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4308
4549
|
process.exit(1);
|
|
4309
4550
|
});
|
|
4310
4551
|
await server.connect(transport);
|
|
4311
|
-
|
|
4552
|
+
router.start(server);
|
|
4312
4553
|
if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== "1") {
|
|
4313
4554
|
parentWatcher = startParentWatcher(() => {
|
|
4314
4555
|
shutdown();
|
|
@@ -4752,7 +4993,7 @@ function createDevServer(deps = {}) {
|
|
|
4752
4993
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
4753
4994
|
const server = new Server({
|
|
4754
4995
|
name: "ait-devtools",
|
|
4755
|
-
version: "0.1.
|
|
4996
|
+
version: "0.1.53"
|
|
4756
4997
|
}, { capabilities: { tools: {} } });
|
|
4757
4998
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
4758
4999
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -4836,9 +5077,29 @@ async function runDevServer() {
|
|
|
4836
5077
|
* --mode=dev — dev mode — reads the live browser mock state from a running
|
|
4837
5078
|
* Vite dev server (the devtools#130 `devtools_get_mock_state` surface).
|
|
4838
5079
|
*
|
|
5080
|
+
* Back-compat (issue #348): the legacy `--mode`/`--target` flags and `MCP_ENV`
|
|
5081
|
+
* still work. `--target=relay`/`local` select the initial active connection;
|
|
5082
|
+
* the in-session `start_debug(mode)` MCP tool can then flip between them with no
|
|
5083
|
+
* restart. `MCP_ENV=relay-live` seeds LIVE intent at boot (deprecated alias);
|
|
5084
|
+
* `MCP_ENV=mock|relay|relay-dev` are accepted and ignored for env derivation
|
|
5085
|
+
* (the active connection's `kind` is authoritative).
|
|
5086
|
+
*
|
|
4839
5087
|
* Node-only stdio process.
|
|
4840
5088
|
*/
|
|
4841
5089
|
/**
|
|
5090
|
+
* Seeds the module-level `liveIntent` bit from the deprecated `MCP_ENV` alias
|
|
5091
|
+
* (issue #348). `MCP_ENV=relay-live` is the only value that matters now — it
|
|
5092
|
+
* arms LIVE intent at boot so a session launched straight into env 4 has the
|
|
5093
|
+
* guard active without a `start_debug({ mode: 'relay-live' })` round-trip. All
|
|
5094
|
+
* other `MCP_ENV` values (`mock`, `relay`, `relay-dev`) are accepted-and-ignored
|
|
5095
|
+
* for env derivation — the active connection's `kind` is authoritative.
|
|
5096
|
+
*
|
|
5097
|
+
* SECRET-HANDLING: reads only the env-var string; never logs a secret.
|
|
5098
|
+
*/
|
|
5099
|
+
function seedLiveIntentFromEnv(env = process.env) {
|
|
5100
|
+
if (env.MCP_ENV === "relay-live") setLiveIntent(true);
|
|
5101
|
+
}
|
|
5102
|
+
/**
|
|
4842
5103
|
* Returns `true` when `--force` or `--takeover` is present in argv.
|
|
4843
5104
|
*
|
|
4844
5105
|
* Both flags are accepted as aliases — `--force` is the short form listed in
|
|
@@ -4893,6 +5154,7 @@ function normalizeTarget(value) {
|
|
|
4893
5154
|
}
|
|
4894
5155
|
async function main() {
|
|
4895
5156
|
const args = process.argv.slice(2);
|
|
5157
|
+
seedLiveIntentFromEnv();
|
|
4896
5158
|
if (parseMode(args) === "dev") await runDevServer();
|
|
4897
5159
|
else {
|
|
4898
5160
|
const target = parseTarget(args);
|
|
@@ -4926,6 +5188,6 @@ if (isEntrypoint()) main().catch((err) => {
|
|
|
4926
5188
|
process.exitCode = 1;
|
|
4927
5189
|
});
|
|
4928
5190
|
//#endregion
|
|
4929
|
-
export { parseForce, parseMode, parseTarget };
|
|
5191
|
+
export { parseForce, parseMode, parseTarget, seedLiveIntentFromEnv };
|
|
4930
5192
|
|
|
4931
5193
|
//# sourceMappingURL=cli.js.map
|