@ait-co/devtools 0.1.41 → 0.1.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/cli.js +709 -90
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +60 -24
- package/dist/mcp/server.js.map +1 -1
- package/dist/mock/index.d.ts +0 -4
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +0 -1
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +40 -52
- package/dist/panel/index.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { argv } from "node:process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -11,9 +11,13 @@ import { WebSocket } from "ws";
|
|
|
11
11
|
import { createServer } from "node:http";
|
|
12
12
|
import { spawn } from "node:child_process";
|
|
13
13
|
import net from "node:net";
|
|
14
|
-
import { platform } from "node:os";
|
|
14
|
+
import { homedir, platform } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
15
16
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
16
17
|
import { Tunnel, bin, install } from "cloudflared";
|
|
18
|
+
//#region \0rolldown/runtime.js
|
|
19
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
|
+
//#endregion
|
|
17
21
|
//#region src/mcp/ait-chii-source.ts
|
|
18
22
|
function isObject$4(value) {
|
|
19
23
|
return typeof value === "object" && value !== null;
|
|
@@ -62,6 +66,12 @@ var ChiiAitSource = class {
|
|
|
62
66
|
* events in ring buffers the tool layer reads via `getBufferedEvents`.
|
|
63
67
|
*
|
|
64
68
|
* Node-only: imports `ws`. Never bundled into the browser/in-app entries.
|
|
69
|
+
*
|
|
70
|
+
* Attach reliability (#281):
|
|
71
|
+
* `refreshTargets()` emits an internal 'target:attached' event whenever a
|
|
72
|
+
* new target is added to the relay. `waitForFirstTarget()` awaits that event
|
|
73
|
+
* (with a polling-interval fallback) so `build_attach_url wait_for_attach`
|
|
74
|
+
* resolves deterministically rather than racing between polling rounds.
|
|
65
75
|
*/
|
|
66
76
|
/** Max events retained per domain ring buffer. */
|
|
67
77
|
const DEFAULT_BUFFER_SIZE$1 = 500;
|
|
@@ -181,12 +191,73 @@ var ChiiCdpConnection = class {
|
|
|
181
191
|
}
|
|
182
192
|
if (newestTargetId !== null) this.activeTargetId = newestTargetId;
|
|
183
193
|
else this.activeTargetId = null;
|
|
184
|
-
|
|
194
|
+
const result = [...this.targets.values()];
|
|
195
|
+
if (newestTargetId !== null) this.emitter.emit("target:attached", result);
|
|
196
|
+
return result;
|
|
185
197
|
}
|
|
186
198
|
listTargets() {
|
|
187
199
|
return [...this.targets.values()];
|
|
188
200
|
}
|
|
189
201
|
/**
|
|
202
|
+
* Waits until at least one target matching `filterFn` is attached, then
|
|
203
|
+
* resolves with the full target list at that moment.
|
|
204
|
+
*
|
|
205
|
+
* Resolution happens on whichever comes first:
|
|
206
|
+
* (a) a `'target:attached'` event from `refreshTargets()` (triggered by
|
|
207
|
+
* the /targets poll finding a new target), OR
|
|
208
|
+
* (b) a `'target:attached'` event from `handleMessage()` (triggered by
|
|
209
|
+
* the first inbound CDP message from a target — confirms the relay
|
|
210
|
+
* websocket has data from the phone, not just a target entry in the map).
|
|
211
|
+
*
|
|
212
|
+
* This dual-signal approach eliminates the polling race that previously
|
|
213
|
+
* caused `wait_for_attach` to resolve before the first CDP message arrived.
|
|
214
|
+
*
|
|
215
|
+
* Falls back to checking `listTargets()` every `pollIntervalMs` in case the
|
|
216
|
+
* EventEmitter is missed (defensive belt-and-suspenders).
|
|
217
|
+
*
|
|
218
|
+
* @param filterFn - Predicate that the returned targets must satisfy.
|
|
219
|
+
* @param timeoutMs - Reject after this many ms (default 90 000).
|
|
220
|
+
* @param pollIntervalMs - Fallback poll interval (default 500ms).
|
|
221
|
+
*/
|
|
222
|
+
waitForFirstTarget(filterFn, timeoutMs = 9e4, pollIntervalMs = 500) {
|
|
223
|
+
const current = this.listTargets();
|
|
224
|
+
if (filterFn(current)) return Promise.resolve(current);
|
|
225
|
+
return new Promise((resolve, reject) => {
|
|
226
|
+
let settled = false;
|
|
227
|
+
let pollHandle = null;
|
|
228
|
+
const settle = (targets) => {
|
|
229
|
+
if (settled) return;
|
|
230
|
+
settled = true;
|
|
231
|
+
clearTimeout(timeoutHandle);
|
|
232
|
+
if (pollHandle !== null) {
|
|
233
|
+
clearInterval(pollHandle);
|
|
234
|
+
pollHandle = null;
|
|
235
|
+
}
|
|
236
|
+
this.emitter.off("target:attached", onAttach);
|
|
237
|
+
resolve(targets);
|
|
238
|
+
};
|
|
239
|
+
const onAttach = (targets) => {
|
|
240
|
+
if (filterFn(targets)) settle(targets);
|
|
241
|
+
};
|
|
242
|
+
const timeoutHandle = setTimeout(() => {
|
|
243
|
+
if (settled) return;
|
|
244
|
+
settled = true;
|
|
245
|
+
if (pollHandle !== null) {
|
|
246
|
+
clearInterval(pollHandle);
|
|
247
|
+
pollHandle = null;
|
|
248
|
+
}
|
|
249
|
+
this.emitter.off("target:attached", onAttach);
|
|
250
|
+
reject(/* @__PURE__ */ new Error(`waitForFirstTarget: 타임아웃 (${timeoutMs}ms) — 폰이 relay에 attach되지 않았습니다.`));
|
|
251
|
+
}, timeoutMs);
|
|
252
|
+
this.emitter.on("target:attached", onAttach);
|
|
253
|
+
pollHandle = setInterval(() => {
|
|
254
|
+
this.refreshTargets().then((targets) => {
|
|
255
|
+
if (filterFn(targets)) settle(targets);
|
|
256
|
+
}, () => {});
|
|
257
|
+
}, pollIntervalMs);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
190
261
|
* Timestamp (ms since epoch) of the most recent crash/destroy/detach event
|
|
191
262
|
* detected since the last `enableDomains()` call, or `null` if none.
|
|
192
263
|
*/
|
|
@@ -423,7 +494,12 @@ var ChiiCdpConnection = class {
|
|
|
423
494
|
return;
|
|
424
495
|
}
|
|
425
496
|
const now = Date.now();
|
|
426
|
-
|
|
497
|
+
let firstMessageSeen = false;
|
|
498
|
+
for (const targetId of this.targets.keys()) {
|
|
499
|
+
if (!this.targetLastSeenAt.has(targetId)) firstMessageSeen = true;
|
|
500
|
+
this.targetLastSeenAt.set(targetId, now);
|
|
501
|
+
}
|
|
502
|
+
if (firstMessageSeen && this.targets.size > 0) this.emitter.emit("target:attached", [...this.targets.values()]);
|
|
427
503
|
if (typeof message.method !== "string") return;
|
|
428
504
|
if (message.method === "Inspector.targetCrashed") {
|
|
429
505
|
this.handleTargetGone("crashed", null);
|
|
@@ -493,9 +569,9 @@ var ChiiCdpConnection = class {
|
|
|
493
569
|
* in any log, error message, or process output. `verifyAuth` is a black-box
|
|
494
570
|
* predicate from the caller's perspective; this module only forwards pass/fail.
|
|
495
571
|
*/
|
|
496
|
-
const require = createRequire(import.meta.url);
|
|
572
|
+
const require$1 = createRequire(import.meta.url);
|
|
497
573
|
function loadChiiServer() {
|
|
498
|
-
const mod = require("chii");
|
|
574
|
+
const mod = require$1("chii");
|
|
499
575
|
if (typeof mod === "object" && mod !== null && "start" in mod && typeof mod.start === "function") return mod;
|
|
500
576
|
throw new Error("chii server module did not expose start()");
|
|
501
577
|
}
|
|
@@ -548,6 +624,206 @@ async function startChiiRelay(options = {}) {
|
|
|
548
624
|
};
|
|
549
625
|
}
|
|
550
626
|
//#endregion
|
|
627
|
+
//#region src/mcp/devtools-opener.ts
|
|
628
|
+
/**
|
|
629
|
+
* Base URL for the Chrome DevTools inspector hosted on appspot.
|
|
630
|
+
*
|
|
631
|
+
* The `@` path segment is the "latest / bleeding edge" alias which tracks the
|
|
632
|
+
* current Chrome stable CDP protocol version — compatible with the chobitsu-
|
|
633
|
+
* based CDP that Chii injects. A specific commit hash may be pinned here if
|
|
634
|
+
* a regression is observed.
|
|
635
|
+
*/
|
|
636
|
+
const DEVTOOLS_FRONTEND_BASE = "https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html";
|
|
637
|
+
/**
|
|
638
|
+
* Assembles the Chrome DevTools inspector URL that connects to a Chii relay
|
|
639
|
+
* WebSocket.
|
|
640
|
+
*
|
|
641
|
+
* The `wss=` parameter expects a host-and-path string without the `wss://`
|
|
642
|
+
* scheme prefix — the DevTools frontend prepends it automatically.
|
|
643
|
+
*
|
|
644
|
+
* @param wssRelayUrl - Full `wss://` URL of the Chii relay (public tunnel).
|
|
645
|
+
* Example: `wss://abc.trycloudflare.com`
|
|
646
|
+
* @param panel - Initial panel. Defaults to `"console"`.
|
|
647
|
+
*
|
|
648
|
+
* @example
|
|
649
|
+
* buildChromeDevtoolsUrl('wss://abc.trycloudflare.com')
|
|
650
|
+
* // → 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html?wss=abc.trycloudflare.com&panel=console'
|
|
651
|
+
*/
|
|
652
|
+
function buildChromeDevtoolsUrl(wssRelayUrl, panel = "console") {
|
|
653
|
+
const wssParam = wssRelayUrl.replace(/^wss:\/\//i, "");
|
|
654
|
+
return `${DEVTOOLS_FRONTEND_BASE}?${new URLSearchParams({
|
|
655
|
+
wss: wssParam,
|
|
656
|
+
panel
|
|
657
|
+
}).toString()}`;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Returns `true` when auto-open is **disabled** via the `AIT_AUTO_DEVTOOLS`
|
|
661
|
+
* env var. Only the explicit `"0"` value disables it; anything else (including
|
|
662
|
+
* absent) leaves auto-open enabled.
|
|
663
|
+
*/
|
|
664
|
+
function isAutoDevtoolsDisabled() {
|
|
665
|
+
return process.env.AIT_AUTO_DEVTOOLS === "0";
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Opens the given URL in the OS default browser using a platform-appropriate
|
|
669
|
+
* command. Returns `true` on success.
|
|
670
|
+
*
|
|
671
|
+
* Failures are silent from the caller's perspective — the caller should log
|
|
672
|
+
* the URL to stderr as a fallback before calling this function.
|
|
673
|
+
*/
|
|
674
|
+
function openUrlInBrowser(url) {
|
|
675
|
+
if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === "1") return false;
|
|
676
|
+
const { spawnSync } = __require("node:child_process");
|
|
677
|
+
const platform = process.platform;
|
|
678
|
+
let candidates;
|
|
679
|
+
if (platform === "darwin") candidates = [{
|
|
680
|
+
cmd: "open",
|
|
681
|
+
args: [url]
|
|
682
|
+
}];
|
|
683
|
+
else if (platform === "win32") candidates = [{
|
|
684
|
+
cmd: "cmd",
|
|
685
|
+
args: [
|
|
686
|
+
"/c",
|
|
687
|
+
"start",
|
|
688
|
+
"",
|
|
689
|
+
url
|
|
690
|
+
]
|
|
691
|
+
}];
|
|
692
|
+
else candidates = [
|
|
693
|
+
{
|
|
694
|
+
cmd: "xdg-open",
|
|
695
|
+
args: [url]
|
|
696
|
+
},
|
|
697
|
+
{
|
|
698
|
+
cmd: "sensible-browser",
|
|
699
|
+
args: [url]
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
cmd: "x-www-browser",
|
|
703
|
+
args: [url]
|
|
704
|
+
}
|
|
705
|
+
];
|
|
706
|
+
for (const { cmd, args } of candidates) try {
|
|
707
|
+
const result = spawnSync(cmd, args, {
|
|
708
|
+
encoding: "utf8",
|
|
709
|
+
timeout: 5e3
|
|
710
|
+
});
|
|
711
|
+
if (!result.error && result.status === 0) return true;
|
|
712
|
+
} catch {}
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Manages auto-opening Chrome DevTools exactly once per relay attach session.
|
|
717
|
+
*
|
|
718
|
+
* Create one instance per `runDebugServer` call and pass its `open()` method
|
|
719
|
+
* as the `onFirstAttach` callback to `startAttachWatcher`.
|
|
720
|
+
*
|
|
721
|
+
* The open fires at most once. Subsequent `open()` calls are no-ops.
|
|
722
|
+
* Opt-out and mock-environment guard are checked at call time.
|
|
723
|
+
*/
|
|
724
|
+
var AutoDevtoolsOpener = class {
|
|
725
|
+
_opened = false;
|
|
726
|
+
/**
|
|
727
|
+
* Attempts to auto-open Chrome DevTools.
|
|
728
|
+
*
|
|
729
|
+
* No-op when any of the following conditions hold:
|
|
730
|
+
* 1. Already opened this session (`_opened` is true).
|
|
731
|
+
* 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.
|
|
732
|
+
* 3. Environment is `mock` (env 1 — F12 is already available).
|
|
733
|
+
* 4. `wssRelayUrl` is null/undefined/empty (tunnel not yet up).
|
|
734
|
+
*
|
|
735
|
+
* Always writes the DevTools URL to stderr so the developer can copy it
|
|
736
|
+
* if the browser open fails or the popup is blocked.
|
|
737
|
+
*
|
|
738
|
+
* @param wssRelayUrl - The public `wss://` relay URL (from tunnel status).
|
|
739
|
+
* @param env - Current MCP environment (`mock` | `relay`).
|
|
740
|
+
*/
|
|
741
|
+
open(wssRelayUrl, env) {
|
|
742
|
+
if (this._opened) return;
|
|
743
|
+
if (isAutoDevtoolsDisabled()) return;
|
|
744
|
+
if (env === "mock") return;
|
|
745
|
+
if (!wssRelayUrl) return;
|
|
746
|
+
this._opened = true;
|
|
747
|
+
const devtoolsUrl = buildChromeDevtoolsUrl(wssRelayUrl);
|
|
748
|
+
process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chrome DevTools를 자동으로 엽니다.
|
|
749
|
+
[ait-debug] Chrome DevTools URL: ${devtoolsUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)
|
|
750
|
+
`);
|
|
751
|
+
if (!openUrlInBrowser(devtoolsUrl)) process.stderr.write("[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\n");
|
|
752
|
+
}
|
|
753
|
+
/** Returns `true` if `open()` has passed all guards and fired once. */
|
|
754
|
+
get opened() {
|
|
755
|
+
return this._opened;
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
//#endregion
|
|
759
|
+
//#region src/mcp/environment.ts
|
|
760
|
+
/**
|
|
761
|
+
* URL patterns that mark a CDP target as a real-device WebView relay.
|
|
762
|
+
*
|
|
763
|
+
* - `intoss-private://` is the Toss in-app private scheme — only ever observed
|
|
764
|
+
* inside the real Toss app WebView.
|
|
765
|
+
* - `*.trycloudflare.com` (host suffix) is the cloudflared quick tunnel used as
|
|
766
|
+
* the relay transport. A target whose URL is on that host is, by construction,
|
|
767
|
+
* reached over the relay.
|
|
768
|
+
*
|
|
769
|
+
* Pattern-only matches — no specific tunnel host or deploymentId is hard-coded.
|
|
770
|
+
*/
|
|
771
|
+
const RELAY_URL_PATTERNS = [/^intoss-private:\/\//i, /:\/\/[a-z0-9-]+\.trycloudflare\.com(\/|$|:|\?)/i];
|
|
772
|
+
/**
|
|
773
|
+
* Returns true when the URL string looks like a real-device WebView attached
|
|
774
|
+
* over the Chii relay. Used for `getEnvironment()` precedence step 2.
|
|
775
|
+
*/
|
|
776
|
+
function isRelayUrl(url) {
|
|
777
|
+
if (typeof url !== "string" || url.length === 0) return false;
|
|
778
|
+
return RELAY_URL_PATTERNS.some((p) => p.test(url));
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Test/override hook — when non-null, `getEnvironment()` returns this value
|
|
782
|
+
* regardless of env vars or connection state. Cleared with `null`.
|
|
783
|
+
*/
|
|
784
|
+
let envOverride = null;
|
|
785
|
+
/** Parses the `MCP_ENV` env var into a `McpEnvironment` if valid. */
|
|
786
|
+
function readEnvVar() {
|
|
787
|
+
const raw = process.env.MCP_ENV;
|
|
788
|
+
if (raw === "mock" || raw === "relay") return raw;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Returns the current MCP environment, applying the precedence rules:
|
|
792
|
+
* 1. test override (if set)
|
|
793
|
+
* 2. `MCP_ENV` env var
|
|
794
|
+
* 3. CDP target URL pattern match
|
|
795
|
+
* 4. default `mock`
|
|
796
|
+
*/
|
|
797
|
+
function getEnvironment(input = {}) {
|
|
798
|
+
if (envOverride !== null) return envOverride;
|
|
799
|
+
const fromEnv = readEnvVar();
|
|
800
|
+
if (fromEnv !== void 0) return fromEnv;
|
|
801
|
+
const { connection } = input;
|
|
802
|
+
if (connection !== void 0) {
|
|
803
|
+
const targets = connection.listTargets();
|
|
804
|
+
for (const t of targets) if (isRelayUrl(t.url)) return "relay";
|
|
805
|
+
}
|
|
806
|
+
return "mock";
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Returns the `EnvironmentReason` that drove the current `getEnvironment()`
|
|
810
|
+
* result. Used by stderr logs and the rejection-reason payload on Tier A/B
|
|
811
|
+
* mismatch errors. SECRET-HANDLING: only stable enum strings — no URL or
|
|
812
|
+
* secret value is ever returned.
|
|
813
|
+
*/
|
|
814
|
+
function getEnvironmentReason(input = {}) {
|
|
815
|
+
if (envOverride !== null) return envOverride === "mock" ? "env-var-mock" : "env-var-relay";
|
|
816
|
+
const fromEnv = readEnvVar();
|
|
817
|
+
if (fromEnv === "mock") return "env-var-mock";
|
|
818
|
+
if (fromEnv === "relay") return "env-var-relay";
|
|
819
|
+
const { connection } = input;
|
|
820
|
+
if (connection !== void 0) {
|
|
821
|
+
const targets = connection.listTargets();
|
|
822
|
+
for (const t of targets) if (isRelayUrl(t.url)) return "cdp-target-url-relay-pattern";
|
|
823
|
+
}
|
|
824
|
+
return "default-mock";
|
|
825
|
+
}
|
|
826
|
+
//#endregion
|
|
551
827
|
//#region src/mcp/local-connection.ts
|
|
552
828
|
/**
|
|
553
829
|
* Local-browser `CdpConnection` — attaches directly to a Chromium instance
|
|
@@ -1049,6 +1325,145 @@ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl) {
|
|
|
1049
1325
|
</html>`;
|
|
1050
1326
|
}
|
|
1051
1327
|
//#endregion
|
|
1328
|
+
//#region src/mcp/server-lock.ts
|
|
1329
|
+
/**
|
|
1330
|
+
* Single debug session lock for the `devtools-mcp` debug server.
|
|
1331
|
+
*
|
|
1332
|
+
* At most one debug server process should run on a given machine at a time —
|
|
1333
|
+
* multiple concurrent instances create duplicate cloudflared tunnels, waste
|
|
1334
|
+
* resources, and confuse the user about which wssUrl to use.
|
|
1335
|
+
*
|
|
1336
|
+
* ## Lock file
|
|
1337
|
+
*
|
|
1338
|
+
* Location: `~/.ait-devtools/server.lock`
|
|
1339
|
+
*
|
|
1340
|
+
* Schema (JSON):
|
|
1341
|
+
* ```json
|
|
1342
|
+
* { "pid": 12345, "wssUrl": "wss://xxx.trycloudflare.com", "startedAt": "2026-01-01T00:00:00.000Z" }
|
|
1343
|
+
* ```
|
|
1344
|
+
*
|
|
1345
|
+
* ## Behaviour
|
|
1346
|
+
*
|
|
1347
|
+
* - **Acquire**: write PID + wssUrl + startedAt. Returns a `release()` handle.
|
|
1348
|
+
* - **Stale lock recovery**: if the stored PID is no longer alive
|
|
1349
|
+
* (`process.kill(pid, 0)` throws ESRCH), the lock is silently replaced.
|
|
1350
|
+
* - **Live conflict (option B)**: if the stored PID is alive, `acquireLock`
|
|
1351
|
+
* throws `ServerLockConflictError` with the existing PID and wssUrl so the
|
|
1352
|
+
* caller can surface a clear message to the agent.
|
|
1353
|
+
* - **Release**: remove the lock file. Called on graceful shutdown (SIGINT /
|
|
1354
|
+
* SIGTERM / SIGHUP). SIGKILL survivors leave a stale file — the next startup
|
|
1355
|
+
* recovers it automatically via the alive check.
|
|
1356
|
+
*
|
|
1357
|
+
* ## wssUrl update
|
|
1358
|
+
*
|
|
1359
|
+
* The lock is written before cloudflared starts, so `wssUrl` begins as `null`
|
|
1360
|
+
* and is updated in place once the tunnel URL is known via `updateWssUrl`.
|
|
1361
|
+
*
|
|
1362
|
+
* Node-only.
|
|
1363
|
+
*/
|
|
1364
|
+
/** Thrown when a live server process already holds the lock. */
|
|
1365
|
+
var ServerLockConflictError = class extends Error {
|
|
1366
|
+
/** PID of the existing server process. */
|
|
1367
|
+
existingPid;
|
|
1368
|
+
/** wssUrl from the existing lock — may be `null` if the tunnel is still starting. */
|
|
1369
|
+
existingWssUrl;
|
|
1370
|
+
constructor(existingPid, existingWssUrl) {
|
|
1371
|
+
const urlNote = existingWssUrl != null ? ` relay URL: ${existingWssUrl}\n` : " relay URL: (tunnel still starting — retry in a moment)\n";
|
|
1372
|
+
super(`A debug server is already running (PID ${existingPid}).\n` + urlNote + `Stop the existing session before starting a new one.
|
|
1373
|
+
If it is already stopped but this error persists, remove the lock file:
|
|
1374
|
+
rm "${lockFilePath()}"`);
|
|
1375
|
+
this.name = "ServerLockConflictError";
|
|
1376
|
+
this.existingPid = existingPid;
|
|
1377
|
+
this.existingWssUrl = existingWssUrl;
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
/** Returns `~/.ait-devtools/server.lock` (or `AIT_DEVTOOLS_LOCK_DIR` override for tests). */
|
|
1381
|
+
function lockFilePath() {
|
|
1382
|
+
return join(process.env.AIT_DEVTOOLS_LOCK_DIR ?? join(homedir(), ".ait-devtools"), "server.lock");
|
|
1383
|
+
}
|
|
1384
|
+
function ensureLockDir(lockPath) {
|
|
1385
|
+
mkdirSync(join(lockPath, ".."), { recursive: true });
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Returns `true` when the given PID refers to a running process.
|
|
1389
|
+
*
|
|
1390
|
+
* Uses `process.kill(pid, 0)` — a no-op signal that succeeds when the process
|
|
1391
|
+
* exists and we have permission to signal it; throws ESRCH when it doesn't exist.
|
|
1392
|
+
*/
|
|
1393
|
+
function isPidAlive(pid) {
|
|
1394
|
+
try {
|
|
1395
|
+
process.kill(pid, 0);
|
|
1396
|
+
return true;
|
|
1397
|
+
} catch (err) {
|
|
1398
|
+
if (err.code === "EPERM") return true;
|
|
1399
|
+
return false;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
function readLock(lockPath) {
|
|
1403
|
+
if (!existsSync(lockPath)) return null;
|
|
1404
|
+
try {
|
|
1405
|
+
const raw = readFileSync(lockPath, "utf8");
|
|
1406
|
+
const parsed = JSON.parse(raw);
|
|
1407
|
+
if (typeof parsed === "object" && parsed !== null && "pid" in parsed && typeof parsed.pid === "number" && "startedAt" in parsed && typeof parsed.startedAt === "string") {
|
|
1408
|
+
const p = parsed;
|
|
1409
|
+
return {
|
|
1410
|
+
pid: p.pid,
|
|
1411
|
+
wssUrl: typeof p.wssUrl === "string" ? p.wssUrl : null,
|
|
1412
|
+
startedAt: p.startedAt
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
return null;
|
|
1416
|
+
} catch {
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
function writeLock(lockPath, data) {
|
|
1421
|
+
ensureLockDir(lockPath);
|
|
1422
|
+
writeFileSync(lockPath, JSON.stringify(data, null, 2), { encoding: "utf8" });
|
|
1423
|
+
}
|
|
1424
|
+
function removeLock(lockPath) {
|
|
1425
|
+
try {
|
|
1426
|
+
rmSync(lockPath);
|
|
1427
|
+
} catch {}
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Attempts to acquire the server lock.
|
|
1431
|
+
*
|
|
1432
|
+
* - If no lock exists (or the lock is stale): writes a new lock and returns a
|
|
1433
|
+
* `LockHandle` with `updateWssUrl` + `release`.
|
|
1434
|
+
* - If a live process holds the lock: throws `ServerLockConflictError`.
|
|
1435
|
+
*
|
|
1436
|
+
* The initial `wssUrl` in the lock file is `null` — call
|
|
1437
|
+
* `handle.updateWssUrl(url)` once the cloudflared tunnel is ready.
|
|
1438
|
+
*/
|
|
1439
|
+
function acquireLock() {
|
|
1440
|
+
const lockPath = lockFilePath();
|
|
1441
|
+
const existing = readLock(lockPath);
|
|
1442
|
+
if (existing !== null) {
|
|
1443
|
+
if (isPidAlive(existing.pid)) throw new ServerLockConflictError(existing.pid, existing.wssUrl);
|
|
1444
|
+
process.stderr.write(`[ait-debug] stale lock from PID ${existing.pid} recovered — starting fresh.\n`);
|
|
1445
|
+
}
|
|
1446
|
+
const data = {
|
|
1447
|
+
pid: process.pid,
|
|
1448
|
+
wssUrl: null,
|
|
1449
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1450
|
+
};
|
|
1451
|
+
writeLock(lockPath, data);
|
|
1452
|
+
let released = false;
|
|
1453
|
+
return {
|
|
1454
|
+
updateWssUrl(wssUrl) {
|
|
1455
|
+
if (released) return;
|
|
1456
|
+
data.wssUrl = wssUrl;
|
|
1457
|
+
writeLock(lockPath, data);
|
|
1458
|
+
},
|
|
1459
|
+
release() {
|
|
1460
|
+
if (released) return;
|
|
1461
|
+
released = true;
|
|
1462
|
+
removeLock(lockPath);
|
|
1463
|
+
}
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
//#endregion
|
|
1052
1467
|
//#region src/mcp/deeplink.ts
|
|
1053
1468
|
/**
|
|
1054
1469
|
* Build a self-attaching dogfood deep link.
|
|
@@ -1327,7 +1742,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1327
1742
|
type: "object",
|
|
1328
1743
|
properties: {},
|
|
1329
1744
|
required: []
|
|
1330
|
-
}
|
|
1745
|
+
},
|
|
1746
|
+
availableIn: "both"
|
|
1331
1747
|
},
|
|
1332
1748
|
{
|
|
1333
1749
|
name: "list_network_requests",
|
|
@@ -1336,7 +1752,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1336
1752
|
type: "object",
|
|
1337
1753
|
properties: {},
|
|
1338
1754
|
required: []
|
|
1339
|
-
}
|
|
1755
|
+
},
|
|
1756
|
+
availableIn: "both"
|
|
1340
1757
|
},
|
|
1341
1758
|
{
|
|
1342
1759
|
name: "list_pages",
|
|
@@ -1345,7 +1762,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1345
1762
|
type: "object",
|
|
1346
1763
|
properties: {},
|
|
1347
1764
|
required: []
|
|
1348
|
-
}
|
|
1765
|
+
},
|
|
1766
|
+
availableIn: "both"
|
|
1349
1767
|
},
|
|
1350
1768
|
{
|
|
1351
1769
|
name: "build_attach_url",
|
|
@@ -1367,7 +1785,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1367
1785
|
}
|
|
1368
1786
|
},
|
|
1369
1787
|
required: ["scheme_url"]
|
|
1370
|
-
}
|
|
1788
|
+
},
|
|
1789
|
+
availableIn: "relay"
|
|
1371
1790
|
},
|
|
1372
1791
|
{
|
|
1373
1792
|
name: "get_dom_document",
|
|
@@ -1376,7 +1795,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1376
1795
|
type: "object",
|
|
1377
1796
|
properties: {},
|
|
1378
1797
|
required: []
|
|
1379
|
-
}
|
|
1798
|
+
},
|
|
1799
|
+
availableIn: "both"
|
|
1380
1800
|
},
|
|
1381
1801
|
{
|
|
1382
1802
|
name: "take_snapshot",
|
|
@@ -1385,7 +1805,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1385
1805
|
type: "object",
|
|
1386
1806
|
properties: {},
|
|
1387
1807
|
required: []
|
|
1388
|
-
}
|
|
1808
|
+
},
|
|
1809
|
+
availableIn: "both"
|
|
1389
1810
|
},
|
|
1390
1811
|
{
|
|
1391
1812
|
name: "take_screenshot",
|
|
@@ -1394,16 +1815,18 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1394
1815
|
type: "object",
|
|
1395
1816
|
properties: {},
|
|
1396
1817
|
required: []
|
|
1397
|
-
}
|
|
1818
|
+
},
|
|
1819
|
+
availableIn: "both"
|
|
1398
1820
|
},
|
|
1399
1821
|
{
|
|
1400
1822
|
name: "measure_safe_area",
|
|
1401
|
-
description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Use in a relay session (phone attached) to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires
|
|
1823
|
+
description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel page with window.__ait state) and `relay` (real-device WebView with window.__sdk). The result includes a `source: \"mock\" | \"relay\"` field so consumers can identify provenance without inspecting payload values. Use in a relay session (phone attached) to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires a page to be attached — call list_pages first.",
|
|
1402
1824
|
inputSchema: {
|
|
1403
1825
|
type: "object",
|
|
1404
1826
|
properties: {},
|
|
1405
1827
|
required: []
|
|
1406
|
-
}
|
|
1828
|
+
},
|
|
1829
|
+
availableIn: "both"
|
|
1407
1830
|
},
|
|
1408
1831
|
{
|
|
1409
1832
|
name: "evaluate",
|
|
@@ -1415,7 +1838,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1415
1838
|
description: "JavaScript expression to evaluate in the page context."
|
|
1416
1839
|
} },
|
|
1417
1840
|
required: ["expression"]
|
|
1418
|
-
}
|
|
1841
|
+
},
|
|
1842
|
+
availableIn: "both"
|
|
1419
1843
|
},
|
|
1420
1844
|
{
|
|
1421
1845
|
name: "list_exceptions",
|
|
@@ -1427,7 +1851,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1427
1851
|
description: "Maximum number of exceptions to return (default 50, max 50)."
|
|
1428
1852
|
} },
|
|
1429
1853
|
required: []
|
|
1430
|
-
}
|
|
1854
|
+
},
|
|
1855
|
+
availableIn: "both"
|
|
1431
1856
|
},
|
|
1432
1857
|
{
|
|
1433
1858
|
name: "call_sdk",
|
|
@@ -1446,7 +1871,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1446
1871
|
}
|
|
1447
1872
|
},
|
|
1448
1873
|
required: ["name"]
|
|
1449
|
-
}
|
|
1874
|
+
},
|
|
1875
|
+
availableIn: "both"
|
|
1450
1876
|
},
|
|
1451
1877
|
{
|
|
1452
1878
|
name: "AIT.getSdkCallHistory",
|
|
@@ -1455,7 +1881,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1455
1881
|
type: "object",
|
|
1456
1882
|
properties: {},
|
|
1457
1883
|
required: []
|
|
1458
|
-
}
|
|
1884
|
+
},
|
|
1885
|
+
availableIn: "both"
|
|
1459
1886
|
},
|
|
1460
1887
|
{
|
|
1461
1888
|
name: "AIT.getMockState",
|
|
@@ -1464,7 +1891,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1464
1891
|
type: "object",
|
|
1465
1892
|
properties: {},
|
|
1466
1893
|
required: []
|
|
1467
|
-
}
|
|
1894
|
+
},
|
|
1895
|
+
availableIn: "both"
|
|
1468
1896
|
},
|
|
1469
1897
|
{
|
|
1470
1898
|
name: "AIT.getOperationalEnvironment",
|
|
@@ -1473,7 +1901,8 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
1473
1901
|
type: "object",
|
|
1474
1902
|
properties: {},
|
|
1475
1903
|
required: []
|
|
1476
|
-
}
|
|
1904
|
+
},
|
|
1905
|
+
availableIn: "both"
|
|
1477
1906
|
}
|
|
1478
1907
|
];
|
|
1479
1908
|
const DEBUG_TOOL_NAMES = new Set(DEBUG_TOOL_DEFINITIONS.map((t) => t.name));
|
|
@@ -1481,6 +1910,34 @@ function isDebugToolName(name) {
|
|
|
1481
1910
|
return DEBUG_TOOL_NAMES.has(name);
|
|
1482
1911
|
}
|
|
1483
1912
|
/**
|
|
1913
|
+
* Returns the `ToolAvailability` declared on a registered debug tool, or
|
|
1914
|
+
* `undefined` when the name is not a known debug tool. Used by the tool
|
|
1915
|
+
* registry to filter `tools/list` by current env and by the call handler to
|
|
1916
|
+
* reject env-mismatch invocations.
|
|
1917
|
+
*/
|
|
1918
|
+
function getToolAvailability(name) {
|
|
1919
|
+
for (const t of DEBUG_TOOL_DEFINITIONS) if (t.name === name) return t.availableIn;
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* Returns true when the named tool is available in the given environment.
|
|
1923
|
+
* Unknown tools return `false` — callers should reject them as unknown rather
|
|
1924
|
+
* than as env-mismatched.
|
|
1925
|
+
*/
|
|
1926
|
+
function isToolAvailableIn(name, env) {
|
|
1927
|
+
const availability = getToolAvailability(name);
|
|
1928
|
+
if (availability === void 0) return false;
|
|
1929
|
+
if (availability === "both") return true;
|
|
1930
|
+
return availability === env;
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Filters a `DEBUG_TOOL_DEFINITIONS`-shaped list to those whose `availableIn`
|
|
1934
|
+
* matches the given env. Pure — preserves order; both Tier C ("both") and the
|
|
1935
|
+
* matching single-env tier pass through.
|
|
1936
|
+
*/
|
|
1937
|
+
function filterToolsByEnvironment(tools, env) {
|
|
1938
|
+
return tools.filter((t) => t.availableIn === "both" || t.availableIn === env);
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1484
1941
|
* Tool names that are available before any page attaches (bootstrap tier).
|
|
1485
1942
|
*
|
|
1486
1943
|
* `build_attach_url` — pure URL synthesis, no attach needed.
|
|
@@ -1781,11 +2238,13 @@ async function takeScreenshot(connection) {
|
|
|
1781
2238
|
* The JS probe injected via `Runtime.evaluate`. It reads:
|
|
1782
2239
|
* 1. `env(safe-area-inset-*)` via a temporary element with padding set to
|
|
1783
2240
|
* those CSS env vars, then `getComputedStyle`.
|
|
1784
|
-
* 2.
|
|
1785
|
-
*
|
|
1786
|
-
*
|
|
1787
|
-
*
|
|
1788
|
-
*
|
|
2241
|
+
* 2. SDK insets via a priority chain so the SAME probe works on both relay
|
|
2242
|
+
* (real device) and mock (devtools panel page):
|
|
2243
|
+
* a. `window.__sdk.SafeAreaInsets.get()` — dogfood bundle on real device.
|
|
2244
|
+
* b. `window.__sdk.getSafeAreaInsets()` — dogfood bundle (deprecated).
|
|
2245
|
+
* c. `window.__ait.state.safeAreaInsets` — devtools mock state (mock env).
|
|
2246
|
+
* The probe records `sdkInsetsSource` = `'window.__sdk'` | `'window.__ait'`
|
|
2247
|
+
* | `null`. If all paths fail the result carries `sdkInsetsError`.
|
|
1789
2248
|
* 3. nav bar geometry: the SDK does not expose navBar height as a standalone
|
|
1790
2249
|
* API — `.ait-navbar` DOM height is read as a cross-check, and
|
|
1791
2250
|
* `navBarHeightSource` records where it came from.
|
|
@@ -1793,9 +2252,15 @@ async function takeScreenshot(connection) {
|
|
|
1793
2252
|
*
|
|
1794
2253
|
* Returns a plain JSON-serialisable object so `returnByValue: true` works.
|
|
1795
2254
|
*
|
|
1796
|
-
* NOTE: This expression is evaluated in the page context on the real device
|
|
1797
|
-
* It does not mutate any page state — the
|
|
1798
|
-
* reading. No secret or auth token is read
|
|
2255
|
+
* NOTE: This expression is evaluated in the page context — on the real device
|
|
2256
|
+
* (relay) or on the mock panel page. It does not mutate any page state — the
|
|
2257
|
+
* temporary element is removed after reading. No secret or auth token is read
|
|
2258
|
+
* or returned.
|
|
2259
|
+
*
|
|
2260
|
+
* RFC #277 Tier C parity: the SAME probe string runs in both envs. Mock fidelity
|
|
2261
|
+
* comes from the panel's `applyViewport` / `computeSafeAreaInsets` correctly
|
|
2262
|
+
* setting `window.__ait.state.safeAreaInsets` (#275). When that is correct,
|
|
2263
|
+
* the cssEnv + sdkInsets pair returned here matches the relay's shape.
|
|
1799
2264
|
*/
|
|
1800
2265
|
const SAFE_AREA_PROBE_EXPRESSION = `
|
|
1801
2266
|
(function() {
|
|
@@ -1815,17 +2280,28 @@ const SAFE_AREA_PROBE_EXPRESSION = `
|
|
|
1815
2280
|
};
|
|
1816
2281
|
document.documentElement.removeChild(el);
|
|
1817
2282
|
var sdkInsets = null;
|
|
2283
|
+
var sdkInsetsSource = null;
|
|
1818
2284
|
var sdkInsetsError = undefined;
|
|
1819
2285
|
try {
|
|
1820
2286
|
var sdk = window.__sdk;
|
|
2287
|
+
var ait = window.__ait;
|
|
1821
2288
|
if (sdk && sdk.SafeAreaInsets && typeof sdk.SafeAreaInsets.get === 'function') {
|
|
1822
2289
|
sdkInsets = sdk.SafeAreaInsets.get();
|
|
2290
|
+
sdkInsetsSource = 'window.__sdk';
|
|
1823
2291
|
} else if (sdk && typeof sdk.getSafeAreaInsets === 'function') {
|
|
1824
2292
|
sdkInsets = sdk.getSafeAreaInsets();
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
2293
|
+
sdkInsetsSource = 'window.__sdk';
|
|
2294
|
+
} else if (ait && ait.state && ait.state.safeAreaInsets &&
|
|
2295
|
+
typeof ait.state.safeAreaInsets.top === 'number') {
|
|
2296
|
+
var s = ait.state.safeAreaInsets;
|
|
2297
|
+
sdkInsets = { top: s.top, bottom: s.bottom, left: s.left, right: s.right };
|
|
2298
|
+
sdkInsetsSource = 'window.__ait';
|
|
2299
|
+
} else if (!sdk && !ait) {
|
|
2300
|
+
sdkInsetsError = 'neither window.__sdk (relay) nor window.__ait (mock) available';
|
|
2301
|
+
} else if (sdk) {
|
|
1828
2302
|
sdkInsetsError = 'neither SafeAreaInsets.get nor getSafeAreaInsets found on window.__sdk';
|
|
2303
|
+
} else {
|
|
2304
|
+
sdkInsetsError = 'window.__ait.state.safeAreaInsets is missing or malformed';
|
|
1829
2305
|
}
|
|
1830
2306
|
} catch(e) {
|
|
1831
2307
|
sdkInsetsError = String(e && e.message || e);
|
|
@@ -1842,6 +2318,7 @@ const SAFE_AREA_PROBE_EXPRESSION = `
|
|
|
1842
2318
|
var result = {
|
|
1843
2319
|
cssEnv: cssEnv,
|
|
1844
2320
|
sdkInsets: sdkInsets,
|
|
2321
|
+
sdkInsetsSource: sdkInsetsSource,
|
|
1845
2322
|
navBarHeight: navBarHeight,
|
|
1846
2323
|
navBarHeightSource: navBarHeightSource,
|
|
1847
2324
|
innerWidth: window.innerWidth,
|
|
@@ -1858,9 +2335,11 @@ const SAFE_AREA_PROBE_EXPRESSION = `
|
|
|
1858
2335
|
* The probe returns a JSON string (because `returnByValue:true` with a plain
|
|
1859
2336
|
* object works unreliably across Chii relay versions — stringifying is safer).
|
|
1860
2337
|
*
|
|
2338
|
+
* `source` is supplied by the caller (`measureSafeArea`) from the env SSoT.
|
|
2339
|
+
*
|
|
1861
2340
|
* Throws if the result is missing, contains an exception, or cannot be parsed.
|
|
1862
2341
|
*/
|
|
1863
|
-
function normalizeSafeAreaResult(rawValue) {
|
|
2342
|
+
function normalizeSafeAreaResult(rawValue, source) {
|
|
1864
2343
|
if (typeof rawValue !== "string") throw new Error(`measure_safe_area: probe returned unexpected type "${typeof rawValue}" — expected JSON string`);
|
|
1865
2344
|
let parsed;
|
|
1866
2345
|
try {
|
|
@@ -1889,6 +2368,7 @@ function normalizeSafeAreaResult(rawValue) {
|
|
|
1889
2368
|
left: 0
|
|
1890
2369
|
};
|
|
1891
2370
|
const sdkInsets = requireInsets("sdkInsets");
|
|
2371
|
+
const sdkInsetsSource = obj.sdkInsetsSource === "window.__sdk" || obj.sdkInsetsSource === "window.__ait" ? obj.sdkInsetsSource : null;
|
|
1892
2372
|
const sdkInsetsError = typeof obj.sdkInsetsError === "string" ? obj.sdkInsetsError : void 0;
|
|
1893
2373
|
const navBarHeight = typeof obj.navBarHeight === "number" ? obj.navBarHeight : null;
|
|
1894
2374
|
const navBarHeightSource = typeof obj.navBarHeightSource === "string" ? obj.navBarHeightSource : "not-exposed-by-sdk";
|
|
@@ -1897,8 +2377,10 @@ function normalizeSafeAreaResult(rawValue) {
|
|
|
1897
2377
|
const devicePixelRatio = typeof obj.devicePixelRatio === "number" ? obj.devicePixelRatio : 1;
|
|
1898
2378
|
const userAgent = typeof obj.userAgent === "string" ? obj.userAgent : "";
|
|
1899
2379
|
return {
|
|
2380
|
+
source,
|
|
1900
2381
|
cssEnv,
|
|
1901
2382
|
sdkInsets,
|
|
2383
|
+
sdkInsetsSource,
|
|
1902
2384
|
...sdkInsetsError !== void 0 ? { sdkInsetsError } : {},
|
|
1903
2385
|
navBarHeight,
|
|
1904
2386
|
navBarHeightSource,
|
|
@@ -1912,9 +2394,16 @@ function normalizeSafeAreaResult(rawValue) {
|
|
|
1912
2394
|
* Runs the safe-area probe on the attached page and returns a normalized
|
|
1913
2395
|
* `SafeAreaMeasurement`. Read-only — does not mutate page state.
|
|
1914
2396
|
*
|
|
2397
|
+
* `source` is supplied by the caller from the env detection SSoT (see
|
|
2398
|
+
* `src/mcp/environment.ts`). The same `Runtime.evaluate` call runs in both
|
|
2399
|
+
* envs — the probe expression tries `window.__sdk` first (relay) then
|
|
2400
|
+
* `window.__ait` (mock), so mock fidelity is enforced by the panel's
|
|
2401
|
+
* `applyViewport`/`computeSafeAreaInsets` keeping `__ait.state.safeAreaInsets`
|
|
2402
|
+
* correct (RFC #277 Tier C parity, #275 model).
|
|
2403
|
+
*
|
|
1915
2404
|
* Throws on CDP error, probe exception, or result parse failure.
|
|
1916
2405
|
*/
|
|
1917
|
-
async function measureSafeArea(connection) {
|
|
2406
|
+
async function measureSafeArea(connection, source) {
|
|
1918
2407
|
const result = await connection.send("Runtime.evaluate", {
|
|
1919
2408
|
expression: SAFE_AREA_PROBE_EXPRESSION,
|
|
1920
2409
|
returnByValue: true,
|
|
@@ -1924,7 +2413,7 @@ async function measureSafeArea(connection) {
|
|
|
1924
2413
|
const msg = result.exceptionDetails.exception?.description ?? result.exceptionDetails.text ?? "Runtime.evaluate threw an exception";
|
|
1925
2414
|
throw new Error(`measure_safe_area: probe threw — ${msg}`);
|
|
1926
2415
|
}
|
|
1927
|
-
return normalizeSafeAreaResult(result.result.value);
|
|
2416
|
+
return normalizeSafeAreaResult(result.result.value, source);
|
|
1928
2417
|
}
|
|
1929
2418
|
/**
|
|
1930
2419
|
* Evaluates an arbitrary JS expression on the attached page via
|
|
@@ -2339,6 +2828,65 @@ async function printAttachBanner(input) {
|
|
|
2339
2828
|
* Node-only.
|
|
2340
2829
|
*/
|
|
2341
2830
|
/**
|
|
2831
|
+
* Parses `_deploymentId` from the query string of a scheme URL.
|
|
2832
|
+
*
|
|
2833
|
+
* Returns `null` when the param is absent or empty — callers treat that as
|
|
2834
|
+
* "no deploymentId filter; match on presence only" and fall back to the
|
|
2835
|
+
* original `attachedPages.length > 0` condition.
|
|
2836
|
+
*
|
|
2837
|
+
* SECRET-HANDLING: deploymentId is a public identifier and may appear in
|
|
2838
|
+
* debug output. Never confuse it with TOTP secrets or relay tunnel URLs.
|
|
2839
|
+
*/
|
|
2840
|
+
function extractDeploymentId(schemeUrl) {
|
|
2841
|
+
try {
|
|
2842
|
+
const qIndex = schemeUrl.indexOf("?");
|
|
2843
|
+
if (qIndex === -1) return null;
|
|
2844
|
+
const id = new URLSearchParams(schemeUrl.slice(qIndex + 1)).get("_deploymentId");
|
|
2845
|
+
return id && id.length > 0 ? id : null;
|
|
2846
|
+
} catch {
|
|
2847
|
+
return null;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Waits for the first target matching `filterFn` to attach, using the
|
|
2852
|
+
* event-driven `waitForFirstTarget()` on `ChiiCdpConnection` instances, or
|
|
2853
|
+
* falling back to a polling loop for generic `CdpConnection` fakes (tests).
|
|
2854
|
+
*
|
|
2855
|
+
* This eliminates the polling-only race that previously caused `wait_for_attach`
|
|
2856
|
+
* to resolve before the relay had observed the first inbound CDP message from
|
|
2857
|
+
* the phone.
|
|
2858
|
+
*
|
|
2859
|
+
* @param connection - The CDP connection (production or fake).
|
|
2860
|
+
* @param filterFn - Resolves when this predicate is satisfied.
|
|
2861
|
+
* @param timeoutMs - Maximum wait time in ms.
|
|
2862
|
+
* @param pollIntervalMs - Fallback poll interval for non-ChiiCdpConnection.
|
|
2863
|
+
*/
|
|
2864
|
+
function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs = 1e3) {
|
|
2865
|
+
if (connection instanceof ChiiCdpConnection) return connection.waitForFirstTarget(filterFn, timeoutMs, pollIntervalMs);
|
|
2866
|
+
return new Promise((resolve, reject) => {
|
|
2867
|
+
const deadline = Date.now() + timeoutMs;
|
|
2868
|
+
let settled = false;
|
|
2869
|
+
const poll = setInterval(() => {
|
|
2870
|
+
const targets = connection.listTargets();
|
|
2871
|
+
if (filterFn(targets)) {
|
|
2872
|
+
settled = true;
|
|
2873
|
+
clearInterval(poll);
|
|
2874
|
+
resolve(targets);
|
|
2875
|
+
} else if (Date.now() >= deadline) {
|
|
2876
|
+
settled = true;
|
|
2877
|
+
clearInterval(poll);
|
|
2878
|
+
reject(/* @__PURE__ */ new Error(`waitForAttachWithEvents: 타임아웃 (${timeoutMs}ms)`));
|
|
2879
|
+
}
|
|
2880
|
+
}, pollIntervalMs);
|
|
2881
|
+
const targets = connection.listTargets();
|
|
2882
|
+
if (!settled && filterFn(targets)) {
|
|
2883
|
+
settled = true;
|
|
2884
|
+
clearInterval(poll);
|
|
2885
|
+
resolve(targets);
|
|
2886
|
+
}
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2342
2890
|
* Builds the debug-mode MCP server around an injected CDP connection + AIT
|
|
2343
2891
|
* source + tunnel status getter. Pure wiring — does not start a relay or
|
|
2344
2892
|
* tunnel, which is what makes the tool surface unit-testable.
|
|
@@ -2351,13 +2899,18 @@ async function printAttachBanner(input) {
|
|
|
2351
2899
|
* naturally via `enableDomains`). The tier only controls visibility.
|
|
2352
2900
|
*/
|
|
2353
2901
|
function createDebugServer(deps) {
|
|
2354
|
-
const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer } = deps;
|
|
2902
|
+
const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep } = deps;
|
|
2903
|
+
const resolveEnvironment = getEnvDep ?? (() => getEnvironment({ connection }));
|
|
2904
|
+
const resolveEnvironmentReason = getEnvReasonDep ?? (() => getEnvironmentReason({ connection }));
|
|
2355
2905
|
const server = new Server({
|
|
2356
2906
|
name: "ait-debug",
|
|
2357
|
-
version: "0.1.
|
|
2907
|
+
version: "0.1.43"
|
|
2358
2908
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
2359
2909
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
2360
|
-
|
|
2910
|
+
const env = resolveEnvironment();
|
|
2911
|
+
const attached = connection.listTargets().length > 0;
|
|
2912
|
+
const envFiltered = filterToolsByEnvironment(DEBUG_TOOL_DEFINITIONS, env);
|
|
2913
|
+
return { tools: attached ? envFiltered.map((tool) => ({ ...tool })) : envFiltered.filter((tool) => BOOTSTRAP_TOOL_NAMES.has(tool.name)).map((tool) => ({ ...tool })) };
|
|
2361
2914
|
});
|
|
2362
2915
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2363
2916
|
const name = request.params.name;
|
|
@@ -2368,6 +2921,18 @@ function createDebugServer(deps) {
|
|
|
2368
2921
|
}],
|
|
2369
2922
|
isError: true
|
|
2370
2923
|
};
|
|
2924
|
+
const env = resolveEnvironment();
|
|
2925
|
+
if (!isToolAvailableIn(name, env)) {
|
|
2926
|
+
const reason = `tool ${name} is available only in ${getToolAvailability(name)}. Current environment is ${env} (${resolveEnvironmentReason()}).`;
|
|
2927
|
+
process.stderr.write(`[ait-debug] tier-filter rejected ${name}: ${reason}\n`);
|
|
2928
|
+
return {
|
|
2929
|
+
content: [{
|
|
2930
|
+
type: "text",
|
|
2931
|
+
text: reason
|
|
2932
|
+
}],
|
|
2933
|
+
isError: true
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2371
2936
|
if (isAitToolName(name)) try {
|
|
2372
2937
|
await connection.enableDomains();
|
|
2373
2938
|
switch (name) {
|
|
@@ -2390,6 +2955,20 @@ function createDebugServer(deps) {
|
|
|
2390
2955
|
};
|
|
2391
2956
|
const waitForAttach = request.params.arguments?.wait_for_attach === true;
|
|
2392
2957
|
const openInBrowser = request.params.arguments?.open_in_browser !== false;
|
|
2958
|
+
const deploymentId = extractDeploymentId(schemeUrl);
|
|
2959
|
+
if (!deploymentId) process.stderr.write("[ait-debug] build_attach_url: no _deploymentId in scheme_url; matching on presence only\n");
|
|
2960
|
+
/** Returns true when the page list satisfies the attach condition. */
|
|
2961
|
+
const isMatchingPage = (pages) => {
|
|
2962
|
+
if (pages.length === 0) return false;
|
|
2963
|
+
if (deploymentId === null) return true;
|
|
2964
|
+
return pages.some((p) => p.url.includes(deploymentId));
|
|
2965
|
+
};
|
|
2966
|
+
/** Builds a timeout error message with diagnostic context. */
|
|
2967
|
+
const buildTimeoutError = (baseText, timeoutSec, observed) => {
|
|
2968
|
+
const observedUrls = observed.slice(0, 3).map((p) => p.url.slice(0, 80)).join(", ");
|
|
2969
|
+
const observedNote = observed.length > 0 ? ` — previously attached pages: [${observedUrls}]` : "";
|
|
2970
|
+
return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
|
|
2971
|
+
};
|
|
2393
2972
|
try {
|
|
2394
2973
|
const { attachUrl, relayUrl, authorityWarning } = buildAttachUrl(schemeUrl, getTunnelStatus());
|
|
2395
2974
|
const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
|
|
@@ -2402,22 +2981,19 @@ function createDebugServer(deps) {
|
|
|
2402
2981
|
type: "text",
|
|
2403
2982
|
text: shortText
|
|
2404
2983
|
}] };
|
|
2405
|
-
const POLL_INTERVAL_MS = 1e3;
|
|
2406
|
-
const TIMEOUT_MS = waitForAttachTimeoutMs;
|
|
2407
|
-
const deadline = Date.now() + TIMEOUT_MS;
|
|
2408
2984
|
let attachedPages = [];
|
|
2409
|
-
|
|
2985
|
+
try {
|
|
2986
|
+
attachedPages = await waitForAttachWithEvents(connection, isMatchingPage, waitForAttachTimeoutMs);
|
|
2987
|
+
} catch {
|
|
2410
2988
|
attachedPages = connection.listTargets();
|
|
2411
|
-
|
|
2412
|
-
|
|
2989
|
+
return {
|
|
2990
|
+
content: [{
|
|
2991
|
+
type: "text",
|
|
2992
|
+
text: buildTimeoutError(shortText, waitForAttachTimeoutMs / 1e3, attachedPages)
|
|
2993
|
+
}],
|
|
2994
|
+
isError: true
|
|
2995
|
+
};
|
|
2413
2996
|
}
|
|
2414
|
-
if (attachedPages.length === 0) return {
|
|
2415
|
-
content: [{
|
|
2416
|
-
type: "text",
|
|
2417
|
-
text: `${shortText}\n\nNo page attached within ${TIMEOUT_MS / 1e3}s — call list_pages to retry.`
|
|
2418
|
-
}],
|
|
2419
|
-
isError: true
|
|
2420
|
-
};
|
|
2421
2997
|
const pagesResult = listPages(connection, getTunnelStatus());
|
|
2422
2998
|
return { content: [{
|
|
2423
2999
|
type: "text",
|
|
@@ -2435,22 +3011,19 @@ function createDebugServer(deps) {
|
|
|
2435
3011
|
type: "text",
|
|
2436
3012
|
text: baseText
|
|
2437
3013
|
}] };
|
|
2438
|
-
const POLL_INTERVAL_MS_FB = 1e3;
|
|
2439
|
-
const TIMEOUT_MS_FB = waitForAttachTimeoutMs;
|
|
2440
|
-
const deadline2 = Date.now() + TIMEOUT_MS_FB;
|
|
2441
3014
|
let attachedPagesFb = [];
|
|
2442
|
-
|
|
3015
|
+
try {
|
|
3016
|
+
attachedPagesFb = await waitForAttachWithEvents(connection, isMatchingPage, waitForAttachTimeoutMs);
|
|
3017
|
+
} catch {
|
|
2443
3018
|
attachedPagesFb = connection.listTargets();
|
|
2444
|
-
|
|
2445
|
-
|
|
3019
|
+
return {
|
|
3020
|
+
content: [{
|
|
3021
|
+
type: "text",
|
|
3022
|
+
text: buildTimeoutError(baseText, waitForAttachTimeoutMs / 1e3, attachedPagesFb)
|
|
3023
|
+
}],
|
|
3024
|
+
isError: true
|
|
3025
|
+
};
|
|
2446
3026
|
}
|
|
2447
|
-
if (attachedPagesFb.length === 0) return {
|
|
2448
|
-
content: [{
|
|
2449
|
-
type: "text",
|
|
2450
|
-
text: `${baseText}\n\nNo page attached within ${TIMEOUT_MS_FB / 1e3}s — call list_pages to retry.`
|
|
2451
|
-
}],
|
|
2452
|
-
isError: true
|
|
2453
|
-
};
|
|
2454
3027
|
const pagesResultFb = listPages(connection, getTunnelStatus());
|
|
2455
3028
|
return { content: [{
|
|
2456
3029
|
type: "text",
|
|
@@ -2466,22 +3039,19 @@ function createDebugServer(deps) {
|
|
|
2466
3039
|
type: "text",
|
|
2467
3040
|
text: baseText
|
|
2468
3041
|
}] };
|
|
2469
|
-
const POLL_INTERVAL_MS = 1e3;
|
|
2470
|
-
const TIMEOUT_MS = waitForAttachTimeoutMs;
|
|
2471
|
-
const deadline = Date.now() + TIMEOUT_MS;
|
|
2472
3042
|
let attachedPages = [];
|
|
2473
|
-
|
|
3043
|
+
try {
|
|
3044
|
+
attachedPages = await waitForAttachWithEvents(connection, isMatchingPage, waitForAttachTimeoutMs);
|
|
3045
|
+
} catch {
|
|
2474
3046
|
attachedPages = connection.listTargets();
|
|
2475
|
-
|
|
2476
|
-
|
|
3047
|
+
return {
|
|
3048
|
+
content: [{
|
|
3049
|
+
type: "text",
|
|
3050
|
+
text: buildTimeoutError(baseText, waitForAttachTimeoutMs / 1e3, attachedPages)
|
|
3051
|
+
}],
|
|
3052
|
+
isError: true
|
|
3053
|
+
};
|
|
2477
3054
|
}
|
|
2478
|
-
if (attachedPages.length === 0) return {
|
|
2479
|
-
content: [{
|
|
2480
|
-
type: "text",
|
|
2481
|
-
text: `${baseText}\n\nNo page attached within ${TIMEOUT_MS / 1e3}s — call list_pages to retry.`
|
|
2482
|
-
}],
|
|
2483
|
-
isError: true
|
|
2484
|
-
};
|
|
2485
3055
|
const pagesResult = listPages(connection, getTunnelStatus());
|
|
2486
3056
|
return { content: [{
|
|
2487
3057
|
type: "text",
|
|
@@ -2495,7 +3065,12 @@ function createDebugServer(deps) {
|
|
|
2495
3065
|
await connection.enableDomains();
|
|
2496
3066
|
} catch (err) {
|
|
2497
3067
|
const message = err instanceof Error ? err.message : String(err);
|
|
2498
|
-
if (name === "list_pages")
|
|
3068
|
+
if (name === "list_pages") {
|
|
3069
|
+
if (connection instanceof ChiiCdpConnection) try {
|
|
3070
|
+
await connection.refreshTargets();
|
|
3071
|
+
} catch {}
|
|
3072
|
+
return jsonResult$1(listPages(connection, getTunnelStatus()));
|
|
3073
|
+
}
|
|
2499
3074
|
return {
|
|
2500
3075
|
content: [{
|
|
2501
3076
|
type: "text",
|
|
@@ -2512,7 +3087,11 @@ function createDebugServer(deps) {
|
|
|
2512
3087
|
return jsonResult$1({ exceptions: listExceptions(connection, typeof rawLimit === "number" && rawLimit > 0 ? rawLimit : 50) });
|
|
2513
3088
|
}
|
|
2514
3089
|
case "list_network_requests": return jsonResult$1(listNetworkRequests(connection));
|
|
2515
|
-
case "list_pages":
|
|
3090
|
+
case "list_pages":
|
|
3091
|
+
if (connection instanceof ChiiCdpConnection) try {
|
|
3092
|
+
await connection.refreshTargets();
|
|
3093
|
+
} catch {}
|
|
3094
|
+
return jsonResult$1(listPages(connection, getTunnelStatus()));
|
|
2516
3095
|
case "get_dom_document": return jsonResult$1(await getDomDocument(connection));
|
|
2517
3096
|
case "take_snapshot": return jsonResult$1(await takeSnapshot(connection));
|
|
2518
3097
|
case "take_screenshot": {
|
|
@@ -2523,7 +3102,7 @@ function createDebugServer(deps) {
|
|
|
2523
3102
|
mimeType: shot.mimeType
|
|
2524
3103
|
}] };
|
|
2525
3104
|
}
|
|
2526
|
-
case "measure_safe_area": return jsonResult$1(await measureSafeArea(connection));
|
|
3105
|
+
case "measure_safe_area": return jsonResult$1(await measureSafeArea(connection, resolveEnvironment()));
|
|
2527
3106
|
case "evaluate": {
|
|
2528
3107
|
const expression = request.params.arguments?.expression;
|
|
2529
3108
|
if (typeof expression !== "string" || expression === "") return {
|
|
@@ -2570,11 +3149,22 @@ function unknownTool(name) {
|
|
|
2570
3149
|
isError: true
|
|
2571
3150
|
};
|
|
2572
3151
|
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Detects whether an error is a relay/websocket disconnect error.
|
|
3154
|
+
* These are distinguished from "no page attached yet" errors because they
|
|
3155
|
+
* require enableDomains() to be called again (re-establish the websocket),
|
|
3156
|
+
* not just waiting for a target to appear.
|
|
3157
|
+
*/
|
|
3158
|
+
function isDisconnectError(err) {
|
|
3159
|
+
if (!(err instanceof Error)) return false;
|
|
3160
|
+
const msg = err.message;
|
|
3161
|
+
return msg.includes("relay에 연결되어 있지 않습니다") || msg.includes("relay WebSocket") || msg.includes("replaced-by-new-attach") || msg.includes("Chii relay connection closed");
|
|
3162
|
+
}
|
|
2573
3163
|
function errorResult(err, name) {
|
|
2574
3164
|
return {
|
|
2575
3165
|
content: [{
|
|
2576
3166
|
type: "text",
|
|
2577
|
-
text: `${name} failed: ${err instanceof Error ? err.message : String(err)}\nCall list_pages to confirm a mini-app has attached over the relay
|
|
3167
|
+
text: `${name} failed: ${err instanceof Error ? err.message : String(err)}${isDisconnectError(err) ? "\n\nrelay 연결이 끊겼습니다. list_pages → enableDomains() 재호출로 재연결하세요. 폰이 백그라운드로 내려갔거나 미니앱이 종료됐을 수 있습니다." : "\nCall list_pages to confirm a mini-app has attached over the relay."}`
|
|
2578
3168
|
}],
|
|
2579
3169
|
isError: true
|
|
2580
3170
|
};
|
|
@@ -2588,19 +3178,28 @@ function errorResult(err, name) {
|
|
|
2588
3178
|
* `server.sendToolListChanged()` exactly once — on the first transition — then
|
|
2589
3179
|
* clears itself. Shutdown calls `stop()` to clear the interval.
|
|
2590
3180
|
*
|
|
3181
|
+
* `onFirstAttach` is called once on the 0→N transition (or immediately when
|
|
3182
|
+
* already attached). Use this to trigger side-effects such as auto-opening
|
|
3183
|
+
* Chrome DevTools (issue #282). The callback is optional; omitting it preserves
|
|
3184
|
+
* the previous behaviour exactly.
|
|
3185
|
+
*
|
|
2591
3186
|
* SECRET-HANDLING: target `id`/`title`/`url` are not written to any log here.
|
|
2592
3187
|
* Only an attach-detected stderr line is emitted (no target details).
|
|
2593
3188
|
*
|
|
2594
3189
|
* @returns `stop` — call this during shutdown to clear the interval.
|
|
2595
3190
|
*/
|
|
2596
|
-
function startAttachWatcher(connection, server, intervalMs = 1e3) {
|
|
3191
|
+
function startAttachWatcher(connection, server, intervalMs = 1e3, onFirstAttach) {
|
|
2597
3192
|
let wasAttached = connection.listTargets().length > 0;
|
|
2598
|
-
if (wasAttached)
|
|
3193
|
+
if (wasAttached) {
|
|
3194
|
+
server.sendToolListChanged();
|
|
3195
|
+
onFirstAttach?.();
|
|
3196
|
+
}
|
|
2599
3197
|
const handle = setInterval(() => {
|
|
2600
3198
|
const isAttached = connection.listTargets().length > 0;
|
|
2601
3199
|
if (!wasAttached && isAttached) {
|
|
2602
3200
|
wasAttached = true;
|
|
2603
3201
|
server.sendToolListChanged();
|
|
3202
|
+
onFirstAttach?.();
|
|
2604
3203
|
clearInterval(handle);
|
|
2605
3204
|
}
|
|
2606
3205
|
}, intervalMs);
|
|
@@ -2640,6 +3239,7 @@ function buildRelayVerifyAuth() {
|
|
|
2640
3239
|
* 4. expose the debug tools backed by a `ChiiCdpConnection` + `ChiiAitSource`.
|
|
2641
3240
|
*/
|
|
2642
3241
|
async function runDebugServer(options = {}) {
|
|
3242
|
+
const lockHandle = acquireLock();
|
|
2643
3243
|
const relayPort = options.relayPort ?? 0;
|
|
2644
3244
|
const verifyAuth = buildRelayVerifyAuth();
|
|
2645
3245
|
const totpEnabled = verifyAuth !== void 0;
|
|
@@ -2659,6 +3259,7 @@ async function runDebugServer(options = {}) {
|
|
|
2659
3259
|
up: true,
|
|
2660
3260
|
wssUrl: t.wssUrl
|
|
2661
3261
|
};
|
|
3262
|
+
lockHandle.updateWssUrl(t.wssUrl);
|
|
2662
3263
|
return printAttachBanner({
|
|
2663
3264
|
wssUrl: t.wssUrl,
|
|
2664
3265
|
totpEnabled
|
|
@@ -2677,6 +3278,7 @@ async function runDebugServer(options = {}) {
|
|
|
2677
3278
|
const message = err instanceof Error ? err.message : String(err);
|
|
2678
3279
|
process.stderr.write(`[ait-debug] QR HTTP 서버 시작 실패 (text QR fallback 사용): ${message}\n`);
|
|
2679
3280
|
}
|
|
3281
|
+
const devtoolsOpener = new AutoDevtoolsOpener();
|
|
2680
3282
|
const server = createDebugServer({
|
|
2681
3283
|
connection,
|
|
2682
3284
|
aitSource,
|
|
@@ -2697,6 +3299,7 @@ async function runDebugServer(options = {}) {
|
|
|
2697
3299
|
relay.close();
|
|
2698
3300
|
server.close();
|
|
2699
3301
|
qrServer?.close();
|
|
3302
|
+
lockHandle.release();
|
|
2700
3303
|
};
|
|
2701
3304
|
process.once("SIGINT", shutdown);
|
|
2702
3305
|
process.once("SIGTERM", shutdown);
|
|
@@ -2706,6 +3309,7 @@ async function runDebugServer(options = {}) {
|
|
|
2706
3309
|
closed = true;
|
|
2707
3310
|
attachWatcher?.stop();
|
|
2708
3311
|
tunnel?.stop();
|
|
3312
|
+
lockHandle.release();
|
|
2709
3313
|
}
|
|
2710
3314
|
});
|
|
2711
3315
|
process.on("uncaughtException", (err) => {
|
|
@@ -2719,7 +3323,9 @@ async function runDebugServer(options = {}) {
|
|
|
2719
3323
|
process.exit(1);
|
|
2720
3324
|
});
|
|
2721
3325
|
await server.connect(transport);
|
|
2722
|
-
attachWatcher = startAttachWatcher(connection, server)
|
|
3326
|
+
attachWatcher = startAttachWatcher(connection, server, 1e3, () => {
|
|
3327
|
+
devtoolsOpener.open(tunnelStatus.wssUrl, getEnvironment({ connection }));
|
|
3328
|
+
});
|
|
2723
3329
|
}
|
|
2724
3330
|
/**
|
|
2725
3331
|
* Boots the local-browser debug stack and serves it over stdio:
|
|
@@ -2740,6 +3346,7 @@ async function runDebugServer(options = {}) {
|
|
|
2740
3346
|
* expected and noted in the PR as an explicit out-of-scope follow-up.
|
|
2741
3347
|
*/
|
|
2742
3348
|
async function runLocalDebugServer(options = {}) {
|
|
3349
|
+
const lockHandle = acquireLock();
|
|
2743
3350
|
const chromium = await launchChromium({
|
|
2744
3351
|
port: options.cdpPort ?? 0,
|
|
2745
3352
|
devUrl: options.devUrl ?? process.env.AIT_DEVTOOLS_URL ?? "http://localhost:5173"
|
|
@@ -2766,6 +3373,7 @@ async function runLocalDebugServer(options = {}) {
|
|
|
2766
3373
|
connection.close();
|
|
2767
3374
|
chromium.stop();
|
|
2768
3375
|
server.close();
|
|
3376
|
+
lockHandle.release();
|
|
2769
3377
|
};
|
|
2770
3378
|
process.once("SIGINT", shutdown);
|
|
2771
3379
|
process.once("SIGTERM", shutdown);
|
|
@@ -2775,6 +3383,7 @@ async function runLocalDebugServer(options = {}) {
|
|
|
2775
3383
|
closed = true;
|
|
2776
3384
|
attachWatcher?.stop();
|
|
2777
3385
|
chromium.stop();
|
|
3386
|
+
lockHandle.release();
|
|
2778
3387
|
}
|
|
2779
3388
|
});
|
|
2780
3389
|
process.on("uncaughtException", (err) => {
|
|
@@ -2863,7 +3472,13 @@ var HttpAitSource = class {
|
|
|
2863
3472
|
* }
|
|
2864
3473
|
* }
|
|
2865
3474
|
*/
|
|
2866
|
-
/**
|
|
3475
|
+
/**
|
|
3476
|
+
* Tool descriptors served by the dev-mode server.
|
|
3477
|
+
*
|
|
3478
|
+
* All dev-mode tools are Tier C (both envs) per RFC #277 — the dev-mode server
|
|
3479
|
+
* itself is the mock-side embodiment of those Tier C tools. `availableIn` is
|
|
3480
|
+
* declared so the surface stays consistent with the debug-mode registry.
|
|
3481
|
+
*/
|
|
2867
3482
|
const DEV_TOOL_DEFINITIONS = [
|
|
2868
3483
|
{
|
|
2869
3484
|
name: "AIT.getMockState",
|
|
@@ -2872,7 +3487,8 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
2872
3487
|
type: "object",
|
|
2873
3488
|
properties: {},
|
|
2874
3489
|
required: []
|
|
2875
|
-
}
|
|
3490
|
+
},
|
|
3491
|
+
availableIn: "both"
|
|
2876
3492
|
},
|
|
2877
3493
|
{
|
|
2878
3494
|
name: "AIT.getOperationalEnvironment",
|
|
@@ -2881,7 +3497,8 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
2881
3497
|
type: "object",
|
|
2882
3498
|
properties: {},
|
|
2883
3499
|
required: []
|
|
2884
|
-
}
|
|
3500
|
+
},
|
|
3501
|
+
availableIn: "both"
|
|
2885
3502
|
},
|
|
2886
3503
|
{
|
|
2887
3504
|
name: "AIT.getSdkCallHistory",
|
|
@@ -2890,7 +3507,8 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
2890
3507
|
type: "object",
|
|
2891
3508
|
properties: {},
|
|
2892
3509
|
required: []
|
|
2893
|
-
}
|
|
3510
|
+
},
|
|
3511
|
+
availableIn: "both"
|
|
2894
3512
|
},
|
|
2895
3513
|
{
|
|
2896
3514
|
name: "devtools_get_mock_state",
|
|
@@ -2899,7 +3517,8 @@ const DEV_TOOL_DEFINITIONS = [
|
|
|
2899
3517
|
type: "object",
|
|
2900
3518
|
properties: {},
|
|
2901
3519
|
required: []
|
|
2902
|
-
}
|
|
3520
|
+
},
|
|
3521
|
+
availableIn: "both"
|
|
2903
3522
|
}
|
|
2904
3523
|
];
|
|
2905
3524
|
const DEV_TOOL_NAMES = new Set(DEV_TOOL_DEFINITIONS.map((t) => t.name));
|
|
@@ -2909,7 +3528,7 @@ function createDevServer(deps = {}) {
|
|
|
2909
3528
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
2910
3529
|
const server = new Server({
|
|
2911
3530
|
name: "ait-devtools",
|
|
2912
|
-
version: "0.1.
|
|
3531
|
+
version: "0.1.43"
|
|
2913
3532
|
}, { capabilities: { tools: {} } });
|
|
2914
3533
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
2915
3534
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|