@ait-co/devtools 0.1.50 → 0.1.52
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/in-app/index.d.ts +7 -0
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +16 -0
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.d.ts +19 -1
- package/dist/mcp/cli.d.ts.map +1 -1
- package/dist/mcp/cli.js +572 -212
- 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";
|
|
946
|
+
let liveIntent = false;
|
|
947
|
+
/** Returns the current `liveIntent` bit. */
|
|
948
|
+
function getLiveIntent() {
|
|
949
|
+
return liveIntent;
|
|
961
950
|
}
|
|
962
951
|
/**
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
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`
|
|
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.
|
|
970
955
|
*/
|
|
971
|
-
function
|
|
972
|
-
|
|
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";
|
|
981
|
-
}
|
|
982
|
-
/**
|
|
983
|
-
* Returns the `EnvironmentReason` that drove the current `getEnvironment()`
|
|
984
|
-
* result. Used by stderr logs and the rejection-reason payload on Tier A/B
|
|
985
|
-
* mismatch errors. SECRET-HANDLING: only stable enum strings — no URL or
|
|
986
|
-
* secret value is ever returned.
|
|
987
|
-
*/
|
|
988
|
-
function getEnvironmentReason(input = {}) {
|
|
989
|
-
if (envOverride !== null) {
|
|
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) {
|
|
@@ -3148,6 +3128,7 @@ function readDevtoolsVersion() {
|
|
|
3148
3128
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
3149
3129
|
*
|
|
3150
3130
|
* Branch rules (evaluated in priority order):
|
|
3131
|
+
* 0. tunnel.droppedAt non-null → restart (permanent tunnel drop — highest priority)
|
|
3151
3132
|
* 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)
|
|
3152
3133
|
* 1b. tunnel.up === false AND env is mock → wait_for_page (local target: tunnel-less is normal)
|
|
3153
3134
|
* 2. tunnel.up, pages empty, env === relay → build_attach_url (start attach)
|
|
@@ -3157,6 +3138,10 @@ function readDevtoolsVersion() {
|
|
|
3157
3138
|
* Pure — does not throw; receives the final assembled snapshot fields.
|
|
3158
3139
|
*/
|
|
3159
3140
|
function computeNextRecommendedAction(tunnel, pages, env) {
|
|
3141
|
+
if (tunnel.droppedAt != null) return {
|
|
3142
|
+
tool: "restart",
|
|
3143
|
+
reason: `tunnel permanently dropped at ${tunnel.droppedAt} after ${tunnel.reissueAttempts} reissue attempt(s) — restart the MCP server (npx @ait-co/devtools devtools-mcp)`
|
|
3144
|
+
};
|
|
3160
3145
|
if (!tunnel.up) if (!isRelayEnv(env)) {
|
|
3161
3146
|
if (pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
|
|
3162
3147
|
tool: "wait_for_page",
|
|
@@ -3187,7 +3172,7 @@ function computeNextRecommendedAction(tunnel, pages, env) {
|
|
|
3187
3172
|
* - Lock file data contains only pid + startedAt + wssUrl — no secrets.
|
|
3188
3173
|
*/
|
|
3189
3174
|
async function getDiagnostics(input) {
|
|
3190
|
-
const { tunnel, connection, env, envReason, collector, readLock: readLockFn, recentErrorsLimit = 10, getMcpVersion = readMcpSdkVersion } = input;
|
|
3175
|
+
const { tunnel, connection, env, envReason, collector, readLock: readLockFn, recentErrorsLimit = 10, getMcpVersion = readMcpSdkVersion, checkParentAlive = () => isPidAlive(process.ppid) } = input;
|
|
3191
3176
|
const [mcpVersion, devtoolsVersion] = await Promise.all([getMcpVersion(), Promise.resolve(readDevtoolsVersion())]);
|
|
3192
3177
|
const lockData = readLockFn();
|
|
3193
3178
|
const serverLockHolder = lockData ? {
|
|
@@ -3199,7 +3184,9 @@ async function getDiagnostics(input) {
|
|
|
3199
3184
|
up: tunnel.up,
|
|
3200
3185
|
wssUrl: tunnel.wssUrl,
|
|
3201
3186
|
pid: lockData?.pid ?? null,
|
|
3202
|
-
startedAt: lockData?.startedAt ?? null
|
|
3187
|
+
startedAt: lockData?.startedAt ?? null,
|
|
3188
|
+
droppedAt: tunnel.droppedAt ?? null,
|
|
3189
|
+
reissueAttempts: tunnel.reissueAttempts ?? 0
|
|
3203
3190
|
};
|
|
3204
3191
|
let pages = null;
|
|
3205
3192
|
if (connection !== void 0) try {
|
|
@@ -3223,6 +3210,11 @@ async function getDiagnostics(input) {
|
|
|
3223
3210
|
liveGuardActive: isLiveRelayEnv(env)
|
|
3224
3211
|
},
|
|
3225
3212
|
serverLockHolder,
|
|
3213
|
+
process: {
|
|
3214
|
+
pid: process.pid,
|
|
3215
|
+
ppid: process.ppid,
|
|
3216
|
+
parentAlive: checkParentAlive()
|
|
3217
|
+
},
|
|
3226
3218
|
nextRecommendedAction
|
|
3227
3219
|
};
|
|
3228
3220
|
}
|
|
@@ -3547,10 +3539,16 @@ function extractDeploymentId(schemeUrl) {
|
|
|
3547
3539
|
return null;
|
|
3548
3540
|
}
|
|
3549
3541
|
}
|
|
3542
|
+
/** Returns `true` when the mode routes to a relay connection. */
|
|
3543
|
+
function isRelayMode(mode) {
|
|
3544
|
+
return mode === "relay-dev" || mode === "relay-live";
|
|
3545
|
+
}
|
|
3550
3546
|
/**
|
|
3551
3547
|
* Waits for the first target matching `filterFn` to attach, using the
|
|
3552
|
-
* event-driven `waitForFirstTarget()`
|
|
3553
|
-
*
|
|
3548
|
+
* event-driven `waitForFirstTarget()` when the connection supports it
|
|
3549
|
+
* (interface-optional member, present on `ChiiCdpConnection`), or falling
|
|
3550
|
+
* back to a polling loop for connections that don't implement it (test fakes,
|
|
3551
|
+
* `LocalCdpConnection`).
|
|
3554
3552
|
*
|
|
3555
3553
|
* This eliminates the polling-only race that previously caused `wait_for_attach`
|
|
3556
3554
|
* to resolve before the relay had observed the first inbound CDP message from
|
|
@@ -3559,10 +3557,10 @@ function extractDeploymentId(schemeUrl) {
|
|
|
3559
3557
|
* @param connection - The CDP connection (production or fake).
|
|
3560
3558
|
* @param filterFn - Resolves when this predicate is satisfied.
|
|
3561
3559
|
* @param timeoutMs - Maximum wait time in ms.
|
|
3562
|
-
* @param pollIntervalMs - Fallback poll interval for
|
|
3560
|
+
* @param pollIntervalMs - Fallback poll interval for connections without waitForFirstTarget.
|
|
3563
3561
|
*/
|
|
3564
3562
|
function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs = 1e3) {
|
|
3565
|
-
if (connection
|
|
3563
|
+
if (connection.waitForFirstTarget) return connection.waitForFirstTarget(filterFn, timeoutMs, pollIntervalMs);
|
|
3566
3564
|
return new Promise((resolve, reject) => {
|
|
3567
3565
|
const deadline = Date.now() + timeoutMs;
|
|
3568
3566
|
let settled = false;
|
|
@@ -3599,23 +3597,19 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
|
|
|
3599
3597
|
* naturally via `enableDomains`). The tier only controls visibility.
|
|
3600
3598
|
*/
|
|
3601
3599
|
function createDebugServer(deps) {
|
|
3602
|
-
const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep,
|
|
3603
|
-
const
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
}));
|
|
3607
|
-
const resolveEnvironmentReason = getEnvReasonDep ?? (() => getEnvironmentReason({
|
|
3608
|
-
connection,
|
|
3609
|
-
defaultEnv
|
|
3610
|
-
}));
|
|
3600
|
+
const { connection, router: routerDep, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, totpSecret } = deps;
|
|
3601
|
+
const router = routerDep ?? makeSingleConnectionRouter(connection);
|
|
3602
|
+
const resolveEnvironment = getEnvDep ?? (() => deriveEnvironment(router.active.kind, getLiveIntent()));
|
|
3603
|
+
const resolveEnvironmentReason = getEnvReasonDep ?? (() => `derived:kind=${router.active.kind},liveIntent=${getLiveIntent()}`);
|
|
3611
3604
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
3612
3605
|
const server = new Server({
|
|
3613
3606
|
name: "ait-debug",
|
|
3614
|
-
version: "0.1.
|
|
3607
|
+
version: "0.1.52"
|
|
3615
3608
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
3616
3609
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
3610
|
+
const conn = router.active;
|
|
3617
3611
|
const env = resolveEnvironment();
|
|
3618
|
-
const attached =
|
|
3612
|
+
const attached = conn.listTargets().length > 0;
|
|
3619
3613
|
const envFiltered = filterToolsByEnvironment(DEBUG_TOOL_DEFINITIONS, env);
|
|
3620
3614
|
return { tools: attached ? envFiltered.map((tool) => ({ ...tool })) : envFiltered.filter((tool) => BOOTSTRAP_TOOL_NAMES.has(tool.name)).map((tool) => ({ ...tool })) };
|
|
3621
3615
|
});
|
|
@@ -3628,10 +3622,22 @@ function createDebugServer(deps) {
|
|
|
3628
3622
|
}],
|
|
3629
3623
|
isError: true
|
|
3630
3624
|
};
|
|
3625
|
+
const conn = router.active;
|
|
3626
|
+
if (name === "start_debug") {
|
|
3627
|
+
const rawMode = request.params.arguments?.mode;
|
|
3628
|
+
const mode = normalizeStartDebugMode(rawMode);
|
|
3629
|
+
if (mode === null) return mcpError("start_debug: mode가 올바르지 않습니다. 'local-browser-dev' | 'local-browser-cdp' | 'relay-dev' | 'relay-live' 중 하나를 전달하세요.");
|
|
3630
|
+
const confirm = request.params.arguments?.confirm === true;
|
|
3631
|
+
try {
|
|
3632
|
+
return jsonResult$1(await router.switchMode(mode, confirm));
|
|
3633
|
+
} catch (err) {
|
|
3634
|
+
return errorResult(err, name);
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3631
3637
|
const env = resolveEnvironment();
|
|
3638
|
+
const envReason = resolveEnvironmentReason();
|
|
3632
3639
|
if (!isToolAvailableIn(name, env)) {
|
|
3633
3640
|
const requiredEnv = getToolAvailability(name) ?? "unknown";
|
|
3634
|
-
const envReason = resolveEnvironmentReason();
|
|
3635
3641
|
logWarn("tool.error", {
|
|
3636
3642
|
tool: name,
|
|
3637
3643
|
errorKind: "tier-filter",
|
|
@@ -3642,7 +3648,7 @@ function createDebugServer(deps) {
|
|
|
3642
3648
|
return tierRejectionError(name, requiredEnv, env, envReason);
|
|
3643
3649
|
}
|
|
3644
3650
|
if (isAitToolName(name)) try {
|
|
3645
|
-
await
|
|
3651
|
+
await conn.enableDomains();
|
|
3646
3652
|
switch (name) {
|
|
3647
3653
|
case "AIT.getSdkCallHistory": return jsonResult$1(await getSdkCallHistory(aitSource));
|
|
3648
3654
|
case "AIT.getMockState": return jsonResult$1(await getMockState(aitSource));
|
|
@@ -3655,17 +3661,15 @@ function createDebugServer(deps) {
|
|
|
3655
3661
|
if (name === "get_diagnostics") try {
|
|
3656
3662
|
const rawLimit = request.params.arguments?.recent_errors_limit;
|
|
3657
3663
|
const recentErrorsLimit = typeof rawLimit === "number" && rawLimit > 0 ? rawLimit : 10;
|
|
3658
|
-
|
|
3664
|
+
return envelopeResult$1(await getDiagnostics({
|
|
3659
3665
|
tunnel: getTunnelStatus(),
|
|
3660
|
-
connection,
|
|
3661
|
-
env
|
|
3662
|
-
envReason
|
|
3666
|
+
connection: conn,
|
|
3667
|
+
env,
|
|
3668
|
+
envReason,
|
|
3663
3669
|
collector,
|
|
3664
3670
|
readLock: readServerLock,
|
|
3665
3671
|
recentErrorsLimit
|
|
3666
|
-
});
|
|
3667
|
-
const attached = connection.listTargets().length > 0;
|
|
3668
|
-
return envelopeResult$1(result, name, resolveEnvironment(), attached);
|
|
3672
|
+
}), name, env, conn.listTargets().length > 0);
|
|
3669
3673
|
} catch (err) {
|
|
3670
3674
|
return errorResult(err, name);
|
|
3671
3675
|
}
|
|
@@ -3710,9 +3714,9 @@ function createDebugServer(deps) {
|
|
|
3710
3714
|
}] };
|
|
3711
3715
|
let attachedPagesHl = [];
|
|
3712
3716
|
try {
|
|
3713
|
-
attachedPagesHl = await waitForAttachWithEvents(
|
|
3717
|
+
attachedPagesHl = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
|
|
3714
3718
|
} catch {
|
|
3715
|
-
attachedPagesHl =
|
|
3719
|
+
attachedPagesHl = conn.listTargets();
|
|
3716
3720
|
return {
|
|
3717
3721
|
content: [{
|
|
3718
3722
|
type: "text",
|
|
@@ -3721,7 +3725,7 @@ function createDebugServer(deps) {
|
|
|
3721
3725
|
isError: true
|
|
3722
3726
|
};
|
|
3723
3727
|
}
|
|
3724
|
-
const pagesResultHl = listPages(
|
|
3728
|
+
const pagesResultHl = listPages(conn, getTunnelStatus());
|
|
3725
3729
|
return { content: [{
|
|
3726
3730
|
type: "text",
|
|
3727
3731
|
text: `${headlessText}\n\n${JSON.stringify(pagesResultHl, null, 2)}`
|
|
@@ -3747,9 +3751,9 @@ function createDebugServer(deps) {
|
|
|
3747
3751
|
}] };
|
|
3748
3752
|
let attachedPages = [];
|
|
3749
3753
|
try {
|
|
3750
|
-
attachedPages = await waitForAttachWithEvents(
|
|
3754
|
+
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
|
|
3751
3755
|
} catch {
|
|
3752
|
-
attachedPages =
|
|
3756
|
+
attachedPages = conn.listTargets();
|
|
3753
3757
|
return {
|
|
3754
3758
|
content: [{
|
|
3755
3759
|
type: "text",
|
|
@@ -3758,7 +3762,7 @@ function createDebugServer(deps) {
|
|
|
3758
3762
|
isError: true
|
|
3759
3763
|
};
|
|
3760
3764
|
}
|
|
3761
|
-
const pagesResult = listPages(
|
|
3765
|
+
const pagesResult = listPages(conn, getTunnelStatus());
|
|
3762
3766
|
return { content: [{
|
|
3763
3767
|
type: "text",
|
|
3764
3768
|
text: `${shortText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
@@ -3787,9 +3791,9 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3787
3791
|
}] };
|
|
3788
3792
|
let attachedPagesFb = [];
|
|
3789
3793
|
try {
|
|
3790
|
-
attachedPagesFb = await waitForAttachWithEvents(
|
|
3794
|
+
attachedPagesFb = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
|
|
3791
3795
|
} catch {
|
|
3792
|
-
attachedPagesFb =
|
|
3796
|
+
attachedPagesFb = conn.listTargets();
|
|
3793
3797
|
return {
|
|
3794
3798
|
content: [{
|
|
3795
3799
|
type: "text",
|
|
@@ -3798,7 +3802,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3798
3802
|
isError: true
|
|
3799
3803
|
};
|
|
3800
3804
|
}
|
|
3801
|
-
const pagesResultFb = listPages(
|
|
3805
|
+
const pagesResultFb = listPages(conn, getTunnelStatus());
|
|
3802
3806
|
return { content: [{
|
|
3803
3807
|
type: "text",
|
|
3804
3808
|
text: `${baseText}\n\n${JSON.stringify(pagesResultFb, null, 2)}`
|
|
@@ -3816,9 +3820,9 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3816
3820
|
}] };
|
|
3817
3821
|
let attachedPages = [];
|
|
3818
3822
|
try {
|
|
3819
|
-
attachedPages = await waitForAttachWithEvents(
|
|
3823
|
+
attachedPages = await waitForAttachWithEvents(conn, isMatchingPage, waitForAttachTimeoutMs);
|
|
3820
3824
|
} catch {
|
|
3821
|
-
attachedPages =
|
|
3825
|
+
attachedPages = conn.listTargets();
|
|
3822
3826
|
return {
|
|
3823
3827
|
content: [{
|
|
3824
3828
|
type: "text",
|
|
@@ -3827,7 +3831,7 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3827
3831
|
isError: true
|
|
3828
3832
|
};
|
|
3829
3833
|
}
|
|
3830
|
-
const pagesResult = listPages(
|
|
3834
|
+
const pagesResult = listPages(conn, getTunnelStatus());
|
|
3831
3835
|
return { content: [{
|
|
3832
3836
|
type: "text",
|
|
3833
3837
|
text: `${baseText}\n\n${JSON.stringify(pagesResult, null, 2)}`
|
|
@@ -3837,65 +3841,55 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3837
3841
|
}
|
|
3838
3842
|
}
|
|
3839
3843
|
try {
|
|
3840
|
-
await
|
|
3844
|
+
await conn.enableDomains();
|
|
3841
3845
|
} catch (err) {
|
|
3842
3846
|
if (name === "list_pages") {
|
|
3843
|
-
|
|
3844
|
-
await
|
|
3847
|
+
try {
|
|
3848
|
+
await conn.refreshTargets?.();
|
|
3845
3849
|
} catch {}
|
|
3846
|
-
|
|
3847
|
-
const attached = connection.listTargets().length > 0;
|
|
3848
|
-
return envelopeResult$1(pagesData, name, resolveEnvironment(), attached);
|
|
3850
|
+
return envelopeResult$1(listPages(conn, getTunnelStatus()), name, env, conn.listTargets().length > 0);
|
|
3849
3851
|
}
|
|
3850
3852
|
return classifyEnableDomainError(err, name);
|
|
3851
3853
|
}
|
|
3852
3854
|
try {
|
|
3853
3855
|
switch (name) {
|
|
3854
|
-
case "list_console_messages": return jsonResult$1(listConsoleMessages(
|
|
3856
|
+
case "list_console_messages": return jsonResult$1(listConsoleMessages(conn));
|
|
3855
3857
|
case "list_exceptions": {
|
|
3856
3858
|
const rawLimit = request.params.arguments?.limit;
|
|
3857
|
-
return jsonResult$1({ exceptions: listExceptions(
|
|
3859
|
+
return jsonResult$1({ exceptions: listExceptions(conn, typeof rawLimit === "number" && rawLimit > 0 ? rawLimit : 50) });
|
|
3858
3860
|
}
|
|
3859
|
-
case "list_network_requests": return jsonResult$1(listNetworkRequests(
|
|
3860
|
-
case "list_pages":
|
|
3861
|
-
|
|
3862
|
-
await
|
|
3861
|
+
case "list_network_requests": return jsonResult$1(listNetworkRequests(conn));
|
|
3862
|
+
case "list_pages":
|
|
3863
|
+
try {
|
|
3864
|
+
await conn.refreshTargets?.();
|
|
3863
3865
|
} catch {}
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
}
|
|
3868
|
-
case "get_dom_document": return jsonResult$1(await getDomDocument(connection));
|
|
3869
|
-
case "take_snapshot": return jsonResult$1(await takeSnapshot(connection));
|
|
3866
|
+
return envelopeResult$1(listPages(conn, getTunnelStatus()), name, env, conn.listTargets().length > 0);
|
|
3867
|
+
case "get_dom_document": return jsonResult$1(await getDomDocument(conn));
|
|
3868
|
+
case "take_snapshot": return jsonResult$1(await takeSnapshot(conn));
|
|
3870
3869
|
case "take_screenshot": {
|
|
3871
|
-
const shot = await takeScreenshot(
|
|
3870
|
+
const shot = await takeScreenshot(conn);
|
|
3872
3871
|
return { content: [{
|
|
3873
3872
|
type: "image",
|
|
3874
3873
|
data: shot.data,
|
|
3875
3874
|
mimeType: shot.mimeType
|
|
3876
3875
|
}] };
|
|
3877
3876
|
}
|
|
3878
|
-
case "measure_safe_area":
|
|
3879
|
-
const safeAreaData = await measureSafeArea(connection, resolveEnvironment());
|
|
3880
|
-
const safeAreaAttached = connection.listTargets().length > 0;
|
|
3881
|
-
return envelopeResult$1(safeAreaData, name, resolveEnvironment(), safeAreaAttached);
|
|
3882
|
-
}
|
|
3877
|
+
case "measure_safe_area": return envelopeResult$1(await measureSafeArea(conn, env), name, env, conn.listTargets().length > 0);
|
|
3883
3878
|
case "evaluate": {
|
|
3884
3879
|
const expression = request.params.arguments?.expression;
|
|
3885
3880
|
if (typeof expression !== "string" || expression === "") return mcpError("evaluate: expression 인자가 비어 있습니다. 평가할 JavaScript 표현식을 전달하세요.");
|
|
3886
|
-
if (
|
|
3887
|
-
return jsonResult$1(await evaluate(
|
|
3881
|
+
if (conn.kind === "relay" && getLiveIntent() && request.params.arguments?.confirm !== true) return liveGuardError("evaluate");
|
|
3882
|
+
return jsonResult$1(await evaluate(conn, expression));
|
|
3888
3883
|
}
|
|
3889
3884
|
case "call_sdk": {
|
|
3890
3885
|
const sdkName = request.params.arguments?.name;
|
|
3891
3886
|
if (typeof sdkName !== "string" || sdkName === "") return mcpError("call_sdk: name 인자가 비어 있습니다. 호출할 SDK 메서드 이름을 전달하세요.");
|
|
3892
3887
|
const rawArgs = request.params.arguments?.args;
|
|
3893
3888
|
const sdkArgs = Array.isArray(rawArgs) ? rawArgs : [];
|
|
3894
|
-
if (
|
|
3895
|
-
const sdkResult = await callSdk(
|
|
3889
|
+
if (conn.kind === "relay" && getLiveIntent() && request.params.arguments?.confirm !== true) return liveGuardError("call_sdk");
|
|
3890
|
+
const sdkResult = await callSdk(conn, sdkName, sdkArgs);
|
|
3896
3891
|
if (!sdkResult.ok && typeof sdkResult.error === "string" && sdkResult.error.startsWith("sdk-absent:")) return sdkAbsentError("call_sdk");
|
|
3897
|
-
|
|
3898
|
-
return envelopeResult$1(sdkResult, name, resolveEnvironment(), callSdkAttached);
|
|
3892
|
+
return envelopeResult$1(sdkResult, name, env, conn.listTargets().length > 0);
|
|
3899
3893
|
}
|
|
3900
3894
|
default: return unknownTool(name);
|
|
3901
3895
|
}
|
|
@@ -3905,6 +3899,46 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3905
3899
|
});
|
|
3906
3900
|
return server;
|
|
3907
3901
|
}
|
|
3902
|
+
/**
|
|
3903
|
+
* Normalizes a raw `start_debug` `mode` argument to a `StartDebugMode`, or
|
|
3904
|
+
* `null` when the value is not one of the four accepted modes.
|
|
3905
|
+
*/
|
|
3906
|
+
function normalizeStartDebugMode(raw) {
|
|
3907
|
+
if (raw === "local-browser-dev") return "local-browser-dev";
|
|
3908
|
+
if (raw === "local-browser-cdp") return "local-browser-cdp";
|
|
3909
|
+
if (raw === "relay-dev") return "relay-dev";
|
|
3910
|
+
if (raw === "relay-live") return "relay-live";
|
|
3911
|
+
return null;
|
|
3912
|
+
}
|
|
3913
|
+
/**
|
|
3914
|
+
* Builds a trivial `ConnectionRouter` pinned to a single connection (issue
|
|
3915
|
+
* #348). Used by `createDebugServer` when no real dual router is injected —
|
|
3916
|
+
* every existing single-connection test and the `local`-only / `relay`-only
|
|
3917
|
+
* boot path. `switchMode` here cannot lazily boot another family, so it only
|
|
3918
|
+
* honors a request that matches the connection's own kind (and arms/disarms
|
|
3919
|
+
* `liveIntent` accordingly for relay-live); any cross-family request is
|
|
3920
|
+
* rejected with a clear "dynamic switch unavailable in this session" error.
|
|
3921
|
+
*/
|
|
3922
|
+
function makeSingleConnectionRouter(connection) {
|
|
3923
|
+
return {
|
|
3924
|
+
get active() {
|
|
3925
|
+
return connection;
|
|
3926
|
+
},
|
|
3927
|
+
switchMode(mode, confirm) {
|
|
3928
|
+
if (isRelayMode(mode) !== (connection.kind === "relay")) return Promise.reject(/* @__PURE__ */ new Error(`start_debug: 이 세션은 단일 ${connection.kind} 연결만 보유합니다 — '${mode}'로 동적 전환할 수 없습니다 (dual-connection 데몬에서만 지원). MCP 서버를 원하는 모드로 재시작하세요.`));
|
|
3929
|
+
if (mode === "relay-live" && !confirm) return Promise.reject(/* @__PURE__ */ new Error("start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — 실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요."));
|
|
3930
|
+
setLiveIntent(mode === "relay-live");
|
|
3931
|
+
const environment = deriveEnvironment(connection.kind, getLiveIntent());
|
|
3932
|
+
return Promise.resolve({
|
|
3933
|
+
mode,
|
|
3934
|
+
environment,
|
|
3935
|
+
kind: connection.kind,
|
|
3936
|
+
liveGuardActive: connection.kind === "relay" && getLiveIntent(),
|
|
3937
|
+
nextStep: connection.kind === "relay" ? "build_attach_url로 attach QR을 생성하세요." : "list_pages로 로컬 페이지 attach를 확인하세요."
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3940
|
+
};
|
|
3941
|
+
}
|
|
3908
3942
|
function jsonResult$1(value) {
|
|
3909
3943
|
return { content: [{
|
|
3910
3944
|
type: "text",
|
|
@@ -3991,6 +4025,45 @@ function startAttachWatcher(connection, server, intervalMs = 1e3, onFirstAttach)
|
|
|
3991
4025
|
} };
|
|
3992
4026
|
}
|
|
3993
4027
|
/**
|
|
4028
|
+
* Starts a periodic watcher that detects when the parent process (e.g. Claude
|
|
4029
|
+
* Code) has died without sending SIGTERM/SIGHUP, and calls `onOrphaned` so the
|
|
4030
|
+
* daemon can self-terminate rather than running as a zombie.
|
|
4031
|
+
*
|
|
4032
|
+
* Mirrors the `startAttachWatcher` pattern: `setInterval`-based, returns
|
|
4033
|
+
* `{ stop(): void }`, injectable deps for testability.
|
|
4034
|
+
*
|
|
4035
|
+
* @param onOrphaned - Called once when the parent is gone.
|
|
4036
|
+
* @param opts.intervalMs - Poll interval in milliseconds (default 5 000).
|
|
4037
|
+
* @param opts.initialPpid - Parent PID to watch (default `process.ppid`).
|
|
4038
|
+
* @param opts.isAlive - Predicate to test if a PID is running (default `isPidAlive`).
|
|
4039
|
+
* @param opts.getPpid - Supplier of current ppid (default `() => process.ppid`).
|
|
4040
|
+
* Detects ppid changes as well as death.
|
|
4041
|
+
* @param opts.log - Logger (default `process.stderr.write`).
|
|
4042
|
+
*
|
|
4043
|
+
* @returns `stop` — call during shutdown to clear the interval.
|
|
4044
|
+
*/
|
|
4045
|
+
function startParentWatcher(onOrphaned, opts) {
|
|
4046
|
+
const { intervalMs = 5e3, initialPpid = process.ppid, isAlive = isPidAlive, getPpid = () => process.ppid, log = (msg) => process.stderr.write(msg) } = opts ?? {};
|
|
4047
|
+
if (initialPpid <= 1) {
|
|
4048
|
+
log("[ait-debug] parent-pid watcher: no parent to watch (ppid<=1), skipping\n");
|
|
4049
|
+
return { stop() {} };
|
|
4050
|
+
}
|
|
4051
|
+
let fired = false;
|
|
4052
|
+
const handle = setInterval(() => {
|
|
4053
|
+
if (fired) return;
|
|
4054
|
+
const currentPpid = getPpid();
|
|
4055
|
+
if (currentPpid !== initialPpid || !isAlive(initialPpid)) {
|
|
4056
|
+
fired = true;
|
|
4057
|
+
clearInterval(handle);
|
|
4058
|
+
log(`[ait-debug] parent-pid watcher: parent PID ${initialPpid} is gone (currentPpid=${currentPpid}) — shutting down\n`);
|
|
4059
|
+
onOrphaned();
|
|
4060
|
+
}
|
|
4061
|
+
}, intervalMs);
|
|
4062
|
+
return { stop() {
|
|
4063
|
+
clearInterval(handle);
|
|
4064
|
+
} };
|
|
4065
|
+
}
|
|
4066
|
+
/**
|
|
3994
4067
|
* Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a
|
|
3995
4068
|
* `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.
|
|
3996
4069
|
*
|
|
@@ -4014,21 +4087,85 @@ function buildRelayVerifyAuth() {
|
|
|
4014
4087
|
};
|
|
4015
4088
|
}
|
|
4016
4089
|
/**
|
|
4017
|
-
*
|
|
4018
|
-
*
|
|
4019
|
-
*
|
|
4020
|
-
*
|
|
4021
|
-
*
|
|
4022
|
-
*
|
|
4090
|
+
* Factory that constructs a `ChiiCdpConnection` for the given relay base URL.
|
|
4091
|
+
*
|
|
4092
|
+
* Introduced as a named seam so PR-2 (dual-connection, #348) can defer
|
|
4093
|
+
* construction to first-activation time by moving or replacing this call —
|
|
4094
|
+
* without changing the current eager construction order at startup.
|
|
4095
|
+
*
|
|
4096
|
+
* The relay base URL is only available after `startChiiRelay()` resolves, so
|
|
4097
|
+
* the factory is called right after that point (same as before this refactor).
|
|
4023
4098
|
*/
|
|
4024
|
-
|
|
4025
|
-
|
|
4099
|
+
function createRelayConnection(relayBaseUrl) {
|
|
4100
|
+
return new ChiiCdpConnection({ relayBaseUrl });
|
|
4101
|
+
}
|
|
4102
|
+
/**
|
|
4103
|
+
* AIT source that always forwards over the *currently active* connection
|
|
4104
|
+
* (issue #348). The single-connection `ChiiAitSource` binds one sender at
|
|
4105
|
+
* construction; in the dual-connection daemon the AIT.* domain must follow the
|
|
4106
|
+
* active connection across `start_debug` swaps, so this indirection reads
|
|
4107
|
+
* `getActive()` on every call.
|
|
4108
|
+
*
|
|
4109
|
+
* Both `ChiiCdpConnection` and `LocalCdpConnection` expose `sendCommand`, so
|
|
4110
|
+
* the active connection is a valid `AitCommandSender`.
|
|
4111
|
+
*/
|
|
4112
|
+
var RoutingAitSource = class extends ChiiAitSource {
|
|
4113
|
+
constructor(getActive) {
|
|
4114
|
+
super({ sendCommand: (method, params) => getActive().sendCommand(method, params) });
|
|
4115
|
+
}
|
|
4116
|
+
};
|
|
4117
|
+
/**
|
|
4118
|
+
* Boots the local-browser family (issues #348, #356). Launches a Chromium with
|
|
4119
|
+
* `--remote-debugging-port` and returns a `LocalCdpConnection` attached to it,
|
|
4120
|
+
* plus a `stop()` that kills both.
|
|
4121
|
+
*
|
|
4122
|
+
* Used two ways:
|
|
4123
|
+
* - `runDebugServer` (relay-eager): the dual router's lazy callback, booted at
|
|
4124
|
+
* most once on the first `start_debug({ mode: 'local-*' })`.
|
|
4125
|
+
* - `runLocalDebugServer` (local-eager, #356): the eager family booted at
|
|
4126
|
+
* startup.
|
|
4127
|
+
*/
|
|
4128
|
+
async function bootLocalFamily() {
|
|
4129
|
+
const chromium = await launchChromium({
|
|
4130
|
+
port: 0,
|
|
4131
|
+
devUrl: process.env.AIT_DEVTOOLS_URL ?? "http://localhost:5173"
|
|
4132
|
+
});
|
|
4133
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
4134
|
+
const connection = new LocalCdpConnection({ devtoolsHttpUrl: chromium.devtoolsUrl });
|
|
4135
|
+
return {
|
|
4136
|
+
connection,
|
|
4137
|
+
stop() {
|
|
4138
|
+
connection.close();
|
|
4139
|
+
chromium.stop();
|
|
4140
|
+
}
|
|
4141
|
+
};
|
|
4142
|
+
}
|
|
4143
|
+
/**
|
|
4144
|
+
* Boots the relay family (issues #348, #356): starts the Chii relay on an
|
|
4145
|
+
* OS-assigned port (with optional TOTP gate), opens a cloudflared quick tunnel
|
|
4146
|
+
* to the relay's confirmed port in the background, prints the attach banner,
|
|
4147
|
+
* and arms the tunnel health probe. Returns a {@link BootedFamily} whose
|
|
4148
|
+
* `getTunnelStatus()` reflects the live tunnel (it flips up once the background
|
|
4149
|
+
* tunnel resolves and follows reissues).
|
|
4150
|
+
*
|
|
4151
|
+
* Used two ways (symmetry with {@link bootLocalFamily}):
|
|
4152
|
+
* - `runDebugServer` (relay-eager): booted at startup.
|
|
4153
|
+
* - `runLocalDebugServer` (local-eager, #356): the dual router's lazy
|
|
4154
|
+
* callback, booted at most once on the first `start_debug({ mode: 'relay-*' })`.
|
|
4155
|
+
*
|
|
4156
|
+
* The relay base URL is only known after `startChiiRelay()` resolves, so the
|
|
4157
|
+
* `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside
|
|
4158
|
+
* this function, after the relay port is confirmed.
|
|
4159
|
+
*
|
|
4160
|
+
* SECRET-HANDLING: the TOTP secret rides only inside `verifyAuth`; the wssUrl
|
|
4161
|
+
* (relay host) is never logged here directly.
|
|
4162
|
+
*/
|
|
4163
|
+
async function bootRelayFamily(options = {}) {
|
|
4026
4164
|
const relayPort = options.relayPort ?? 0;
|
|
4027
|
-
const
|
|
4028
|
-
const totpEnabled = verifyAuth !== void 0;
|
|
4165
|
+
const totpEnabled = options.verifyAuth !== void 0;
|
|
4029
4166
|
const relay = await startChiiRelay({
|
|
4030
4167
|
port: relayPort,
|
|
4031
|
-
verifyAuth
|
|
4168
|
+
verifyAuth: options.verifyAuth
|
|
4032
4169
|
});
|
|
4033
4170
|
logInfo("server.start", {
|
|
4034
4171
|
port: relay.port,
|
|
@@ -4036,18 +4173,18 @@ async function runDebugServer(options = {}) {
|
|
|
4036
4173
|
});
|
|
4037
4174
|
let tunnel = null;
|
|
4038
4175
|
let tunnelStatus = makeTunnelStatus(false, null);
|
|
4039
|
-
generateAttachToken();
|
|
4040
4176
|
let tunnelProbe = null;
|
|
4177
|
+
generateAttachToken();
|
|
4041
4178
|
startQuickTunnel(relay.port).then((t) => {
|
|
4042
4179
|
tunnel = t;
|
|
4043
4180
|
tunnelStatus = makeTunnelStatus(true, t.wssUrl);
|
|
4044
|
-
|
|
4181
|
+
options.onWssUrl?.(t.wssUrl);
|
|
4045
4182
|
logInfo("tunnel.up", { totpEnabled });
|
|
4046
4183
|
tunnelProbe = startTunnelHealthProbe(t, relay.port, {
|
|
4047
4184
|
onReissue: (newTunnel) => {
|
|
4048
4185
|
tunnel = newTunnel;
|
|
4049
4186
|
tunnelStatus = makeTunnelStatus(true, newTunnel.wssUrl, null, 0);
|
|
4050
|
-
|
|
4187
|
+
options.onWssUrl?.(newTunnel.wssUrl);
|
|
4051
4188
|
printAttachBanner({
|
|
4052
4189
|
wssUrl: newTunnel.wssUrl,
|
|
4053
4190
|
totpEnabled
|
|
@@ -4070,38 +4207,179 @@ async function runDebugServer(options = {}) {
|
|
|
4070
4207
|
}, (err) => {
|
|
4071
4208
|
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.` });
|
|
4072
4209
|
});
|
|
4073
|
-
const connection =
|
|
4074
|
-
|
|
4210
|
+
const connection = createRelayConnection(relay.baseUrl);
|
|
4211
|
+
return {
|
|
4212
|
+
connection,
|
|
4213
|
+
getTunnelStatus: () => tunnelStatus,
|
|
4214
|
+
stop() {
|
|
4215
|
+
tunnelProbe?.stop();
|
|
4216
|
+
tunnel?.stop();
|
|
4217
|
+
connection.close();
|
|
4218
|
+
relay.close();
|
|
4219
|
+
}
|
|
4220
|
+
};
|
|
4221
|
+
}
|
|
4222
|
+
/**
|
|
4223
|
+
* Production `ConnectionRouter` (issues #348, #356 — DUAL-CONNECTION-COEXIST).
|
|
4224
|
+
*
|
|
4225
|
+
* Holds two families — one booted eagerly at startup, the other lazily on the
|
|
4226
|
+
* first cross-family switch — an `active` pointer, and the single attach watcher
|
|
4227
|
+
* armed on the active connection. The router is **direction-neutral** (#356):
|
|
4228
|
+
* either family kind can be the eager one, so a `--target=local` session can
|
|
4229
|
+
* hot-switch into relay (and vice versa) without restarting the MCP server.
|
|
4230
|
+
*
|
|
4231
|
+
* `switchMode`:
|
|
4232
|
+
* 1. rejects re-entrant swaps (`swapInFlight`) and an unconfirmed relay-live;
|
|
4233
|
+
* 2. routes by the requested mode's family kind: same kind as `eager` → reuse
|
|
4234
|
+
* eager; different kind → lazily boot (once) and keep warm;
|
|
4235
|
+
* 3. flips `active` (the MCP `Server` never re-handshakes — it reads through
|
|
4236
|
+
* `active` per request);
|
|
4237
|
+
* 4. sets `liveIntent` (true only for relay-live);
|
|
4238
|
+
* 5. stops the old attach watcher and re-arms one on the new connection
|
|
4239
|
+
* (the watcher self-clears, so re-arm is mandatory);
|
|
4240
|
+
* 6. emits `tools/list_changed`.
|
|
4241
|
+
*
|
|
4242
|
+
* Inactive infra is left WARM — teardown happens only at process exit (the
|
|
4243
|
+
* unified shutdown in the run functions), which is what keeps a phone attach
|
|
4244
|
+
* alive across a local→relay→local round trip.
|
|
4245
|
+
*/
|
|
4246
|
+
var DualConnectionRouter = class {
|
|
4247
|
+
deps;
|
|
4248
|
+
/** The opposite-kind family, booted lazily on the first cross-family switch. */
|
|
4249
|
+
lazyFamily = null;
|
|
4250
|
+
activeFamily;
|
|
4251
|
+
server = null;
|
|
4252
|
+
attachWatcher = null;
|
|
4253
|
+
swapInFlight = false;
|
|
4254
|
+
constructor(deps) {
|
|
4255
|
+
this.deps = deps;
|
|
4256
|
+
this.activeFamily = deps.eager;
|
|
4257
|
+
}
|
|
4258
|
+
get active() {
|
|
4259
|
+
return this.activeFamily.connection;
|
|
4260
|
+
}
|
|
4261
|
+
/** Every booted family (for unified shutdown). */
|
|
4262
|
+
bootedFamilies() {
|
|
4263
|
+
return this.lazyFamily ? [this.deps.eager, this.lazyFamily] : [this.deps.eager];
|
|
4264
|
+
}
|
|
4265
|
+
/**
|
|
4266
|
+
* Live tunnel status of the relay family, regardless of whether relay is the
|
|
4267
|
+
* eager or lazy family (#356). Returns "down" until the relay family has been
|
|
4268
|
+
* booted (local-eager sessions before the first relay switch) — which is the
|
|
4269
|
+
* correct signal for `build_attach_url` (no tunnel exists yet).
|
|
4270
|
+
*/
|
|
4271
|
+
relayTunnelStatus() {
|
|
4272
|
+
for (const family of this.bootedFamilies()) if (family.getTunnelStatus) return family.getTunnelStatus();
|
|
4273
|
+
return {
|
|
4274
|
+
up: false,
|
|
4275
|
+
wssUrl: null
|
|
4276
|
+
};
|
|
4277
|
+
}
|
|
4278
|
+
/**
|
|
4279
|
+
* Binds the MCP `Server` and arms the initial attach watcher on the active
|
|
4280
|
+
* connection. Called once after `createDebugServer` + `connect`.
|
|
4281
|
+
*/
|
|
4282
|
+
start(server) {
|
|
4283
|
+
this.server = server;
|
|
4284
|
+
this.armWatcher();
|
|
4285
|
+
}
|
|
4286
|
+
/** Stops the current attach watcher (for shutdown). */
|
|
4287
|
+
stopWatcher() {
|
|
4288
|
+
this.attachWatcher?.stop();
|
|
4289
|
+
this.attachWatcher = null;
|
|
4290
|
+
}
|
|
4291
|
+
/** Arms a fresh attach watcher on the current active connection. */
|
|
4292
|
+
armWatcher() {
|
|
4293
|
+
const server = this.server;
|
|
4294
|
+
if (!server) return;
|
|
4295
|
+
this.attachWatcher = startAttachWatcher(this.activeFamily.connection, server, this.deps.attachWatcherIntervalMs ?? 1e3, () => {
|
|
4296
|
+
this.deps.diagnosticsCollector.recordAttach();
|
|
4297
|
+
if (this.activeFamily.connection.kind === "relay") this.deps.devtoolsOpener.open(this.relayTunnelStatus().wssUrl, deriveEnvironment(this.activeFamily.connection.kind, getLiveIntent()));
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
async switchMode(mode, confirm) {
|
|
4301
|
+
if (this.swapInFlight) throw new Error("start_debug: 이전 전환이 아직 진행 중입니다 — 잠시 후 다시 호출하세요.");
|
|
4302
|
+
if (mode === "relay-live" && !confirm) throw new Error("start_debug: relay-live(실서비스 LIVE)는 confirm: true가 필요합니다 — 실유저에게 영향이 갈 수 있는 LIVE 디버깅 진입을 명시적으로 승인하세요.");
|
|
4303
|
+
this.swapInFlight = true;
|
|
4304
|
+
try {
|
|
4305
|
+
const wantRelay = isRelayMode(mode);
|
|
4306
|
+
const wantKind = wantRelay ? "relay" : "local";
|
|
4307
|
+
let target;
|
|
4308
|
+
if (wantKind === this.deps.eager.connection.kind) target = this.deps.eager;
|
|
4309
|
+
else {
|
|
4310
|
+
if (this.lazyFamily === null) this.lazyFamily = await this.deps.bootLazy();
|
|
4311
|
+
target = this.lazyFamily;
|
|
4312
|
+
}
|
|
4313
|
+
this.activeFamily = target;
|
|
4314
|
+
setLiveIntent(mode === "relay-live");
|
|
4315
|
+
this.stopWatcher();
|
|
4316
|
+
this.armWatcher();
|
|
4317
|
+
this.server?.sendToolListChanged();
|
|
4318
|
+
return {
|
|
4319
|
+
mode,
|
|
4320
|
+
environment: deriveEnvironment(target.connection.kind, getLiveIntent()),
|
|
4321
|
+
kind: target.connection.kind,
|
|
4322
|
+
liveGuardActive: target.connection.kind === "relay" && getLiveIntent(),
|
|
4323
|
+
nextStep: wantRelay ? "build_attach_url로 attach QR을 생성하세요 (relay 세션)." : "list_pages로 로컬 Chromium 페이지 attach를 확인하세요."
|
|
4324
|
+
};
|
|
4325
|
+
} finally {
|
|
4326
|
+
this.swapInFlight = false;
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
};
|
|
4330
|
+
/**
|
|
4331
|
+
* Boots the live debug stack and serves it over stdio:
|
|
4332
|
+
* 1. start the Chii relay on an OS-assigned port (with TOTP auth if
|
|
4333
|
+
* AIT_DEBUG_TOTP_SECRET is set),
|
|
4334
|
+
* 2. open a cloudflared quick tunnel to the relay's confirmed port,
|
|
4335
|
+
* 3. print relay URL + attach instructions,
|
|
4336
|
+
* 4. expose the debug tools backed by a `ChiiCdpConnection` + `ChiiAitSource`.
|
|
4337
|
+
*/
|
|
4338
|
+
async function runDebugServer(options = {}) {
|
|
4339
|
+
const lockHandle = acquireLock({ force: options.force ?? false });
|
|
4340
|
+
const verifyAuth = buildRelayVerifyAuth();
|
|
4341
|
+
const relayFamily = await bootRelayFamily({
|
|
4342
|
+
relayPort: options.relayPort,
|
|
4343
|
+
verifyAuth,
|
|
4344
|
+
onWssUrl: (wssUrl) => lockHandle.updateWssUrl(wssUrl)
|
|
4345
|
+
});
|
|
4346
|
+
const devtoolsOpener = new AutoDevtoolsOpener();
|
|
4347
|
+
const diagnosticsCollector = new InMemoryDiagnosticsCollector();
|
|
4348
|
+
const router = new DualConnectionRouter({
|
|
4349
|
+
eager: relayFamily,
|
|
4350
|
+
bootLazy: bootLocalFamily,
|
|
4351
|
+
diagnosticsCollector,
|
|
4352
|
+
devtoolsOpener
|
|
4353
|
+
});
|
|
4354
|
+
const aitSource = new RoutingAitSource(() => {
|
|
4355
|
+
return router.active;
|
|
4356
|
+
});
|
|
4075
4357
|
let qrServer;
|
|
4076
4358
|
try {
|
|
4077
4359
|
qrServer = await startQrHttpServer();
|
|
4078
4360
|
} catch (err) {
|
|
4079
4361
|
logWarn("server.start", { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${err instanceof Error ? err.message : String(err)}` });
|
|
4080
4362
|
}
|
|
4081
|
-
const devtoolsOpener = new AutoDevtoolsOpener();
|
|
4082
|
-
const diagnosticsCollector = new InMemoryDiagnosticsCollector();
|
|
4083
4363
|
const server = createDebugServer({
|
|
4084
|
-
connection,
|
|
4364
|
+
connection: router.active,
|
|
4365
|
+
router,
|
|
4085
4366
|
aitSource,
|
|
4086
|
-
getTunnelStatus: () =>
|
|
4367
|
+
getTunnelStatus: () => router.relayTunnelStatus(),
|
|
4087
4368
|
get qrHttpServer() {
|
|
4088
4369
|
return qrServer;
|
|
4089
4370
|
},
|
|
4090
4371
|
diagnosticsCollector,
|
|
4091
|
-
defaultEnv: "relay-dev",
|
|
4092
4372
|
...process.env.AIT_DEBUG_TOTP_SECRET ? { totpSecret: process.env.AIT_DEBUG_TOTP_SECRET } : {}
|
|
4093
4373
|
});
|
|
4094
4374
|
const transport = new StdioServerTransport();
|
|
4095
4375
|
let closed = false;
|
|
4096
|
-
let
|
|
4376
|
+
let parentWatcher = null;
|
|
4097
4377
|
const shutdown = () => {
|
|
4098
4378
|
if (closed) return;
|
|
4099
4379
|
closed = true;
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
tunnel?.stop();
|
|
4104
|
-
relay.close();
|
|
4380
|
+
parentWatcher?.stop();
|
|
4381
|
+
router.stopWatcher();
|
|
4382
|
+
for (const family of router.bootedFamilies()) family.stop();
|
|
4105
4383
|
server.close();
|
|
4106
4384
|
qrServer?.close();
|
|
4107
4385
|
lockHandle.release();
|
|
@@ -4112,9 +4390,9 @@ async function runDebugServer(options = {}) {
|
|
|
4112
4390
|
process.on("exit", () => {
|
|
4113
4391
|
if (!closed) {
|
|
4114
4392
|
closed = true;
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4393
|
+
parentWatcher?.stop();
|
|
4394
|
+
router.stopWatcher();
|
|
4395
|
+
for (const family of router.bootedFamilies()) family.stop();
|
|
4118
4396
|
lockHandle.release();
|
|
4119
4397
|
}
|
|
4120
4398
|
});
|
|
@@ -4135,31 +4413,48 @@ async function runDebugServer(options = {}) {
|
|
|
4135
4413
|
process.exit(1);
|
|
4136
4414
|
});
|
|
4137
4415
|
await server.connect(transport);
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
})
|
|
4144
|
-
|
|
4416
|
+
router.start(server);
|
|
4417
|
+
if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== "1") {
|
|
4418
|
+
parentWatcher = startParentWatcher(() => {
|
|
4419
|
+
shutdown();
|
|
4420
|
+
process.exit(0);
|
|
4421
|
+
}, { intervalMs: 5e3 });
|
|
4422
|
+
process.stdin.once("end", () => {
|
|
4423
|
+
shutdown();
|
|
4424
|
+
process.exit(0);
|
|
4425
|
+
});
|
|
4426
|
+
process.stdin.once("close", () => {
|
|
4427
|
+
shutdown();
|
|
4428
|
+
process.exit(0);
|
|
4429
|
+
});
|
|
4430
|
+
}
|
|
4145
4431
|
}
|
|
4146
4432
|
/**
|
|
4147
4433
|
* Boots the local-browser debug stack and serves it over stdio:
|
|
4148
4434
|
* 1. launch a local Chromium with `--remote-debugging-port=<port>`,
|
|
4149
4435
|
* 2. attach a `LocalCdpConnection` to the first non-blank page target,
|
|
4150
|
-
* 3. expose the debug tools
|
|
4151
|
-
*
|
|
4152
|
-
*
|
|
4153
|
-
*
|
|
4154
|
-
*
|
|
4155
|
-
*
|
|
4436
|
+
* 3. expose the debug tools through the SAME direction-neutral
|
|
4437
|
+
* `DualConnectionRouter` that `runDebugServer` uses (issue #356) — the
|
|
4438
|
+
* local family is eager, the relay family is lazy-booted on the first
|
|
4439
|
+
* `start_debug({ mode: 'relay-*' })`.
|
|
4440
|
+
*
|
|
4441
|
+
* Symmetry with `runDebugServer` (#356): starting with `--target=local` no
|
|
4442
|
+
* longer pins a single-connection router. A `--target=local` session can
|
|
4443
|
+
* hot-switch into relay (env 1 → env 3) without restarting the MCP server,
|
|
4444
|
+
* closing the asymmetry where only the default (relay-target) entry point had
|
|
4445
|
+
* bidirectional hot-switch. The intended fidelity-ladder flow — "validate in
|
|
4446
|
+
* env 1 (local), then env 3 (intoss-private) in ONE session, no restart" — now
|
|
4447
|
+
* works from either entry point.
|
|
4448
|
+
*
|
|
4449
|
+
* `build_attach_url` (relay-specific) stays effectively hidden / non-applicable
|
|
4450
|
+
* until the relay family is booted: before the first relay switch the env
|
|
4451
|
+
* derives to `mock` and `relayTunnelStatus()` reports "down", so the tool fails
|
|
4452
|
+
* with a clear "tunnel not up" message. After a relay switch the relay tunnel
|
|
4453
|
+
* is live and the tool works.
|
|
4156
4454
|
*
|
|
4157
4455
|
* The AIT.* tools (`AIT.getSdkCallHistory`, `AIT.getMockState`,
|
|
4158
|
-
* `AIT.getOperationalEnvironment`) ride the
|
|
4159
|
-
* `
|
|
4160
|
-
* the sdk-example dev-bridge (`window.__sdkCall` install) lands in sdk-example;
|
|
4161
|
-
* until then they return the sdk-example "bridge absent" message — which is
|
|
4162
|
-
* expected and noted in the PR as an explicit out-of-scope follow-up.
|
|
4456
|
+
* `AIT.getOperationalEnvironment`) ride the *active* connection's CDP channel
|
|
4457
|
+
* via `RoutingAitSource`, so they follow `start_debug` swaps.
|
|
4163
4458
|
*/
|
|
4164
4459
|
async function runLocalDebugServer(options = {}) {
|
|
4165
4460
|
const lockHandle = acquireLock({ force: options.force ?? false });
|
|
@@ -4168,28 +4463,57 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4168
4463
|
devUrl: options.devUrl ?? process.env.AIT_DEVTOOLS_URL ?? "http://localhost:5173"
|
|
4169
4464
|
});
|
|
4170
4465
|
await new Promise((r) => setTimeout(r, 800));
|
|
4171
|
-
const
|
|
4172
|
-
const
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4466
|
+
const localConnection = new LocalCdpConnection({ devtoolsHttpUrl: chromium.devtoolsUrl });
|
|
4467
|
+
const localFamily = {
|
|
4468
|
+
connection: localConnection,
|
|
4469
|
+
stop() {
|
|
4470
|
+
localConnection.close();
|
|
4471
|
+
chromium.stop();
|
|
4472
|
+
}
|
|
4176
4473
|
};
|
|
4474
|
+
const verifyAuth = buildRelayVerifyAuth();
|
|
4475
|
+
const devtoolsOpener = new AutoDevtoolsOpener();
|
|
4476
|
+
const diagnosticsCollector = new InMemoryDiagnosticsCollector();
|
|
4477
|
+
const router = new DualConnectionRouter({
|
|
4478
|
+
eager: localFamily,
|
|
4479
|
+
bootLazy: () => bootRelayFamily({
|
|
4480
|
+
verifyAuth,
|
|
4481
|
+
onWssUrl: (wssUrl) => lockHandle.updateWssUrl(wssUrl)
|
|
4482
|
+
}),
|
|
4483
|
+
diagnosticsCollector,
|
|
4484
|
+
devtoolsOpener
|
|
4485
|
+
});
|
|
4486
|
+
const aitSource = new RoutingAitSource(() => {
|
|
4487
|
+
return router.active;
|
|
4488
|
+
});
|
|
4489
|
+
let qrServer;
|
|
4490
|
+
try {
|
|
4491
|
+
qrServer = await startQrHttpServer();
|
|
4492
|
+
} catch (err) {
|
|
4493
|
+
logWarn("server.start", { msg: `QR HTTP 서버 시작 실패 (text QR fallback 사용): ${err instanceof Error ? err.message : String(err)}` });
|
|
4494
|
+
}
|
|
4177
4495
|
const server = createDebugServer({
|
|
4178
|
-
connection,
|
|
4496
|
+
connection: router.active,
|
|
4497
|
+
router,
|
|
4179
4498
|
aitSource,
|
|
4180
|
-
getTunnelStatus: () =>
|
|
4181
|
-
|
|
4499
|
+
getTunnelStatus: () => router.relayTunnelStatus(),
|
|
4500
|
+
get qrHttpServer() {
|
|
4501
|
+
return qrServer;
|
|
4502
|
+
},
|
|
4503
|
+
diagnosticsCollector,
|
|
4504
|
+
...process.env.AIT_DEBUG_TOTP_SECRET ? { totpSecret: process.env.AIT_DEBUG_TOTP_SECRET } : {}
|
|
4182
4505
|
});
|
|
4183
4506
|
const transport = new StdioServerTransport();
|
|
4184
4507
|
let closed = false;
|
|
4185
|
-
let
|
|
4508
|
+
let parentWatcher = null;
|
|
4186
4509
|
const shutdown = () => {
|
|
4187
4510
|
if (closed) return;
|
|
4188
4511
|
closed = true;
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4512
|
+
parentWatcher?.stop();
|
|
4513
|
+
router.stopWatcher();
|
|
4514
|
+
for (const family of router.bootedFamilies()) family.stop();
|
|
4192
4515
|
server.close();
|
|
4516
|
+
qrServer?.close();
|
|
4193
4517
|
lockHandle.release();
|
|
4194
4518
|
};
|
|
4195
4519
|
process.once("SIGINT", shutdown);
|
|
@@ -4198,8 +4522,9 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4198
4522
|
process.on("exit", () => {
|
|
4199
4523
|
if (!closed) {
|
|
4200
4524
|
closed = true;
|
|
4201
|
-
|
|
4202
|
-
|
|
4525
|
+
parentWatcher?.stop();
|
|
4526
|
+
router.stopWatcher();
|
|
4527
|
+
for (const family of router.bootedFamilies()) family.stop();
|
|
4203
4528
|
lockHandle.release();
|
|
4204
4529
|
}
|
|
4205
4530
|
});
|
|
@@ -4222,7 +4547,21 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4222
4547
|
process.exit(1);
|
|
4223
4548
|
});
|
|
4224
4549
|
await server.connect(transport);
|
|
4225
|
-
|
|
4550
|
+
router.start(server);
|
|
4551
|
+
if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== "1") {
|
|
4552
|
+
parentWatcher = startParentWatcher(() => {
|
|
4553
|
+
shutdown();
|
|
4554
|
+
process.exit(0);
|
|
4555
|
+
}, { intervalMs: 5e3 });
|
|
4556
|
+
process.stdin.once("end", () => {
|
|
4557
|
+
shutdown();
|
|
4558
|
+
process.exit(0);
|
|
4559
|
+
});
|
|
4560
|
+
process.stdin.once("close", () => {
|
|
4561
|
+
shutdown();
|
|
4562
|
+
process.exit(0);
|
|
4563
|
+
});
|
|
4564
|
+
}
|
|
4226
4565
|
}
|
|
4227
4566
|
//#endregion
|
|
4228
4567
|
//#region src/mcp/ait-http-source.ts
|
|
@@ -4652,7 +4991,7 @@ function createDevServer(deps = {}) {
|
|
|
4652
4991
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
4653
4992
|
const server = new Server({
|
|
4654
4993
|
name: "ait-devtools",
|
|
4655
|
-
version: "0.1.
|
|
4994
|
+
version: "0.1.52"
|
|
4656
4995
|
}, { capabilities: { tools: {} } });
|
|
4657
4996
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
4658
4997
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -4736,9 +5075,29 @@ async function runDevServer() {
|
|
|
4736
5075
|
* --mode=dev — dev mode — reads the live browser mock state from a running
|
|
4737
5076
|
* Vite dev server (the devtools#130 `devtools_get_mock_state` surface).
|
|
4738
5077
|
*
|
|
5078
|
+
* Back-compat (issue #348): the legacy `--mode`/`--target` flags and `MCP_ENV`
|
|
5079
|
+
* still work. `--target=relay`/`local` select the initial active connection;
|
|
5080
|
+
* the in-session `start_debug(mode)` MCP tool can then flip between them with no
|
|
5081
|
+
* restart. `MCP_ENV=relay-live` seeds LIVE intent at boot (deprecated alias);
|
|
5082
|
+
* `MCP_ENV=mock|relay|relay-dev` are accepted and ignored for env derivation
|
|
5083
|
+
* (the active connection's `kind` is authoritative).
|
|
5084
|
+
*
|
|
4739
5085
|
* Node-only stdio process.
|
|
4740
5086
|
*/
|
|
4741
5087
|
/**
|
|
5088
|
+
* Seeds the module-level `liveIntent` bit from the deprecated `MCP_ENV` alias
|
|
5089
|
+
* (issue #348). `MCP_ENV=relay-live` is the only value that matters now — it
|
|
5090
|
+
* arms LIVE intent at boot so a session launched straight into env 4 has the
|
|
5091
|
+
* guard active without a `start_debug({ mode: 'relay-live' })` round-trip. All
|
|
5092
|
+
* other `MCP_ENV` values (`mock`, `relay`, `relay-dev`) are accepted-and-ignored
|
|
5093
|
+
* for env derivation — the active connection's `kind` is authoritative.
|
|
5094
|
+
*
|
|
5095
|
+
* SECRET-HANDLING: reads only the env-var string; never logs a secret.
|
|
5096
|
+
*/
|
|
5097
|
+
function seedLiveIntentFromEnv(env = process.env) {
|
|
5098
|
+
if (env.MCP_ENV === "relay-live") setLiveIntent(true);
|
|
5099
|
+
}
|
|
5100
|
+
/**
|
|
4742
5101
|
* Returns `true` when `--force` or `--takeover` is present in argv.
|
|
4743
5102
|
*
|
|
4744
5103
|
* Both flags are accepted as aliases — `--force` is the short form listed in
|
|
@@ -4793,6 +5152,7 @@ function normalizeTarget(value) {
|
|
|
4793
5152
|
}
|
|
4794
5153
|
async function main() {
|
|
4795
5154
|
const args = process.argv.slice(2);
|
|
5155
|
+
seedLiveIntentFromEnv();
|
|
4796
5156
|
if (parseMode(args) === "dev") await runDevServer();
|
|
4797
5157
|
else {
|
|
4798
5158
|
const target = parseTarget(args);
|
|
@@ -4826,6 +5186,6 @@ if (isEntrypoint()) main().catch((err) => {
|
|
|
4826
5186
|
process.exitCode = 1;
|
|
4827
5187
|
});
|
|
4828
5188
|
//#endregion
|
|
4829
|
-
export { parseForce, parseMode, parseTarget };
|
|
5189
|
+
export { parseForce, parseMode, parseTarget, seedLiveIntentFromEnv };
|
|
4830
5190
|
|
|
4831
5191
|
//# sourceMappingURL=cli.js.map
|