@narumitw/pi-chrome-devtools 0.52.3 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -9
- package/dist/chunks/{browser-settings-menu-VWCNG4HB.js → browser-settings-menu-REIU7C3Q.js} +83 -7
- package/dist/chunks/browser-settings-menu-REIU7C3Q.js.map +7 -0
- package/dist/chunks/chunk-GTTYS5GT.js +384 -0
- package/dist/chunks/chunk-GTTYS5GT.js.map +7 -0
- package/dist/chunks/{chunk-RH6KESL3.js → chunk-SRT3UUIP.js} +473 -574
- package/dist/chunks/chunk-SRT3UUIP.js.map +7 -0
- package/dist/chunks/chunk-YJVYJ2L2.js +469 -0
- package/dist/chunks/chunk-YJVYJ2L2.js.map +7 -0
- package/dist/chunks/{menu-PLD3S3EO.js → menu-H5HMVGHI.js} +72 -39
- package/dist/chunks/menu-H5HMVGHI.js.map +7 -0
- package/dist/chunks/tools-6P7EUX37.js +964 -0
- package/dist/chunks/tools-6P7EUX37.js.map +7 -0
- package/dist/index.ts +166 -199
- package/dist/index.ts.map +4 -4
- package/package.json +4 -3
- package/src/browser-manager.ts +408 -169
- package/src/browser-settings-menu.ts +100 -5
- package/src/cdp-client.ts +347 -90
- package/src/chrome-devtools.ts +58 -11
- package/src/lazy-tools.ts +37 -4
- package/src/menu.ts +80 -39
- package/src/render.ts +12 -1
- package/src/runtime.ts +92 -0
- package/src/settings.ts +47 -10
- package/src/tool-names.ts +16 -1
- package/src/tool-selector.ts +85 -44
- package/src/tools.ts +91 -15
- package/src/webmcp/discovery.ts +235 -0
- package/src/webmcp/policy.ts +409 -0
- package/src/webmcp/protocol.ts +426 -0
- package/src/webmcp/tools.ts +190 -0
- package/dist/chunks/browser-settings-menu-VWCNG4HB.js.map +0 -7
- package/dist/chunks/chunk-RH6KESL3.js.map +0 -7
- package/dist/chunks/menu-PLD3S3EO.js.map +0 -7
|
@@ -2,13 +2,24 @@
|
|
|
2
2
|
// @ts-nocheck -- the generated entry uses a .ts extension for Pi's Jiti loader.
|
|
3
3
|
|
|
4
4
|
// src/tool-names.ts
|
|
5
|
-
var
|
|
5
|
+
var CORE_CHROME_DEVTOOLS_TOOL_NAMES = [
|
|
6
6
|
"chrome_devtools_list_pages",
|
|
7
7
|
"chrome_devtools_select_page",
|
|
8
8
|
"chrome_devtools_navigate",
|
|
9
9
|
"chrome_devtools_evaluate",
|
|
10
10
|
"chrome_devtools_screenshot"
|
|
11
11
|
];
|
|
12
|
+
var WEBMCP_TOOL_NAMES = [
|
|
13
|
+
"chrome_devtools_webmcp_list_tools",
|
|
14
|
+
"chrome_devtools_webmcp_call_tool"
|
|
15
|
+
];
|
|
16
|
+
var CHROME_DEVTOOLS_TOOL_NAMES = [
|
|
17
|
+
...CORE_CHROME_DEVTOOLS_TOOL_NAMES,
|
|
18
|
+
...WEBMCP_TOOL_NAMES
|
|
19
|
+
];
|
|
20
|
+
function isWebMcpToolName(value) {
|
|
21
|
+
return WEBMCP_TOOL_NAMES.includes(value);
|
|
22
|
+
}
|
|
12
23
|
|
|
13
24
|
// src/settings.ts
|
|
14
25
|
import { randomUUID } from "node:crypto";
|
|
@@ -82,7 +93,7 @@ async function loadSettings(options = {}) {
|
|
|
82
93
|
const userBrowser = user.normalized?.browser ?? {};
|
|
83
94
|
const effectiveBrowser = resolveEffectiveBrowser(userBrowser, project.normalized?.browser);
|
|
84
95
|
const settings = resolveSettings(user.normalized, project.normalized, effectiveBrowser);
|
|
85
|
-
const recognized = settings.tools !== void 0 || user.normalized?.browser !== void 0 || project.normalized?.browser !== void 0;
|
|
96
|
+
const recognized = settings.tools !== void 0 || user.normalized?.browser !== void 0 || project.normalized?.browser !== void 0 || user.normalized?.webmcp !== void 0;
|
|
86
97
|
const invalidReasons = [user, project].filter((result) => result.kind === "invalid").map((result) => result.reason).filter((reason) => Boolean(reason));
|
|
87
98
|
const userFile = user.kind === "invalid" ? {
|
|
88
99
|
kind: "invalid",
|
|
@@ -90,6 +101,7 @@ async function loadSettings(options = {}) {
|
|
|
90
101
|
} : { kind: user.kind };
|
|
91
102
|
const base = {
|
|
92
103
|
effectiveBrowser,
|
|
104
|
+
effectiveWebMcpEnabled: settings.webmcp.enabled,
|
|
93
105
|
paths,
|
|
94
106
|
userFile,
|
|
95
107
|
warnings,
|
|
@@ -104,7 +116,8 @@ async function loadSettings(options = {}) {
|
|
|
104
116
|
function resolveSettings(user, _project, browser) {
|
|
105
117
|
return {
|
|
106
118
|
...user?.tools ? { tools: user.tools, updatedAt: user.updatedAt } : {},
|
|
107
|
-
browser
|
|
119
|
+
browser,
|
|
120
|
+
webmcp: { enabled: user?.webmcp?.enabled ?? false }
|
|
108
121
|
};
|
|
109
122
|
}
|
|
110
123
|
function resolveEffectiveBrowser(user, project) {
|
|
@@ -165,7 +178,7 @@ async function readSettingsDocument(filePath, scope, cwd) {
|
|
|
165
178
|
const reason = `${filePath}: expected a JSON object`;
|
|
166
179
|
return { kind: "invalid", reason, warnings: [`Chrome DevTools settings ignored: ${reason}`] };
|
|
167
180
|
}
|
|
168
|
-
const scopeWarnings =
|
|
181
|
+
const scopeWarnings = projectOwnedSettingsWarnings(parsed, scope, filePath);
|
|
169
182
|
try {
|
|
170
183
|
const normalized = await normalizeSettingsDocument(parsed, scope, filePath, cwd);
|
|
171
184
|
return { kind: "valid", document: { ...parsed }, normalized, warnings: scopeWarnings };
|
|
@@ -178,12 +191,18 @@ async function readSettingsDocument(filePath, scope, cwd) {
|
|
|
178
191
|
};
|
|
179
192
|
}
|
|
180
193
|
}
|
|
181
|
-
function
|
|
182
|
-
if (scope !== "project"
|
|
183
|
-
const browser = document.browser;
|
|
184
|
-
|
|
194
|
+
function projectOwnedSettingsWarnings(document, scope, filePath) {
|
|
195
|
+
if (scope !== "project") return [];
|
|
196
|
+
const browser = isRecord(document.browser) ? document.browser : void 0;
|
|
197
|
+
const warnings = browser ? ["endpoint", "autoLaunch", "executablePath"].filter((field) => browser[field] !== void 0).map(
|
|
185
198
|
(field) => `Chrome DevTools project browser.${field} ignored in ${filePath}; configure this machine-owned setting in ${settingsFilePath()}.`
|
|
186
|
-
);
|
|
199
|
+
) : [];
|
|
200
|
+
if (document.webmcp !== void 0) {
|
|
201
|
+
warnings.push(
|
|
202
|
+
`Chrome DevTools project webmcp settings ignored in ${filePath}; only the user settings file may enable experimental WebMCP.`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return warnings;
|
|
187
206
|
}
|
|
188
207
|
async function normalizeSettingsDocument(document, scope, filePath, cwd) {
|
|
189
208
|
const normalized = {};
|
|
@@ -201,6 +220,12 @@ async function normalizeSettingsDocument(document, scope, filePath, cwd) {
|
|
|
201
220
|
if (!isRecord(document.browser)) throw new Error("expected browser to be an object");
|
|
202
221
|
normalized.browser = await normalizeBrowserSection(document.browser, scope, filePath, cwd);
|
|
203
222
|
}
|
|
223
|
+
if (scope === "user" && document.webmcp !== void 0) {
|
|
224
|
+
if (!isRecord(document.webmcp) || typeof document.webmcp.enabled !== "boolean") {
|
|
225
|
+
throw new Error("expected webmcp.enabled to be a boolean");
|
|
226
|
+
}
|
|
227
|
+
normalized.webmcp = { enabled: document.webmcp.enabled };
|
|
228
|
+
}
|
|
204
229
|
return normalized;
|
|
205
230
|
}
|
|
206
231
|
async function normalizeBrowserSection(browser, scope, _filePath, cwd) {
|
|
@@ -337,6 +362,18 @@ function saveSettings(settings, operations = {}) {
|
|
|
337
362
|
operations
|
|
338
363
|
);
|
|
339
364
|
}
|
|
365
|
+
function saveWebMcpSettings(enabled, operations = {}) {
|
|
366
|
+
return queueSettingsMutation(
|
|
367
|
+
(current) => ({
|
|
368
|
+
...current,
|
|
369
|
+
webmcp: {
|
|
370
|
+
...isRecord(current.webmcp) ? current.webmcp : {},
|
|
371
|
+
enabled
|
|
372
|
+
}
|
|
373
|
+
}),
|
|
374
|
+
operations
|
|
375
|
+
);
|
|
376
|
+
}
|
|
340
377
|
function saveBrowserSettings(patch, operations = {}) {
|
|
341
378
|
return queueSettingsMutation(async (current) => {
|
|
342
379
|
const browser = isRecord(current.browser) ? { ...current.browser } : {};
|
|
@@ -446,6 +483,72 @@ var state = {
|
|
|
446
483
|
sessionGeneration: 0,
|
|
447
484
|
sessionController: new AbortController()
|
|
448
485
|
};
|
|
486
|
+
var webMcpRuntimeByOwner = /* @__PURE__ */ new WeakMap();
|
|
487
|
+
function setWebMcpSessionOwner(owner) {
|
|
488
|
+
const runtime = webMcpRuntime(owner);
|
|
489
|
+
runtime.enabled = false;
|
|
490
|
+
runtime.sessionGeneration += 1;
|
|
491
|
+
}
|
|
492
|
+
function beginWebMcpOperation(owner, toolSignal) {
|
|
493
|
+
const runtime = webMcpRuntime(owner);
|
|
494
|
+
const controller = new AbortController();
|
|
495
|
+
runtime.controllers.add(controller);
|
|
496
|
+
const signals = [controller.signal, runtime.controller.signal];
|
|
497
|
+
if (toolSignal) signals.push(toolSignal);
|
|
498
|
+
const signal = AbortSignal.any(signals);
|
|
499
|
+
return {
|
|
500
|
+
signal,
|
|
501
|
+
owner,
|
|
502
|
+
sessionGeneration: runtime.sessionGeneration,
|
|
503
|
+
webMcpGeneration: runtime.generation,
|
|
504
|
+
dispose() {
|
|
505
|
+
runtime.controllers.delete(controller);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function invalidateWebMcpOperations(owner, reason) {
|
|
510
|
+
const runtime = webMcpRuntime(owner);
|
|
511
|
+
runtime.generation += 1;
|
|
512
|
+
const controllers = [...runtime.controllers];
|
|
513
|
+
runtime.controllers.clear();
|
|
514
|
+
const error = new DOMException(reason, "AbortError");
|
|
515
|
+
runtime.controller.abort(error);
|
|
516
|
+
runtime.controller = new AbortController();
|
|
517
|
+
for (const controller of controllers) controller.abort(error);
|
|
518
|
+
}
|
|
519
|
+
function currentWebMcpGeneration(owner) {
|
|
520
|
+
return webMcpRuntime(owner).generation;
|
|
521
|
+
}
|
|
522
|
+
function webMcpSessionSignal(owner) {
|
|
523
|
+
return webMcpRuntime(owner).controller.signal;
|
|
524
|
+
}
|
|
525
|
+
function applyRuntimeWebMcpSetting(enabled, owner) {
|
|
526
|
+
const runtime = webMcpRuntime(owner);
|
|
527
|
+
if (runtime.enabled !== enabled) {
|
|
528
|
+
invalidateWebMcpOperations(owner, `WebMCP ${enabled ? "enabled" : "disabled"}`);
|
|
529
|
+
}
|
|
530
|
+
runtime.enabled = enabled;
|
|
531
|
+
}
|
|
532
|
+
function webMcpEnabled(owner) {
|
|
533
|
+
return webMcpRuntime(owner).enabled;
|
|
534
|
+
}
|
|
535
|
+
function webMcpOperationIsCurrent(operation) {
|
|
536
|
+
const runtime = webMcpRuntime(operation.owner);
|
|
537
|
+
return operation.sessionGeneration === runtime.sessionGeneration && operation.webMcpGeneration === runtime.generation;
|
|
538
|
+
}
|
|
539
|
+
function webMcpRuntime(owner) {
|
|
540
|
+
const existing = webMcpRuntimeByOwner.get(owner);
|
|
541
|
+
if (existing) return existing;
|
|
542
|
+
const created = {
|
|
543
|
+
controller: new AbortController(),
|
|
544
|
+
controllers: /* @__PURE__ */ new Set(),
|
|
545
|
+
enabled: false,
|
|
546
|
+
generation: 0,
|
|
547
|
+
sessionGeneration: 0
|
|
548
|
+
};
|
|
549
|
+
webMcpRuntimeByOwner.set(owner, created);
|
|
550
|
+
return created;
|
|
551
|
+
}
|
|
449
552
|
function applyRuntimeBrowserSettings(browser, paths, projectTrusted) {
|
|
450
553
|
state.host = browser.host;
|
|
451
554
|
state.port = browser.port;
|
|
@@ -483,6 +586,94 @@ var DEFAULT_BROWSER_MANAGER_OPERATIONS = {
|
|
|
483
586
|
sleep: abortableSleep
|
|
484
587
|
};
|
|
485
588
|
var browserManagerOperations = DEFAULT_BROWSER_MANAGER_OPERATIONS;
|
|
589
|
+
var managedBrowserScopes = /* @__PURE__ */ new WeakMap();
|
|
590
|
+
function cloneBrowserSettings(browser) {
|
|
591
|
+
return { ...browser, extensionPaths: [...browser.extensionPaths] };
|
|
592
|
+
}
|
|
593
|
+
function runtimeBrowserSettings() {
|
|
594
|
+
return {
|
|
595
|
+
endpoint: `http://${formatHostForUrl(state.host)}:${state.configuredPort}`,
|
|
596
|
+
host: state.host,
|
|
597
|
+
port: state.configuredPort,
|
|
598
|
+
hostConfigured: state.hostConfigured,
|
|
599
|
+
portConfigured: state.portConfigured,
|
|
600
|
+
autoLaunchEnabled: state.autoLaunchEnabled,
|
|
601
|
+
...state.browserExecutable ? { executablePath: state.browserExecutable } : {},
|
|
602
|
+
extensionPaths: [...state.extensionPaths],
|
|
603
|
+
endpointSource: state.endpointSource,
|
|
604
|
+
autoLaunchSource: state.autoLaunchSource,
|
|
605
|
+
executablePathSource: state.browserExecutableSource,
|
|
606
|
+
extensionPathsSource: state.extensionPathsSource
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
function managedBrowserScope(owner) {
|
|
610
|
+
const existing = managedBrowserScopes.get(owner);
|
|
611
|
+
if (existing) return existing;
|
|
612
|
+
const browser = runtimeBrowserSettings();
|
|
613
|
+
const created = {
|
|
614
|
+
browser,
|
|
615
|
+
controller: new AbortController(),
|
|
616
|
+
generation: 0,
|
|
617
|
+
port: browser.port
|
|
618
|
+
};
|
|
619
|
+
managedBrowserScopes.set(owner, created);
|
|
620
|
+
return created;
|
|
621
|
+
}
|
|
622
|
+
function startManagedBrowserSession(owner) {
|
|
623
|
+
const scope = managedBrowserScope(owner);
|
|
624
|
+
scope.controller.abort(new DOMException("Chrome DevTools session replaced", "AbortError"));
|
|
625
|
+
scope.controller = new AbortController();
|
|
626
|
+
scope.generation += 1;
|
|
627
|
+
scope.lastLaunchAttempt = void 0;
|
|
628
|
+
}
|
|
629
|
+
function syncManagedBrowserSettings(owner, browser) {
|
|
630
|
+
const scope = managedBrowserScope(owner);
|
|
631
|
+
scope.browser = cloneBrowserSettings(browser);
|
|
632
|
+
scope.port = browser.port;
|
|
633
|
+
}
|
|
634
|
+
function browserSettingsForOwner(owner) {
|
|
635
|
+
return owner ? managedBrowserScope(owner).browser : runtimeBrowserSettings();
|
|
636
|
+
}
|
|
637
|
+
function managedBrowserExtensionPaths(owner) {
|
|
638
|
+
return browserSettingsForOwner(owner).extensionPaths;
|
|
639
|
+
}
|
|
640
|
+
function managedBrowserForOwner(owner) {
|
|
641
|
+
if (!owner) return state.managedBrowser;
|
|
642
|
+
const scopedBrowser = managedBrowserScope(owner).managedBrowser;
|
|
643
|
+
if (scopedBrowser) return scopedBrowser;
|
|
644
|
+
return state.managedBrowser?.sessionOwner === owner ? state.managedBrowser : void 0;
|
|
645
|
+
}
|
|
646
|
+
function setManagedBrowser(owner, browser) {
|
|
647
|
+
if (owner) managedBrowserScope(owner).managedBrowser = browser;
|
|
648
|
+
else state.managedBrowser = browser;
|
|
649
|
+
}
|
|
650
|
+
function launchPromiseForOwner(owner) {
|
|
651
|
+
return owner ? managedBrowserScope(owner).launchPromise : state.launchPromise;
|
|
652
|
+
}
|
|
653
|
+
function setLaunchPromise(owner, launchPromise) {
|
|
654
|
+
if (owner) managedBrowserScope(owner).launchPromise = launchPromise;
|
|
655
|
+
else state.launchPromise = launchPromise;
|
|
656
|
+
}
|
|
657
|
+
function managedBrowserPort(owner) {
|
|
658
|
+
return owner ? managedBrowserScope(owner).port : state.port;
|
|
659
|
+
}
|
|
660
|
+
function managedBrowserConfiguredPort(owner) {
|
|
661
|
+
return browserSettingsForOwner(owner).port;
|
|
662
|
+
}
|
|
663
|
+
function managedBrowserPortConfigured(owner) {
|
|
664
|
+
return browserSettingsForOwner(owner).portConfigured;
|
|
665
|
+
}
|
|
666
|
+
function setManagedBrowserPort(owner, port) {
|
|
667
|
+
if (owner) managedBrowserScope(owner).port = port;
|
|
668
|
+
else state.port = port;
|
|
669
|
+
}
|
|
670
|
+
function lastLaunchAttempt(owner) {
|
|
671
|
+
return owner ? managedBrowserScope(owner).lastLaunchAttempt : state.lastLaunchAttempt;
|
|
672
|
+
}
|
|
673
|
+
function setLastLaunchAttempt(owner, attempt) {
|
|
674
|
+
if (owner) managedBrowserScope(owner).lastLaunchAttempt = attempt;
|
|
675
|
+
else state.lastLaunchAttempt = attempt;
|
|
676
|
+
}
|
|
486
677
|
function formatError2(error) {
|
|
487
678
|
return error instanceof Error ? error.message : String(error);
|
|
488
679
|
}
|
|
@@ -492,142 +683,189 @@ function isNodeError2(error) {
|
|
|
492
683
|
function normalizePathForComparison(value) {
|
|
493
684
|
return process.platform === "win32" ? value.toLowerCase() : value;
|
|
494
685
|
}
|
|
495
|
-
async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
686
|
+
async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS, callerSignal, owner) {
|
|
687
|
+
callerSignal?.throwIfAborted();
|
|
688
|
+
if (extensionsConfigured(owner)) {
|
|
689
|
+
validateExtensionLaunchMode(owner);
|
|
690
|
+
await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
|
|
499
691
|
return;
|
|
500
692
|
}
|
|
501
|
-
if (canAutoLaunchBrowser()) {
|
|
693
|
+
if (canAutoLaunchBrowser(owner)) {
|
|
502
694
|
try {
|
|
503
|
-
await withEndpointRetry(
|
|
695
|
+
await withEndpointRetry(
|
|
696
|
+
() => fetchDevToolsJson("/json/version", { signal: callerSignal }, owner),
|
|
697
|
+
waitMs,
|
|
698
|
+
callerSignal
|
|
699
|
+
);
|
|
504
700
|
return;
|
|
505
701
|
} catch (error) {
|
|
506
|
-
if (shouldAutoLaunchAfterEndpointError(error)) {
|
|
507
|
-
await ensureManagedBrowserLaunched(waitMs);
|
|
702
|
+
if (shouldAutoLaunchAfterEndpointError(error, owner)) {
|
|
703
|
+
await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
|
|
508
704
|
return;
|
|
509
705
|
}
|
|
510
706
|
throw error;
|
|
511
707
|
}
|
|
512
708
|
}
|
|
513
709
|
try {
|
|
514
|
-
await fetchDevToolsJson("/json/version");
|
|
710
|
+
await fetchDevToolsJson("/json/version", { signal: callerSignal }, owner);
|
|
515
711
|
} catch (error) {
|
|
516
712
|
if (isRetryableEndpointError(error)) return;
|
|
517
713
|
throw error;
|
|
518
714
|
}
|
|
519
715
|
}
|
|
520
|
-
function validateExtensionLaunchMode() {
|
|
521
|
-
|
|
716
|
+
function validateExtensionLaunchMode(owner) {
|
|
717
|
+
const browser = browserSettingsForOwner(owner);
|
|
718
|
+
if (!isLocalDevToolsHost(browser.host)) {
|
|
522
719
|
throw new DevToolsEndpointError(
|
|
523
720
|
"Unpacked extensions require a local extension-owned managed browser; remote CDP endpoints cannot be modified."
|
|
524
721
|
);
|
|
525
722
|
}
|
|
526
|
-
if (!
|
|
723
|
+
if (!browser.autoLaunchEnabled) {
|
|
527
724
|
throw new DevToolsEndpointError(
|
|
528
725
|
"Auto-launch is required for unpacked extensions so Pi can create an isolated managed browser."
|
|
529
726
|
);
|
|
530
727
|
}
|
|
531
|
-
if (!
|
|
728
|
+
if (!browser.executablePath) {
|
|
532
729
|
throw new DevToolsEndpointError(
|
|
533
730
|
"Unpacked extensions require browser.executablePath for Chrome for Testing or Chromium in pi-chrome-devtools.json."
|
|
534
731
|
);
|
|
535
732
|
}
|
|
536
733
|
}
|
|
537
|
-
async function ensureManagedBrowserLaunched(waitMs) {
|
|
538
|
-
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
734
|
+
async function ensureManagedBrowserLaunched(waitMs, callerSignal, owner) {
|
|
735
|
+
const existingLaunch = launchPromiseForOwner(owner);
|
|
736
|
+
if (existingLaunch) {
|
|
737
|
+
await waitForCaller(existingLaunch, callerSignal);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const existingBrowser = managedBrowserForOwner(owner);
|
|
741
|
+
if (existingBrowser && !existingBrowser.exited && existingBrowser.ready) return;
|
|
742
|
+
const generation = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
|
|
743
|
+
const signal = owner ? managedBrowserScope(owner).controller.signal : state.sessionController.signal;
|
|
744
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
745
|
+
const launchPromise = prepareManagedBrowserLaunch(waitMs, generation, signal, owner);
|
|
544
746
|
const wrappedPromise = launchPromise.finally(() => {
|
|
545
|
-
if (
|
|
747
|
+
if (launchPromiseForOwner(owner) === wrappedPromise) setLaunchPromise(owner, void 0);
|
|
546
748
|
});
|
|
547
|
-
|
|
548
|
-
|
|
749
|
+
setLaunchPromise(owner, wrappedPromise);
|
|
750
|
+
await waitForCaller(wrappedPromise, callerSignal);
|
|
549
751
|
}
|
|
550
|
-
|
|
551
|
-
if (
|
|
552
|
-
|
|
553
|
-
|
|
752
|
+
function waitForCaller(operation, signal) {
|
|
753
|
+
if (!signal) return operation;
|
|
754
|
+
signal.throwIfAborted();
|
|
755
|
+
return new Promise((resolve3, reject) => {
|
|
756
|
+
let settled = false;
|
|
757
|
+
const onAbort = () => settle(() => reject(signal.reason));
|
|
758
|
+
const settle = (callback) => {
|
|
759
|
+
if (settled) return;
|
|
760
|
+
settled = true;
|
|
761
|
+
signal.removeEventListener("abort", onAbort);
|
|
762
|
+
callback();
|
|
763
|
+
};
|
|
764
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
765
|
+
operation.then(
|
|
766
|
+
(value) => settle(() => resolve3(value)),
|
|
767
|
+
(error) => settle(() => reject(error))
|
|
768
|
+
);
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
async function prepareManagedBrowserLaunch(waitMs, generation, signal, owner) {
|
|
772
|
+
const existingBrowser = managedBrowserForOwner(owner);
|
|
773
|
+
if (existingBrowser) {
|
|
774
|
+
await shutdownManagedBrowser(existingBrowser, { awaitLaunch: false, owner });
|
|
775
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
554
776
|
}
|
|
555
|
-
if (
|
|
556
|
-
const available = await browserManagerOperations.isPortAvailable(
|
|
557
|
-
|
|
777
|
+
if (managedBrowserPortConfigured(owner)) {
|
|
778
|
+
const available = await browserManagerOperations.isPortAvailable(
|
|
779
|
+
browserSettingsForOwner(owner).host,
|
|
780
|
+
managedBrowserPort(owner)
|
|
781
|
+
);
|
|
782
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
558
783
|
if (!available) {
|
|
559
784
|
throw new DevToolsEndpointError(
|
|
560
|
-
`Cannot launch the extension-owned browser because explicit port ${
|
|
785
|
+
`Cannot launch the extension-owned browser because explicit port ${managedBrowserPort(owner)} is already in use. Choose a free browser.endpoint port or reset the endpoint to use a dynamic managed port.`
|
|
561
786
|
);
|
|
562
787
|
}
|
|
563
788
|
}
|
|
564
|
-
return launchManagedBrowser(waitMs, generation, signal);
|
|
789
|
+
return launchManagedBrowser(waitMs, generation, signal, owner);
|
|
565
790
|
}
|
|
566
|
-
async function launchManagedBrowser(waitMs, generation, signal) {
|
|
567
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
568
|
-
const candidateDefinitions = browserCandidateDefinitions();
|
|
569
|
-
const candidates = await resolveBrowserCandidates(
|
|
570
|
-
|
|
571
|
-
|
|
791
|
+
async function launchManagedBrowser(waitMs, generation, signal, owner) {
|
|
792
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
793
|
+
const candidateDefinitions = browserCandidateDefinitions(owner);
|
|
794
|
+
const candidates = await resolveBrowserCandidates(
|
|
795
|
+
candidateDefinitions,
|
|
796
|
+
generation,
|
|
797
|
+
signal,
|
|
798
|
+
owner
|
|
799
|
+
);
|
|
800
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
801
|
+
setLastLaunchAttempt(owner, {
|
|
572
802
|
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
573
|
-
mode:
|
|
574
|
-
};
|
|
803
|
+
mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port"
|
|
804
|
+
});
|
|
575
805
|
if (candidates.length === 0) {
|
|
576
|
-
throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions));
|
|
806
|
+
throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions, owner));
|
|
577
807
|
}
|
|
578
808
|
let lastError;
|
|
579
809
|
for (const candidate of candidates) {
|
|
580
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
810
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
581
811
|
try {
|
|
582
|
-
if (extensionsConfigured())
|
|
583
|
-
await requireSupportedExtensionBrowser(candidate, generation, signal);
|
|
584
|
-
await launchBrowserCandidate(candidate, waitMs, generation, signal);
|
|
585
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
586
|
-
|
|
587
|
-
...
|
|
812
|
+
if (extensionsConfigured(owner))
|
|
813
|
+
await requireSupportedExtensionBrowser(candidate, generation, signal, owner);
|
|
814
|
+
await launchBrowserCandidate(candidate, waitMs, generation, signal, owner);
|
|
815
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
816
|
+
setLastLaunchAttempt(owner, {
|
|
817
|
+
...lastLaunchAttempt(owner),
|
|
818
|
+
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
819
|
+
mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port",
|
|
588
820
|
selectedCandidate: formatBrowserCandidate(candidate),
|
|
589
|
-
userDataDir:
|
|
590
|
-
};
|
|
821
|
+
userDataDir: managedBrowserForOwner(owner)?.userDataDir
|
|
822
|
+
});
|
|
591
823
|
return;
|
|
592
824
|
} catch (error) {
|
|
593
825
|
try {
|
|
594
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
826
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
595
827
|
} catch (cancellation) {
|
|
596
|
-
|
|
597
|
-
...
|
|
828
|
+
setLastLaunchAttempt(owner, {
|
|
829
|
+
...lastLaunchAttempt(owner) ?? {
|
|
830
|
+
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
831
|
+
mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port"
|
|
832
|
+
},
|
|
598
833
|
lastError: formatError2(cancellation)
|
|
599
|
-
};
|
|
834
|
+
});
|
|
600
835
|
throw cancellation;
|
|
601
836
|
}
|
|
602
837
|
lastError = error;
|
|
603
|
-
|
|
604
|
-
...
|
|
838
|
+
setLastLaunchAttempt(owner, {
|
|
839
|
+
...lastLaunchAttempt(owner) ?? {
|
|
840
|
+
candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
|
|
841
|
+
mode: managedBrowserPortConfigured(owner) ? "explicit-port" : "dynamic-port"
|
|
842
|
+
},
|
|
605
843
|
lastError: formatError2(error)
|
|
606
|
-
};
|
|
844
|
+
});
|
|
607
845
|
}
|
|
608
846
|
}
|
|
609
|
-
if (lastError instanceof DevToolsEndpointError && extensionsConfigured()) throw lastError;
|
|
847
|
+
if (lastError instanceof DevToolsEndpointError && extensionsConfigured(owner)) throw lastError;
|
|
610
848
|
throw new DevToolsEndpointError(
|
|
611
849
|
[
|
|
612
850
|
"Unable to auto-launch a Chromium-family browser for Chrome DevTools.",
|
|
613
851
|
`Tried: ${candidates.map(formatBrowserCandidate).join(", ")}`,
|
|
614
852
|
lastError ? `Last error: ${formatError2(lastError)}` : void 0,
|
|
615
|
-
launchHint(),
|
|
853
|
+
launchHint(owner),
|
|
616
854
|
endpointConfigHint()
|
|
617
855
|
].filter(Boolean).join("\n")
|
|
618
856
|
);
|
|
619
857
|
}
|
|
620
|
-
async function requireSupportedExtensionBrowser(candidate, generation, signal) {
|
|
858
|
+
async function requireSupportedExtensionBrowser(candidate, generation, signal, owner) {
|
|
621
859
|
let version;
|
|
622
860
|
try {
|
|
623
861
|
version = (await browserManagerOperations.inspectBrowserVersion(candidate.resolvedExecutable, signal)).trim();
|
|
624
862
|
} catch (error) {
|
|
625
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
863
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
626
864
|
throw new DevToolsEndpointError(
|
|
627
865
|
`Unable to identify the configured extension browser ${candidate.resolvedExecutable}: ${formatError2(error)}. Configure Chrome for Testing or Chromium.`
|
|
628
866
|
);
|
|
629
867
|
}
|
|
630
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
868
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
631
869
|
const classification = classifyExtensionBrowserVersion(version);
|
|
632
870
|
if (!classification.supported) {
|
|
633
871
|
throw new DevToolsEndpointError(
|
|
@@ -643,19 +881,19 @@ function classifyExtensionBrowserVersion(version) {
|
|
|
643
881
|
const product = version.trim().replace(/\s+\d+(?:\.\d+)*.*$/, "") || "Unknown";
|
|
644
882
|
return { supported: false, product };
|
|
645
883
|
}
|
|
646
|
-
async function launchBrowserCandidate(candidate, waitMs, generation, signal) {
|
|
647
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
884
|
+
async function launchBrowserCandidate(candidate, waitMs, generation, signal, owner) {
|
|
885
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
648
886
|
const userDataDir = await browserManagerOperations.mkdtemp(
|
|
649
887
|
join2(tmpdir(), MANAGED_BROWSER_PROFILE_PREFIX)
|
|
650
888
|
);
|
|
651
889
|
let managedBrowser;
|
|
652
890
|
try {
|
|
653
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
654
|
-
const portArgument =
|
|
891
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
892
|
+
const portArgument = managedBrowserPortConfigured(owner) ? String(managedBrowserPort(owner)) : "0";
|
|
655
893
|
const args = buildManagedBrowserLaunchArguments(
|
|
656
894
|
userDataDir,
|
|
657
895
|
portArgument,
|
|
658
|
-
|
|
896
|
+
managedBrowserExtensionPaths(owner)
|
|
659
897
|
);
|
|
660
898
|
const child = browserManagerOperations.spawn(candidate.resolvedExecutable, args, {
|
|
661
899
|
shell: false,
|
|
@@ -666,37 +904,42 @@ async function launchBrowserCandidate(candidate, waitMs, generation, signal) {
|
|
|
666
904
|
userDataDir,
|
|
667
905
|
exited: false,
|
|
668
906
|
ready: false,
|
|
669
|
-
ownerGeneration: generation
|
|
907
|
+
ownerGeneration: generation,
|
|
908
|
+
...owner ? { sessionOwner: owner } : {}
|
|
670
909
|
};
|
|
671
910
|
managedBrowser = launchedBrowser;
|
|
672
|
-
|
|
911
|
+
setManagedBrowser(owner, launchedBrowser);
|
|
673
912
|
child.once("exit", () => {
|
|
913
|
+
if (managedBrowserForOwner(owner) === launchedBrowser && owner) {
|
|
914
|
+
invalidateWebMcpOperations(owner, "Chrome DevTools managed browser exited");
|
|
915
|
+
}
|
|
674
916
|
launchedBrowser.exited = true;
|
|
675
917
|
launchedBrowser.ready = false;
|
|
676
|
-
if (!
|
|
677
|
-
|
|
918
|
+
if (!managedBrowserPortConfigured(owner) && launchedBrowser.port === managedBrowserPort(owner)) {
|
|
919
|
+
setManagedBrowserPort(owner, managedBrowserConfiguredPort(owner));
|
|
678
920
|
}
|
|
679
921
|
});
|
|
680
922
|
await waitForBrowserSpawn(child);
|
|
681
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
682
|
-
if (
|
|
683
|
-
launchedBrowser.port =
|
|
923
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
924
|
+
if (managedBrowserPortConfigured(owner)) {
|
|
925
|
+
launchedBrowser.port = managedBrowserPort(owner);
|
|
684
926
|
} else {
|
|
685
927
|
launchedBrowser.port = await readManagedBrowserPort(
|
|
686
928
|
userDataDir,
|
|
687
929
|
launchedBrowser,
|
|
688
930
|
waitMs,
|
|
689
931
|
generation,
|
|
690
|
-
signal
|
|
932
|
+
signal,
|
|
933
|
+
owner
|
|
691
934
|
);
|
|
692
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
693
|
-
|
|
935
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
936
|
+
setManagedBrowserPort(owner, launchedBrowser.port);
|
|
694
937
|
}
|
|
695
|
-
await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal);
|
|
696
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
938
|
+
await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal, owner);
|
|
939
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
697
940
|
launchedBrowser.ready = true;
|
|
698
941
|
} catch (error) {
|
|
699
|
-
if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false });
|
|
942
|
+
if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false, owner });
|
|
700
943
|
else {
|
|
701
944
|
await browserManagerOperations.rm(userDataDir, { recursive: true, force: true }).catch(() => void 0);
|
|
702
945
|
}
|
|
@@ -734,7 +977,7 @@ function waitForBrowserSpawn(child) {
|
|
|
734
977
|
child.once("spawn", onSpawn);
|
|
735
978
|
});
|
|
736
979
|
}
|
|
737
|
-
async function readManagedBrowserPort(userDataDir, managedBrowser, waitMs, generation, signal) {
|
|
980
|
+
async function readManagedBrowserPort(userDataDir, managedBrowser, waitMs, generation, signal, owner) {
|
|
738
981
|
const activePortFile = join2(userDataDir, DEVTOOLS_ACTIVE_PORT_FILE);
|
|
739
982
|
const deadline = Date.now() + waitMs;
|
|
740
983
|
while (true) {
|
|
@@ -743,7 +986,7 @@ async function readManagedBrowserPort(userDataDir, managedBrowser, waitMs, gener
|
|
|
743
986
|
if (isNodeError2(error) && error.code === "ENOENT") return void 0;
|
|
744
987
|
throw error;
|
|
745
988
|
});
|
|
746
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
989
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
747
990
|
const portText = text?.split(/\r?\n/, 1)[0]?.trim();
|
|
748
991
|
const port = Number(portText);
|
|
749
992
|
if (Number.isInteger(port) && port > 0) return port;
|
|
@@ -753,31 +996,31 @@ async function readManagedBrowserPort(userDataDir, managedBrowser, waitMs, gener
|
|
|
753
996
|
[
|
|
754
997
|
"Timed out waiting for auto-launched browser DevToolsActivePort.",
|
|
755
998
|
`Expected file: ${activePortFile}`,
|
|
756
|
-
launchHint()
|
|
999
|
+
launchHint(owner)
|
|
757
1000
|
].join("\n")
|
|
758
1001
|
);
|
|
759
1002
|
}
|
|
760
1003
|
await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs), signal);
|
|
761
1004
|
}
|
|
762
1005
|
}
|
|
763
|
-
async function waitForDevToolsEndpoint(waitMs, managedBrowser, generation, signal) {
|
|
1006
|
+
async function waitForDevToolsEndpoint(waitMs, managedBrowser, generation, signal, owner) {
|
|
764
1007
|
const deadline = Date.now() + waitMs;
|
|
765
1008
|
while (true) {
|
|
766
1009
|
throwIfManagedBrowserExited(managedBrowser);
|
|
767
1010
|
try {
|
|
768
|
-
await fetchDevToolsJson("/json/version", { signal });
|
|
769
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
1011
|
+
await fetchDevToolsJson("/json/version", { signal }, owner);
|
|
1012
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
770
1013
|
return;
|
|
771
1014
|
} catch (error) {
|
|
772
1015
|
if (!isRetryableEndpointError(error)) throw error;
|
|
773
1016
|
}
|
|
774
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
1017
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
775
1018
|
const remainingMs = deadline - Date.now();
|
|
776
1019
|
if (remainingMs <= 0) {
|
|
777
1020
|
throw new DevToolsEndpointError(
|
|
778
1021
|
[
|
|
779
|
-
`Timed out waiting for auto-launched browser at ${devToolsEndpoint()}.`,
|
|
780
|
-
launchHint()
|
|
1022
|
+
`Timed out waiting for auto-launched browser at ${devToolsEndpoint(owner)}.`,
|
|
1023
|
+
launchHint(owner)
|
|
781
1024
|
].join("\n")
|
|
782
1025
|
);
|
|
783
1026
|
}
|
|
@@ -788,28 +1031,44 @@ function throwIfManagedBrowserExited(managedBrowser) {
|
|
|
788
1031
|
if (!managedBrowser.exited) return;
|
|
789
1032
|
throw new DevToolsEndpointError("Auto-launched browser exited before DevTools became available.");
|
|
790
1033
|
}
|
|
791
|
-
function throwIfBrowserLaunchCancelled(generation, signal) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1034
|
+
function throwIfBrowserLaunchCancelled(generation, signal, owner) {
|
|
1035
|
+
const currentGeneration = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
|
|
1036
|
+
const shuttingDown = owner ? signal.aborted : state.shuttingDown;
|
|
1037
|
+
if (!shuttingDown && generation === currentGeneration && !signal.aborted) return;
|
|
795
1038
|
throw new DevToolsEndpointError(
|
|
796
|
-
generation ===
|
|
1039
|
+
generation === currentGeneration ? "Chrome DevTools browser launch cancelled during shutdown." : "Chrome DevTools browser launch cancelled because the session was replaced."
|
|
797
1040
|
);
|
|
798
1041
|
}
|
|
799
|
-
async function shutdownManagedBrowser(managedBrowser
|
|
800
|
-
|
|
1042
|
+
async function shutdownManagedBrowser(managedBrowser, options = {}) {
|
|
1043
|
+
const owner = options.owner ?? managedBrowser?.sessionOwner;
|
|
1044
|
+
if (options.cancelLaunch) {
|
|
1045
|
+
if (owner) {
|
|
1046
|
+
const scope = managedBrowserScope(owner);
|
|
1047
|
+
scope.controller.abort(
|
|
1048
|
+
new DOMException("Chrome DevTools browser launch cancelled during shutdown", "AbortError")
|
|
1049
|
+
);
|
|
1050
|
+
scope.generation += 1;
|
|
1051
|
+
} else {
|
|
1052
|
+
state.shuttingDown = true;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
managedBrowser ??= managedBrowserForOwner(owner);
|
|
801
1056
|
if (options.awaitLaunch !== false) {
|
|
802
|
-
await
|
|
803
|
-
managedBrowser
|
|
1057
|
+
await launchPromiseForOwner(owner)?.catch(() => void 0);
|
|
1058
|
+
managedBrowser ??= managedBrowserForOwner(owner);
|
|
804
1059
|
}
|
|
805
1060
|
if (!managedBrowser) return;
|
|
806
1061
|
if (managedBrowser.cleanupPromise) return managedBrowser.cleanupPromise;
|
|
807
|
-
const cleanup = cleanupManagedBrowser(managedBrowser);
|
|
1062
|
+
const cleanup = cleanupManagedBrowser(managedBrowser, owner);
|
|
808
1063
|
managedBrowser.cleanupPromise = cleanup;
|
|
809
1064
|
return cleanup;
|
|
810
1065
|
}
|
|
811
|
-
async function cleanupManagedBrowser(managedBrowser) {
|
|
812
|
-
if (
|
|
1066
|
+
async function cleanupManagedBrowser(managedBrowser, owner) {
|
|
1067
|
+
if (managedBrowserForOwner(owner) === managedBrowser) {
|
|
1068
|
+
if (owner) invalidateWebMcpOperations(owner, "Chrome DevTools managed browser closed");
|
|
1069
|
+
if (state.managedBrowser === managedBrowser) state.managedBrowser = void 0;
|
|
1070
|
+
else setManagedBrowser(owner, void 0);
|
|
1071
|
+
}
|
|
813
1072
|
if (!managedBrowser.exited) {
|
|
814
1073
|
killManagedBrowserProcess(managedBrowser);
|
|
815
1074
|
await waitForManagedBrowserExit(managedBrowser, BROWSER_SHUTDOWN_WAIT_MS).catch(async () => {
|
|
@@ -820,8 +1079,8 @@ async function cleanupManagedBrowser(managedBrowser) {
|
|
|
820
1079
|
});
|
|
821
1080
|
}
|
|
822
1081
|
await browserManagerOperations.rm(managedBrowser.userDataDir, { recursive: true, force: true }).catch(() => void 0);
|
|
823
|
-
if (!
|
|
824
|
-
|
|
1082
|
+
if (!managedBrowserPortConfigured(owner) && managedBrowser.port === managedBrowserPort(owner)) {
|
|
1083
|
+
setManagedBrowserPort(owner, managedBrowserConfiguredPort(owner));
|
|
825
1084
|
}
|
|
826
1085
|
}
|
|
827
1086
|
function killManagedBrowserProcess(managedBrowser, signal) {
|
|
@@ -851,8 +1110,8 @@ function waitForManagedBrowserExit(managedBrowser, waitMs) {
|
|
|
851
1110
|
managedBrowser.process.once("close", onExitOrClose);
|
|
852
1111
|
});
|
|
853
1112
|
}
|
|
854
|
-
async function fetchDevToolsJson(path, init) {
|
|
855
|
-
const url = `${devToolsEndpoint()}${path}`;
|
|
1113
|
+
async function fetchDevToolsJson(path, init, owner) {
|
|
1114
|
+
const url = `${devToolsEndpoint(owner)}${path}`;
|
|
856
1115
|
let response;
|
|
857
1116
|
try {
|
|
858
1117
|
const timeoutSignal = AbortSignal.timeout(DEFAULT_HTTP_TIMEOUT_MS);
|
|
@@ -861,7 +1120,7 @@ async function fetchDevToolsJson(path, init) {
|
|
|
861
1120
|
signal: init?.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
|
|
862
1121
|
});
|
|
863
1122
|
} catch (error) {
|
|
864
|
-
throw new DevToolsEndpointError(endpointConnectionErrorMessage(error), {
|
|
1123
|
+
throw new DevToolsEndpointError(endpointConnectionErrorMessage(error, owner), {
|
|
865
1124
|
launchable: true,
|
|
866
1125
|
retryable: true
|
|
867
1126
|
});
|
|
@@ -888,16 +1147,20 @@ async function fetchDevToolsJson(path, init) {
|
|
|
888
1147
|
);
|
|
889
1148
|
}
|
|
890
1149
|
}
|
|
891
|
-
async function withEndpointRetry(operation, waitMs) {
|
|
1150
|
+
async function withEndpointRetry(operation, waitMs, signal) {
|
|
892
1151
|
const deadline = Date.now() + waitMs;
|
|
893
1152
|
while (true) {
|
|
1153
|
+
signal?.throwIfAborted();
|
|
894
1154
|
try {
|
|
895
1155
|
return await operation();
|
|
896
1156
|
} catch (error) {
|
|
897
1157
|
if (!isRetryableEndpointError(error)) throw error;
|
|
898
1158
|
const remainingMs = deadline - Date.now();
|
|
899
1159
|
if (remainingMs <= 0) throw error;
|
|
900
|
-
await browserManagerOperations.sleep(
|
|
1160
|
+
await browserManagerOperations.sleep(
|
|
1161
|
+
Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs),
|
|
1162
|
+
signal
|
|
1163
|
+
);
|
|
901
1164
|
}
|
|
902
1165
|
}
|
|
903
1166
|
}
|
|
@@ -907,100 +1170,101 @@ function isRetryableEndpointError(error) {
|
|
|
907
1170
|
function isLaunchableEndpointError(error) {
|
|
908
1171
|
return error instanceof DevToolsEndpointError && error.launchable;
|
|
909
1172
|
}
|
|
910
|
-
function shouldAutoLaunchAfterEndpointError(error) {
|
|
911
|
-
if (!canAutoLaunchBrowser()) return false;
|
|
1173
|
+
function shouldAutoLaunchAfterEndpointError(error, owner) {
|
|
1174
|
+
if (!canAutoLaunchBrowser(owner)) return false;
|
|
912
1175
|
if (isLaunchableEndpointError(error)) return true;
|
|
913
|
-
return !
|
|
1176
|
+
return !managedBrowserPortConfigured(owner) && error instanceof DevToolsEndpointError;
|
|
914
1177
|
}
|
|
915
|
-
function canAutoLaunchBrowser() {
|
|
916
|
-
|
|
1178
|
+
function canAutoLaunchBrowser(owner) {
|
|
1179
|
+
const browser = browserSettingsForOwner(owner);
|
|
1180
|
+
return browser.autoLaunchEnabled && isLocalDevToolsHost(browser.host);
|
|
917
1181
|
}
|
|
918
|
-
function extensionsConfigured() {
|
|
919
|
-
return
|
|
1182
|
+
function extensionsConfigured(owner) {
|
|
1183
|
+
return managedBrowserExtensionPaths(owner).length > 0;
|
|
920
1184
|
}
|
|
921
|
-
function endpointConnectionErrorMessage(error) {
|
|
1185
|
+
function endpointConnectionErrorMessage(error, owner) {
|
|
922
1186
|
const reason = isTimeoutError(error) ? "request timed out" : "connection failed";
|
|
923
1187
|
return [
|
|
924
|
-
`Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint()} (${reason}).`,
|
|
925
|
-
launchHint(),
|
|
1188
|
+
`Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint(owner)} (${reason}).`,
|
|
1189
|
+
launchHint(owner),
|
|
926
1190
|
endpointConfigHint()
|
|
927
1191
|
].join("\n");
|
|
928
1192
|
}
|
|
929
1193
|
function isTimeoutError(error) {
|
|
930
1194
|
return error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
|
|
931
1195
|
}
|
|
932
|
-
function devToolsEndpoint() {
|
|
933
|
-
return `http://${formatHostForUrl(
|
|
1196
|
+
function devToolsEndpoint(owner) {
|
|
1197
|
+
return `http://${formatHostForUrl(browserSettingsForOwner(owner).host)}:${managedBrowserPort(owner)}`;
|
|
934
1198
|
}
|
|
935
1199
|
function formatHostForUrl(host) {
|
|
936
1200
|
if (host.startsWith("[") && host.endsWith("]")) return host;
|
|
937
1201
|
return host.includes(":") ? `[${host}]` : host;
|
|
938
1202
|
}
|
|
939
|
-
function endpointSourceLabel() {
|
|
940
|
-
return
|
|
1203
|
+
function endpointSourceLabel(owner) {
|
|
1204
|
+
return browserSettingsForOwner(owner).endpointSource;
|
|
941
1205
|
}
|
|
942
|
-
function browserLifecycleState() {
|
|
943
|
-
if (
|
|
944
|
-
if (
|
|
945
|
-
if (
|
|
946
|
-
if (
|
|
1206
|
+
function browserLifecycleState(owner) {
|
|
1207
|
+
if (launchPromiseForOwner(owner)) return "starting";
|
|
1208
|
+
if (managedBrowserForOwner(owner)?.exited) return "exited";
|
|
1209
|
+
if (managedBrowserForOwner(owner)?.ready) return "running";
|
|
1210
|
+
if (lastLaunchAttempt(owner)?.lastError) return "failed";
|
|
947
1211
|
return "unobserved";
|
|
948
1212
|
}
|
|
949
|
-
function launchModeLabel() {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
if (
|
|
954
|
-
|
|
1213
|
+
function launchModeLabel(owner) {
|
|
1214
|
+
const browser = browserSettingsForOwner(owner);
|
|
1215
|
+
const managedBrowser = managedBrowserForOwner(owner);
|
|
1216
|
+
if (extensionsConfigured(owner)) {
|
|
1217
|
+
if (!isLocalDevToolsHost(browser.host)) return "invalid; extensions require a local endpoint";
|
|
1218
|
+
if (!browser.autoLaunchEnabled) return "invalid; extensions require auto-launch";
|
|
1219
|
+
if (managedBrowser && !managedBrowser.exited) {
|
|
1220
|
+
return managedBrowserPortConfigured(owner) ? "extension-owned browser on explicit port" : "extension-owned browser on dynamic port";
|
|
955
1221
|
}
|
|
956
|
-
return
|
|
1222
|
+
return managedBrowserPortConfigured(owner) ? "force extension-owned launch on explicit port" : "force extension-owned launch on dynamic port";
|
|
957
1223
|
}
|
|
958
|
-
if (!isLocalDevToolsHost(
|
|
959
|
-
if (!
|
|
960
|
-
if (
|
|
961
|
-
return
|
|
1224
|
+
if (!isLocalDevToolsHost(browser.host)) return "manual remote endpoint";
|
|
1225
|
+
if (!browser.autoLaunchEnabled) return "manual; auto-launch disabled";
|
|
1226
|
+
if (managedBrowser && !managedBrowser.exited) {
|
|
1227
|
+
return managedBrowserPortConfigured(owner) ? "auto-launched on explicit port" : "auto-launched on dynamic port";
|
|
962
1228
|
}
|
|
963
|
-
return
|
|
1229
|
+
return managedBrowserPortConfigured(owner) ? "attach first; auto-launch explicit port" : "attach first; auto-launch dynamic port";
|
|
964
1230
|
}
|
|
965
|
-
function launchAttemptLines() {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1231
|
+
function launchAttemptLines(owner) {
|
|
1232
|
+
const attempt = lastLaunchAttempt(owner);
|
|
1233
|
+
if (!attempt) return [];
|
|
1234
|
+
const lines = [`Last launch attempt: ${attempt.mode}`];
|
|
1235
|
+
if (attempt.selectedCandidate) {
|
|
1236
|
+
lines.push(`Launched browser: ${attempt.selectedCandidate}`);
|
|
970
1237
|
} else {
|
|
971
|
-
lines.push(`Tried browser candidates: ${
|
|
972
|
-
}
|
|
973
|
-
if (state.lastLaunchAttempt.userDataDir) {
|
|
974
|
-
lines.push(`Managed browser profile: ${state.lastLaunchAttempt.userDataDir}`);
|
|
975
|
-
}
|
|
976
|
-
if (state.lastLaunchAttempt.lastError) {
|
|
977
|
-
lines.push(`Last launch error: ${state.lastLaunchAttempt.lastError}`);
|
|
1238
|
+
lines.push(`Tried browser candidates: ${attempt.candidateLabels.join(", ")}`);
|
|
978
1239
|
}
|
|
1240
|
+
if (attempt.userDataDir) lines.push(`Managed browser profile: ${attempt.userDataDir}`);
|
|
1241
|
+
if (attempt.lastError) lines.push(`Last launch error: ${attempt.lastError}`);
|
|
979
1242
|
return lines;
|
|
980
1243
|
}
|
|
981
|
-
function launchHint() {
|
|
982
|
-
|
|
1244
|
+
function launchHint(owner) {
|
|
1245
|
+
const browser = browserSettingsForOwner(owner);
|
|
1246
|
+
if (extensionsConfigured(owner)) {
|
|
983
1247
|
return "Configured unpacked extensions force an isolated extension-owned launch; existing CDP browsers are never reused, modified, or closed.";
|
|
984
1248
|
}
|
|
985
|
-
if (!isLocalDevToolsHost(
|
|
986
|
-
return `Remote/non-local endpoints are not auto-launched. Start a browser with CDP enabled at ${devToolsEndpoint()}.`;
|
|
1249
|
+
if (!isLocalDevToolsHost(browser.host)) {
|
|
1250
|
+
return `Remote/non-local endpoints are not auto-launched. Start a browser with CDP enabled at ${devToolsEndpoint(owner)}.`;
|
|
987
1251
|
}
|
|
988
|
-
if (!
|
|
989
|
-
return `Auto-launch is disabled. Start a browser manually: ${chromeLaunchCommand()}`;
|
|
1252
|
+
if (!browser.autoLaunchEnabled) {
|
|
1253
|
+
return `Auto-launch is disabled. Start a browser manually: ${chromeLaunchCommand(owner)}`;
|
|
990
1254
|
}
|
|
991
|
-
const managedMode =
|
|
992
|
-
return `If no endpoint is available, Pi will auto-launch a Chromium-family browser with ${managedMode} and an isolated temp profile. Manual command: ${chromeLaunchCommand()}`;
|
|
1255
|
+
const managedMode = managedBrowserPortConfigured(owner) ? `port ${managedBrowserPort(owner)}` : "a dynamic DevTools port";
|
|
1256
|
+
return `If no endpoint is available, Pi will auto-launch a Chromium-family browser with ${managedMode} and an isolated temp profile. Manual command: ${chromeLaunchCommand(owner)}`;
|
|
993
1257
|
}
|
|
994
|
-
function browserCandidateHint() {
|
|
995
|
-
if (extensionsConfigured()) {
|
|
1258
|
+
function browserCandidateHint(owner) {
|
|
1259
|
+
if (extensionsConfigured(owner)) {
|
|
996
1260
|
return "Extension browser requirement: configure Chrome for Testing or Chromium with browser.executablePath; branded Chrome is rejected because it may ignore unpacked-extension flags.";
|
|
997
1261
|
}
|
|
998
|
-
return `Browser candidates: ${browserCandidateDefinitions().map((candidate) => candidate.label).join(", ")}`;
|
|
1262
|
+
return `Browser candidates: ${browserCandidateDefinitions(owner).map((candidate) => candidate.label).join(", ")}`;
|
|
999
1263
|
}
|
|
1000
|
-
function chromeLaunchCommand() {
|
|
1001
|
-
const executable =
|
|
1264
|
+
function chromeLaunchCommand(owner) {
|
|
1265
|
+
const executable = browserSettingsForOwner(owner).executablePath ?? defaultManualBrowserExecutable();
|
|
1002
1266
|
const dataDir = process.platform === "win32" ? "%TEMP%\\pi-chrome-devtools" : "/tmp/pi-chrome-devtools";
|
|
1003
|
-
return `${quoteCommandPart(executable)} --remote-debugging-port=${
|
|
1267
|
+
return `${quoteCommandPart(executable)} --remote-debugging-port=${managedBrowserPort(owner)} --user-data-dir=${dataDir}`;
|
|
1004
1268
|
}
|
|
1005
1269
|
function defaultManualBrowserExecutable() {
|
|
1006
1270
|
return process.platform === "darwin" ? "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" : process.platform === "win32" ? "chrome.exe" : "google-chrome";
|
|
@@ -1015,17 +1279,18 @@ function isLocalDevToolsHost(host) {
|
|
|
1015
1279
|
const normalizedHost = host.toLowerCase().replace(/^\[(.*)]$/, "$1");
|
|
1016
1280
|
return ["localhost", "127.0.0.1", "::1", "0:0:0:0:0:0:0:1"].includes(normalizedHost);
|
|
1017
1281
|
}
|
|
1018
|
-
function browserCandidateDefinitions() {
|
|
1019
|
-
const explicitCandidate = explicitBrowserCandidateDefinition();
|
|
1282
|
+
function browserCandidateDefinitions(owner) {
|
|
1283
|
+
const explicitCandidate = explicitBrowserCandidateDefinition(owner);
|
|
1020
1284
|
if (explicitCandidate.length > 0) return explicitCandidate;
|
|
1021
1285
|
return uniqueBrowserCandidates(platformBrowserCandidateDefinitions());
|
|
1022
1286
|
}
|
|
1023
|
-
function explicitBrowserCandidateDefinition() {
|
|
1024
|
-
|
|
1287
|
+
function explicitBrowserCandidateDefinition(owner) {
|
|
1288
|
+
const browser = browserSettingsForOwner(owner);
|
|
1289
|
+
if (!browser.executablePath) return [];
|
|
1025
1290
|
return [
|
|
1026
1291
|
{
|
|
1027
|
-
label:
|
|
1028
|
-
executable:
|
|
1292
|
+
label: browser.executablePathSource === "environment" ? "PI_CHROME_DEVTOOLS_BROWSER" : "browser.executablePath",
|
|
1293
|
+
executable: browser.executablePath,
|
|
1029
1294
|
source: "env"
|
|
1030
1295
|
}
|
|
1031
1296
|
];
|
|
@@ -1111,11 +1376,11 @@ function uniqueBrowserCandidates(candidates) {
|
|
|
1111
1376
|
return true;
|
|
1112
1377
|
});
|
|
1113
1378
|
}
|
|
1114
|
-
async function resolveBrowserCandidates(definitions, generation, signal) {
|
|
1379
|
+
async function resolveBrowserCandidates(definitions, generation, signal, owner) {
|
|
1115
1380
|
const candidates = [];
|
|
1116
1381
|
for (const definition of definitions) {
|
|
1117
1382
|
const resolvedExecutable = await resolveBrowserExecutable(definition.executable);
|
|
1118
|
-
throwIfBrowserLaunchCancelled(generation, signal);
|
|
1383
|
+
throwIfBrowserLaunchCancelled(generation, signal, owner);
|
|
1119
1384
|
if (!resolvedExecutable) continue;
|
|
1120
1385
|
candidates.push({ ...definition, resolvedExecutable });
|
|
1121
1386
|
}
|
|
@@ -1170,8 +1435,8 @@ function formatBrowserCandidate(candidate) {
|
|
|
1170
1435
|
function formatBrowserCandidateDefinition(candidate) {
|
|
1171
1436
|
return `${candidate.label} (${candidate.executable})`;
|
|
1172
1437
|
}
|
|
1173
|
-
function noBrowserCandidateMessage(candidateDefinitions) {
|
|
1174
|
-
if (extensionsConfigured()) {
|
|
1438
|
+
function noBrowserCandidateMessage(candidateDefinitions, owner) {
|
|
1439
|
+
if (extensionsConfigured(owner)) {
|
|
1175
1440
|
return [
|
|
1176
1441
|
"Cannot launch unpacked extensions because the configured browser executable was not found or is not executable.",
|
|
1177
1442
|
`Tried: ${candidateDefinitions.map(formatBrowserCandidateDefinition).join(", ")}`,
|
|
@@ -1249,412 +1514,46 @@ var DevToolsEndpointError = class extends Error {
|
|
|
1249
1514
|
}
|
|
1250
1515
|
};
|
|
1251
1516
|
|
|
1252
|
-
// src/lazy-tools.ts
|
|
1253
|
-
import {
|
|
1254
|
-
defineTool
|
|
1255
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1256
|
-
import { Type } from "typebox";
|
|
1257
|
-
var CHROME_DEVTOOLS_LOAD_TOOL_NAME = "chrome_devtools_load";
|
|
1258
|
-
var AVAILABLE_TOOLS_STORE = /* @__PURE__ */ Symbol.for("@narumitw/pi-chrome-devtools.available-tools-store");
|
|
1259
|
-
var sharedGlobal = globalThis;
|
|
1260
|
-
var existingAvailableToolsStore = sharedGlobal[AVAILABLE_TOOLS_STORE];
|
|
1261
|
-
var availableToolsByApi = existingAvailableToolsStore ?? /* @__PURE__ */ new WeakMap();
|
|
1262
|
-
if (!existingAvailableToolsStore) sharedGlobal[AVAILABLE_TOOLS_STORE] = availableToolsByApi;
|
|
1263
|
-
var lazyExposureByApi = /* @__PURE__ */ new WeakMap();
|
|
1264
|
-
var SEARCH_TEXT = {
|
|
1265
|
-
chrome_devtools_list_pages: "list open inspectable chrome browser pages tabs targets",
|
|
1266
|
-
chrome_devtools_select_page: "select choose active chrome browser page tab target",
|
|
1267
|
-
chrome_devtools_navigate: "navigate open create chrome browser page url website",
|
|
1268
|
-
chrome_devtools_evaluate: "evaluate run javascript expression dom inspect chrome browser page",
|
|
1269
|
-
chrome_devtools_screenshot: "capture screenshot png image visual chrome browser page"
|
|
1270
|
-
};
|
|
1271
|
-
function initializeAvailableChromeDevtoolsTools(pi) {
|
|
1272
|
-
if (availableToolsByApi.has(pi)) return;
|
|
1273
|
-
const activeTools = new Set(pi.getActiveTools());
|
|
1274
|
-
setAvailableTools(
|
|
1275
|
-
pi,
|
|
1276
|
-
CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => activeTools.has(name))
|
|
1277
|
-
);
|
|
1278
|
-
}
|
|
1279
|
-
function configureChromeDevtoolsToolExposure(pi, availableTools, model) {
|
|
1280
|
-
const available = setAvailableTools(pi, availableTools);
|
|
1281
|
-
const lazyExposure = supportsNativeDeferredToolLoading(model);
|
|
1282
|
-
lazyExposureByApi.set(pi, lazyExposure);
|
|
1283
|
-
const exposedTools = lazyExposure ? [] : CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
|
|
1284
|
-
const nonCapabilityTools = pi.getActiveTools().filter((name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name));
|
|
1285
|
-
pi.setActiveTools(
|
|
1286
|
-
unique([...nonCapabilityTools, CHROME_DEVTOOLS_LOAD_TOOL_NAME, ...exposedTools])
|
|
1287
|
-
);
|
|
1288
|
-
}
|
|
1289
|
-
function requireEagerChromeDevtoolsToolExposure(pi) {
|
|
1290
|
-
lazyExposureByApi.set(pi, false);
|
|
1291
|
-
const active = pi.getActiveTools();
|
|
1292
|
-
const available = availableChromeDevtoolsTools(pi);
|
|
1293
|
-
pi.setActiveTools(unique([...active, CHROME_DEVTOOLS_LOAD_TOOL_NAME, ...available]));
|
|
1294
|
-
}
|
|
1295
|
-
function applyAvailableChromeDevtoolsTools(pi, availableTools) {
|
|
1296
|
-
const available = setAvailableTools(pi, availableTools);
|
|
1297
|
-
const lazyExposure = lazyExposureByApi.get(pi) === true;
|
|
1298
|
-
const active = pi.getActiveTools().filter(
|
|
1299
|
-
(name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name) || lazyExposure && available.has(name)
|
|
1300
|
-
);
|
|
1301
|
-
const eagerTools = lazyExposure ? [] : CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
|
|
1302
|
-
pi.setActiveTools(unique([...active, CHROME_DEVTOOLS_LOAD_TOOL_NAME, ...eagerTools]));
|
|
1303
|
-
}
|
|
1304
|
-
function chromeDevtoolsToolExposureMode(pi) {
|
|
1305
|
-
return lazyExposureByApi.get(pi) === true ? "native deferred" : "eager";
|
|
1306
|
-
}
|
|
1307
|
-
function supportsNativeDeferredToolLoading(model) {
|
|
1308
|
-
if (!model) return false;
|
|
1309
|
-
if (model.api === "anthropic-messages") {
|
|
1310
|
-
const configured = compatBoolean(model.compat, "supportsToolReferences");
|
|
1311
|
-
if (configured !== void 0) return configured;
|
|
1312
|
-
if (model.provider !== "anthropic" || model.id.includes("haiku")) return false;
|
|
1313
|
-
const version = model.id.match(/^claude-(?:opus|sonnet|fable)-(\d+)(?:-(\d+))?(?:-|$)/);
|
|
1314
|
-
if (!version) return false;
|
|
1315
|
-
const major = Number(version[1]);
|
|
1316
|
-
const minor = version[2] && version[2].length < 8 ? Number(version[2]) : 0;
|
|
1317
|
-
return major > 4 || major === 4 && minor >= 5;
|
|
1318
|
-
}
|
|
1319
|
-
if (model.api === "openai-completions") {
|
|
1320
|
-
return compatString(model.compat, "deferredToolsMode") === "kimi";
|
|
1321
|
-
}
|
|
1322
|
-
if (model.api === "openai-responses" || model.api === "openai-codex-responses") {
|
|
1323
|
-
return compatBoolean(model.compat, "supportsAdditionalTools") === true || compatBoolean(model.compat, "supportsToolSearch") === true;
|
|
1324
|
-
}
|
|
1325
|
-
return false;
|
|
1326
|
-
}
|
|
1327
|
-
function availableChromeDevtoolsTools(pi) {
|
|
1328
|
-
const available = availableToolsByApi.get(pi) ?? /* @__PURE__ */ new Set();
|
|
1329
|
-
return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name));
|
|
1330
|
-
}
|
|
1331
|
-
function createChromeDevtoolsLoadTool(pi) {
|
|
1332
|
-
return defineTool({
|
|
1333
|
-
name: CHROME_DEVTOOLS_LOAD_TOOL_NAME,
|
|
1334
|
-
label: "Chrome DevTools: Load Tools",
|
|
1335
|
-
description: "Find and enable Chrome DevTools browser tools relevant to a task. Loaded tools remain available for the session.",
|
|
1336
|
-
promptSnippet: "Load Chrome DevTools browser capabilities on demand",
|
|
1337
|
-
promptGuidelines: [
|
|
1338
|
-
"Use chrome_devtools_load when a task requires inspecting or controlling a Chrome browser and the needed chrome_devtools_* capability is not active."
|
|
1339
|
-
],
|
|
1340
|
-
parameters: Type.Object({
|
|
1341
|
-
query: Type.String({
|
|
1342
|
-
description: "Browser capability or task to find tools for.",
|
|
1343
|
-
maxLength: 500
|
|
1344
|
-
}),
|
|
1345
|
-
limit: Type.Optional(
|
|
1346
|
-
Type.Integer({
|
|
1347
|
-
description: "Maximum tools to load. Defaults to 3.",
|
|
1348
|
-
minimum: 1,
|
|
1349
|
-
maximum: 5
|
|
1350
|
-
})
|
|
1351
|
-
)
|
|
1352
|
-
}),
|
|
1353
|
-
async execute(_toolCallId, params, signal) {
|
|
1354
|
-
signal?.throwIfAborted();
|
|
1355
|
-
const available = new Set(availableChromeDevtoolsTools(pi));
|
|
1356
|
-
const matches = matchChromeDevtoolsTools(params.query, params.limit ?? 3, available);
|
|
1357
|
-
const active = pi.getActiveTools();
|
|
1358
|
-
const activeSet = new Set(active);
|
|
1359
|
-
const added = matches.filter((name) => !activeSet.has(name));
|
|
1360
|
-
if (added.length > 0) {
|
|
1361
|
-
pi.setActiveTools(unique([...active, ...added]));
|
|
1362
|
-
}
|
|
1363
|
-
const text = matches.length === 0 ? "No available Chrome DevTools tools matched the query." : added.length > 0 ? `Loaded Chrome DevTools tools: ${added.join(", ")}` : `Matching Chrome DevTools tools are already loaded: ${matches.join(", ")}`;
|
|
1364
|
-
return {
|
|
1365
|
-
content: [{ type: "text", text }],
|
|
1366
|
-
details: { matches, added }
|
|
1367
|
-
};
|
|
1368
|
-
}
|
|
1369
|
-
});
|
|
1370
|
-
}
|
|
1371
|
-
function matchChromeDevtoolsTools(query, limit, available) {
|
|
1372
|
-
const terms = query.toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length >= 2);
|
|
1373
|
-
if (terms.length === 0) return [];
|
|
1374
|
-
return CHROME_DEVTOOLS_TOOL_NAMES.filter((name) => available.has(name)).map((name, index) => ({
|
|
1375
|
-
name,
|
|
1376
|
-
index,
|
|
1377
|
-
score: terms.reduce((score, term) => score + (SEARCH_TEXT[name].includes(term) ? 1 : 0), 0)
|
|
1378
|
-
})).filter((match) => match.score > 0).sort((left, right) => right.score - left.score || left.index - right.index).slice(0, limit).map((match) => match.name);
|
|
1379
|
-
}
|
|
1380
|
-
function setAvailableTools(pi, availableTools) {
|
|
1381
|
-
const available = new Set(availableTools);
|
|
1382
|
-
availableToolsByApi.set(pi, available);
|
|
1383
|
-
return available;
|
|
1384
|
-
}
|
|
1385
|
-
function compatBoolean(value, key) {
|
|
1386
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1387
|
-
const record = value;
|
|
1388
|
-
return typeof record[key] === "boolean" ? record[key] : void 0;
|
|
1389
|
-
}
|
|
1390
|
-
function compatString(value, key) {
|
|
1391
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1392
|
-
const record = value;
|
|
1393
|
-
return typeof record[key] === "string" ? record[key] : void 0;
|
|
1394
|
-
}
|
|
1395
|
-
function unique(values) {
|
|
1396
|
-
return [...new Set(values)];
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
// src/tool-selector.ts
|
|
1400
|
-
import { stripVTControlCharacters } from "node:util";
|
|
1401
|
-
function unique2(values) {
|
|
1402
|
-
return Array.from(new Set(values));
|
|
1403
|
-
}
|
|
1404
|
-
function formatError3(error) {
|
|
1405
|
-
return error instanceof Error ? error.message : String(error);
|
|
1406
|
-
}
|
|
1407
|
-
async function updateChromeDevtoolsTools(pi, ctx, selectedTools, action) {
|
|
1408
|
-
const generation = state.sessionGeneration;
|
|
1409
|
-
const result = await transactSelectedTools(pi, ctx, selectedTools, generation);
|
|
1410
|
-
if (result !== "saved" || generation !== state.sessionGeneration) return;
|
|
1411
|
-
const status = await buildToolStatusMessage(pi);
|
|
1412
|
-
if (generation !== state.sessionGeneration) return;
|
|
1413
|
-
ctx.ui.notify(`Chrome DevTools tool catalog ${action}.
|
|
1414
|
-
|
|
1415
|
-
${status}`, "info");
|
|
1416
|
-
}
|
|
1417
|
-
async function setSelectedChromeDevtoolsTools(pi, ctx, selectedTools, expectedActiveTools) {
|
|
1418
|
-
return transactSelectedTools(
|
|
1419
|
-
pi,
|
|
1420
|
-
ctx,
|
|
1421
|
-
selectedTools,
|
|
1422
|
-
state.sessionGeneration,
|
|
1423
|
-
expectedActiveTools
|
|
1424
|
-
);
|
|
1425
|
-
}
|
|
1426
|
-
var toolTransactionQueue = Promise.resolve();
|
|
1427
|
-
async function waitForChromeDevtoolsSettings() {
|
|
1428
|
-
await toolTransactionQueue;
|
|
1429
|
-
}
|
|
1430
|
-
function transactSelectedTools(pi, ctx, selectedTools, expectedGeneration, expectedActiveTools) {
|
|
1431
|
-
const operation = toolTransactionQueue.then(
|
|
1432
|
-
() => transactSelectedToolsNow(pi, ctx, selectedTools, expectedGeneration, expectedActiveTools)
|
|
1433
|
-
);
|
|
1434
|
-
toolTransactionQueue = operation.then(
|
|
1435
|
-
() => void 0,
|
|
1436
|
-
() => void 0
|
|
1437
|
-
);
|
|
1438
|
-
return operation;
|
|
1439
|
-
}
|
|
1440
|
-
async function transactSelectedToolsNow(pi, ctx, selectedTools, expectedGeneration, expectedActiveTools) {
|
|
1441
|
-
if (expectedGeneration !== state.sessionGeneration) return "failed";
|
|
1442
|
-
if (expectedActiveTools && !arraysEqual(availableChromeDevtoolsTools(pi), expectedActiveTools)) {
|
|
1443
|
-
ctx.ui.notify(
|
|
1444
|
-
"Browser tool selection changed while review was open. Review the current state, then apply again.",
|
|
1445
|
-
"warning"
|
|
1446
|
-
);
|
|
1447
|
-
return "active-tools-changed";
|
|
1448
|
-
}
|
|
1449
|
-
const previousActiveTools = pi.getActiveTools();
|
|
1450
|
-
const previousAvailableTools = availableChromeDevtoolsTools(pi);
|
|
1451
|
-
try {
|
|
1452
|
-
applyChromeDevtoolsTools(pi, selectedTools);
|
|
1453
|
-
await persistSettings(selectedTools);
|
|
1454
|
-
return expectedGeneration === state.sessionGeneration ? "saved" : "failed";
|
|
1455
|
-
} catch (error) {
|
|
1456
|
-
let rollbackError;
|
|
1457
|
-
try {
|
|
1458
|
-
applyAvailableChromeDevtoolsTools(pi, previousAvailableTools);
|
|
1459
|
-
const currentNonChromeTools = pi.getActiveTools().filter((name) => !CHROME_DEVTOOLS_TOOL_NAMES.includes(name));
|
|
1460
|
-
const previousLoadedChromeTools = previousActiveTools.filter(
|
|
1461
|
-
(name) => CHROME_DEVTOOLS_TOOL_NAMES.includes(name)
|
|
1462
|
-
);
|
|
1463
|
-
const restoredChromeTools = chromeDevtoolsToolExposureMode(pi) === "eager" ? previousAvailableTools : previousLoadedChromeTools;
|
|
1464
|
-
pi.setActiveTools(unique2([...currentNonChromeTools, ...restoredChromeTools]));
|
|
1465
|
-
} catch (caught) {
|
|
1466
|
-
rollbackError = caught;
|
|
1467
|
-
}
|
|
1468
|
-
if (expectedGeneration !== state.sessionGeneration) return "failed";
|
|
1469
|
-
ctx.ui.notify(
|
|
1470
|
-
sanitizeChromeDevtoolsDisplay(
|
|
1471
|
-
rollbackError ? `Chrome DevTools settings save failed: ${formatError3(error)}; active-tool rollback failed: ${formatError3(rollbackError)}` : `Chrome DevTools settings save failed; active tools restored: ${formatError3(error)}`
|
|
1472
|
-
),
|
|
1473
|
-
"warning"
|
|
1474
|
-
);
|
|
1475
|
-
return "failed";
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
function arraysEqual(left, right) {
|
|
1479
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1480
|
-
}
|
|
1481
|
-
function applyChromeDevtoolsTools(pi, selectedTools) {
|
|
1482
|
-
applyAvailableChromeDevtoolsTools(pi, selectedTools);
|
|
1483
|
-
}
|
|
1484
|
-
function getToolStatusSummary(pi) {
|
|
1485
|
-
const chromeToolNames = new Set(CHROME_DEVTOOLS_TOOL_NAMES);
|
|
1486
|
-
const activeToolNames = new Set(pi.getActiveTools());
|
|
1487
|
-
const loadedChromeToolCount = CHROME_DEVTOOLS_TOOL_NAMES.filter(
|
|
1488
|
-
(name) => activeToolNames.has(name)
|
|
1489
|
-
).length;
|
|
1490
|
-
const availableChromeToolCount = availableChromeDevtoolsTools(pi).length;
|
|
1491
|
-
const activeNonChromeToolCount = Array.from(activeToolNames).filter(
|
|
1492
|
-
(name) => !chromeToolNames.has(name) && name !== CHROME_DEVTOOLS_LOAD_TOOL_NAME
|
|
1493
|
-
).length;
|
|
1494
|
-
const availabilityStatus = availableChromeToolCount === CHROME_DEVTOOLS_TOOL_NAMES.length ? "enabled" : availableChromeToolCount === 0 ? "disabled" : "partial";
|
|
1495
|
-
return {
|
|
1496
|
-
availabilityStatus,
|
|
1497
|
-
availableChromeToolCount,
|
|
1498
|
-
loadedChromeToolCount,
|
|
1499
|
-
activeNonChromeToolCount
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
async function buildToolStatusMessage(pi) {
|
|
1503
|
-
const summary = getToolStatusSummary(pi);
|
|
1504
|
-
const persistedSetting = await persistedSettingLabel();
|
|
1505
|
-
return sanitizeChromeDevtoolsDisplay(
|
|
1506
|
-
[
|
|
1507
|
-
`Chrome DevTools tools available: ${formatRuntimeStatus(summary)}`,
|
|
1508
|
-
`Tool exposure: ${chromeDevtoolsToolExposureMode(pi)}`,
|
|
1509
|
-
`Loaded capability tools this session: ${summary.loadedChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length}`,
|
|
1510
|
-
`Loader: ${pi.getActiveTools().includes(CHROME_DEVTOOLS_LOAD_TOOL_NAME) ? "active" : "inactive"}`,
|
|
1511
|
-
`Persisted tool catalog: ${persistedSetting}`,
|
|
1512
|
-
...browserSettingsStatusLines(),
|
|
1513
|
-
...state.settingsNotice ? [`Settings note: ${state.settingsNotice}`] : [],
|
|
1514
|
-
`Other active tools preserved: ${summary.activeNonChromeToolCount}`,
|
|
1515
|
-
`Endpoint: ${devToolsEndpoint()}`,
|
|
1516
|
-
`Endpoint source: ${endpointSourceLabel()}`,
|
|
1517
|
-
`Launch mode: ${launchModeLabel()}`,
|
|
1518
|
-
...launchAttemptLines()
|
|
1519
|
-
].join("\n")
|
|
1520
|
-
);
|
|
1521
|
-
}
|
|
1522
|
-
function buildQuickstartMessage() {
|
|
1523
|
-
return buildSettingsSetupMessage();
|
|
1524
|
-
}
|
|
1525
|
-
function buildBrowserStatusMessage() {
|
|
1526
|
-
const lifecycle = browserLifecycleState();
|
|
1527
|
-
const browserState = lifecycle === "starting" ? "starting managed browser" : lifecycle === "running" ? "managed browser running" : lifecycle === "exited" ? "managed browser exited" : lifecycle === "failed" ? "last launch failed" : "not started; connection has not been checked";
|
|
1528
|
-
const needsRecovery = lifecycle === "exited" || lifecycle === "failed";
|
|
1529
|
-
return sanitizeChromeDevtoolsDisplay(
|
|
1530
|
-
[
|
|
1531
|
-
`Browser: ${browserState}`,
|
|
1532
|
-
"Viewing this status does not probe the endpoint or launch Chrome.",
|
|
1533
|
-
`Endpoint: ${devToolsEndpoint()}`,
|
|
1534
|
-
`Endpoint source: ${endpointSourceLabel()}`,
|
|
1535
|
-
`Launch mode: ${launchModeLabel()}`,
|
|
1536
|
-
`Unpacked extensions: ${state.extensionPaths.length} (${state.extensionPathsSource})`,
|
|
1537
|
-
...state.extensionPaths.length > 0 ? ["Unpacked extensions execute trusted browser code in an isolated managed browser."] : [],
|
|
1538
|
-
...launchAttemptLines(),
|
|
1539
|
-
...needsRecovery ? [launchHint(), endpointConfigHint()] : []
|
|
1540
|
-
].join("\n")
|
|
1541
|
-
);
|
|
1542
|
-
}
|
|
1543
|
-
function buildSettingsSetupMessage() {
|
|
1544
|
-
return sanitizeChromeDevtoolsDisplay(
|
|
1545
|
-
[
|
|
1546
|
-
`Chrome DevTools endpoint: ${devToolsEndpoint()}`,
|
|
1547
|
-
`Endpoint source: ${endpointSourceLabel()}`,
|
|
1548
|
-
`Launch mode: ${launchModeLabel()}`,
|
|
1549
|
-
...browserSettingsStatusLines(),
|
|
1550
|
-
launchHint(),
|
|
1551
|
-
browserCandidateHint(),
|
|
1552
|
-
...launchAttemptLines(),
|
|
1553
|
-
endpointConfigHint()
|
|
1554
|
-
].join("\n")
|
|
1555
|
-
);
|
|
1556
|
-
}
|
|
1557
|
-
function sanitizeChromeDevtoolsDisplay(value, maxCharacters = 5e4) {
|
|
1558
|
-
const sanitized = Array.from(stripVTControlCharacters(value), (character) => {
|
|
1559
|
-
const codePoint = character.codePointAt(0) ?? 0;
|
|
1560
|
-
const unsafeControl = codePoint >= 0 && codePoint <= 8 || codePoint >= 11 && codePoint <= 31 || codePoint >= 127 && codePoint <= 159;
|
|
1561
|
-
return unsafeControl ? "\uFFFD" : character;
|
|
1562
|
-
}).join("");
|
|
1563
|
-
if (sanitized.length <= maxCharacters) return sanitized;
|
|
1564
|
-
return `${sanitized.slice(0, Math.max(0, maxCharacters - 1))}\u2026`;
|
|
1565
|
-
}
|
|
1566
|
-
function browserSettingsStatusLines() {
|
|
1567
|
-
const extensionLines = state.extensionPaths.length > 0 ? state.extensionPaths.map((extensionPath) => ` - ${extensionPath}`) : [" - none"];
|
|
1568
|
-
return [
|
|
1569
|
-
`Settings file: ${state.settingsFilePath ?? settingsFilePath()} (user)`,
|
|
1570
|
-
...state.projectSettingsFilePath ? [
|
|
1571
|
-
`Project settings: ${state.projectSettingsFilePath} (${state.projectSettingsTrusted ? "trusted" : "untrusted; ignored"})`
|
|
1572
|
-
] : [],
|
|
1573
|
-
`Auto-launch: ${state.autoLaunchEnabled ? "on" : "off"} (${state.autoLaunchSource})`,
|
|
1574
|
-
`Browser executable: ${state.browserExecutable ?? "automatic discovery"} (${state.browserExecutableSource})`,
|
|
1575
|
-
`Unpacked extensions (${state.extensionPathsSource}):`,
|
|
1576
|
-
...extensionLines,
|
|
1577
|
-
"Confirmed menu settings apply before the next browser connection; manual JSON edits require /reload or session replacement.",
|
|
1578
|
-
...state.extensionPaths.length > 0 ? [
|
|
1579
|
-
"Unpacked extensions require Chrome for Testing or Chromium and execute trusted browser code."
|
|
1580
|
-
] : []
|
|
1581
|
-
];
|
|
1582
|
-
}
|
|
1583
|
-
function buildCommandGuide() {
|
|
1584
|
-
return [
|
|
1585
|
-
"Chrome DevTools commands:",
|
|
1586
|
-
"/chrome-devtools \u2014 open this menu",
|
|
1587
|
-
"/chrome-devtools help \u2014 show command usage",
|
|
1588
|
-
"/chrome-devtools quickstart \u2014 show endpoint and launch help",
|
|
1589
|
-
"/chrome-devtools status \u2014 show tool and settings status",
|
|
1590
|
-
"/chrome-devtools settings \u2014 edit browser connection settings",
|
|
1591
|
-
"/chrome-devtools tools \u2014 choose available Chrome DevTools tools",
|
|
1592
|
-
"/chrome-devtools toggle|select \u2014 compatibility aliases for tools",
|
|
1593
|
-
"/chrome-devtools enable|on \u2014 make all Chrome DevTools tools available",
|
|
1594
|
-
"/chrome-devtools disable|off \u2014 make all Chrome DevTools capability tools unavailable"
|
|
1595
|
-
].join("\n");
|
|
1596
|
-
}
|
|
1597
|
-
function allChromeDevtoolsTools() {
|
|
1598
|
-
return [...CHROME_DEVTOOLS_TOOL_NAMES];
|
|
1599
|
-
}
|
|
1600
|
-
function formatRuntimeStatus(summary) {
|
|
1601
|
-
return `${summary.availabilityStatus} (${summary.availableChromeToolCount}/${CHROME_DEVTOOLS_TOOL_NAMES.length} available)`;
|
|
1602
|
-
}
|
|
1603
|
-
async function persistedSettingLabel() {
|
|
1604
|
-
const settings = await loadSettings();
|
|
1605
|
-
if (settings.kind === "loaded" && settings.settings.tools) {
|
|
1606
|
-
return formatPersistedSelection(settings.settings.tools);
|
|
1607
|
-
}
|
|
1608
|
-
if (settings.kind === "invalid") {
|
|
1609
|
-
return `none; current active-tool policy preserved (invalid settings ignored: ${settings.reason})`;
|
|
1610
|
-
}
|
|
1611
|
-
return "none; current active-tool policy preserved";
|
|
1612
|
-
}
|
|
1613
|
-
function formatPersistedSelection(tools) {
|
|
1614
|
-
if (tools.length === CHROME_DEVTOOLS_TOOL_NAMES.length) {
|
|
1615
|
-
return `all available (${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
|
|
1616
|
-
}
|
|
1617
|
-
if (tools.length === 0)
|
|
1618
|
-
return `all unavailable (0/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected)`;
|
|
1619
|
-
return `${tools.length}/${CHROME_DEVTOOLS_TOOL_NAMES.length} selected: ${tools.join(", ")}`;
|
|
1620
|
-
}
|
|
1621
|
-
async function persistSettings(selectedTools) {
|
|
1622
|
-
await saveSettings({ tools: [...selectedTools], updatedAt: Date.now() });
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
1517
|
export {
|
|
1518
|
+
CORE_CHROME_DEVTOOLS_TOOL_NAMES,
|
|
1519
|
+
WEBMCP_TOOL_NAMES,
|
|
1626
1520
|
CHROME_DEVTOOLS_TOOL_NAMES,
|
|
1521
|
+
isWebMcpToolName,
|
|
1627
1522
|
waitForSettingsWrites,
|
|
1628
1523
|
loadSettings,
|
|
1629
1524
|
parseBrowserEndpoint,
|
|
1525
|
+
saveSettings,
|
|
1526
|
+
saveWebMcpSettings,
|
|
1630
1527
|
saveBrowserSettings,
|
|
1528
|
+
settingsFilePath,
|
|
1631
1529
|
DEFAULT_TIMEOUT_MS,
|
|
1632
1530
|
DEFAULT_ENDPOINT_WAIT_MS,
|
|
1633
1531
|
state,
|
|
1532
|
+
setWebMcpSessionOwner,
|
|
1533
|
+
beginWebMcpOperation,
|
|
1534
|
+
invalidateWebMcpOperations,
|
|
1535
|
+
currentWebMcpGeneration,
|
|
1536
|
+
webMcpSessionSignal,
|
|
1537
|
+
applyRuntimeWebMcpSetting,
|
|
1538
|
+
webMcpEnabled,
|
|
1539
|
+
webMcpOperationIsCurrent,
|
|
1634
1540
|
applyRuntimeBrowserSettings,
|
|
1541
|
+
startManagedBrowserSession,
|
|
1542
|
+
syncManagedBrowserSettings,
|
|
1543
|
+
browserSettingsForOwner,
|
|
1544
|
+
managedBrowserForOwner,
|
|
1635
1545
|
ensureDevToolsEndpoint,
|
|
1636
1546
|
shutdownManagedBrowser,
|
|
1637
1547
|
fetchDevToolsJson,
|
|
1638
1548
|
withEndpointRetry,
|
|
1639
1549
|
devToolsEndpoint,
|
|
1550
|
+
endpointSourceLabel,
|
|
1640
1551
|
browserLifecycleState,
|
|
1641
1552
|
launchModeLabel,
|
|
1553
|
+
launchAttemptLines,
|
|
1642
1554
|
launchHint,
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
requireEagerChromeDevtoolsToolExposure,
|
|
1647
|
-
supportsNativeDeferredToolLoading,
|
|
1648
|
-
availableChromeDevtoolsTools,
|
|
1649
|
-
createChromeDevtoolsLoadTool,
|
|
1650
|
-
updateChromeDevtoolsTools,
|
|
1651
|
-
setSelectedChromeDevtoolsTools,
|
|
1652
|
-
waitForChromeDevtoolsSettings,
|
|
1653
|
-
buildToolStatusMessage,
|
|
1654
|
-
buildQuickstartMessage,
|
|
1655
|
-
buildBrowserStatusMessage,
|
|
1656
|
-
sanitizeChromeDevtoolsDisplay,
|
|
1657
|
-
buildCommandGuide,
|
|
1658
|
-
allChromeDevtoolsTools
|
|
1555
|
+
browserCandidateHint,
|
|
1556
|
+
endpointConfigHint,
|
|
1557
|
+
formatPageListItem
|
|
1659
1558
|
};
|
|
1660
|
-
//# sourceMappingURL=chunk-
|
|
1559
|
+
//# sourceMappingURL=chunk-SRT3UUIP.js.map
|