@narumitw/pi-chrome-devtools 0.52.3 → 0.53.0

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.
Files changed (35) hide show
  1. package/README.md +62 -9
  2. package/dist/chunks/{browser-settings-menu-VWCNG4HB.js → browser-settings-menu-N5O65I7S.js} +81 -7
  3. package/dist/chunks/browser-settings-menu-N5O65I7S.js.map +7 -0
  4. package/dist/chunks/chunk-B7B63PWB.js +465 -0
  5. package/dist/chunks/chunk-B7B63PWB.js.map +7 -0
  6. package/dist/chunks/{chunk-RH6KESL3.js → chunk-GVBXT3FB.js} +369 -514
  7. package/dist/chunks/chunk-GVBXT3FB.js.map +7 -0
  8. package/dist/chunks/chunk-RTECHAEZ.js +373 -0
  9. package/dist/chunks/chunk-RTECHAEZ.js.map +7 -0
  10. package/dist/chunks/{menu-PLD3S3EO.js → menu-UBHW6UP2.js} +72 -39
  11. package/dist/chunks/menu-UBHW6UP2.js.map +7 -0
  12. package/dist/chunks/tools-V7OR5KJ3.js +945 -0
  13. package/dist/chunks/tools-V7OR5KJ3.js.map +7 -0
  14. package/dist/index.ts +158 -195
  15. package/dist/index.ts.map +4 -4
  16. package/package.json +4 -3
  17. package/src/browser-manager.ts +282 -109
  18. package/src/browser-settings-menu.ts +98 -4
  19. package/src/cdp-client.ts +329 -83
  20. package/src/chrome-devtools.ts +51 -11
  21. package/src/lazy-tools.ts +37 -4
  22. package/src/menu.ts +80 -39
  23. package/src/render.ts +12 -1
  24. package/src/runtime.ts +92 -0
  25. package/src/settings.ts +47 -10
  26. package/src/tool-names.ts +16 -1
  27. package/src/tool-selector.ts +67 -30
  28. package/src/tools.ts +86 -10
  29. package/src/webmcp/discovery.ts +235 -0
  30. package/src/webmcp/policy.ts +402 -0
  31. package/src/webmcp/protocol.ts +414 -0
  32. package/src/webmcp/tools.ts +190 -0
  33. package/dist/chunks/browser-settings-menu-VWCNG4HB.js.map +0 -7
  34. package/dist/chunks/chunk-RH6KESL3.js.map +0 -7
  35. 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 CHROME_DEVTOOLS_TOOL_NAMES = [
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 = projectMachineSettingsWarnings(parsed, scope, filePath);
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 projectMachineSettingsWarnings(document, scope, filePath) {
182
- if (scope !== "project" || !isRecord(document.browser)) return [];
183
- const browser = document.browser;
184
- return ["endpoint", "autoLaunch", "executablePath"].filter((field) => browser[field] !== void 0).map(
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,57 @@ 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 managedBrowserScope(owner) {
591
+ const existing = managedBrowserScopes.get(owner);
592
+ if (existing) return existing;
593
+ const created = {
594
+ controller: new AbortController(),
595
+ generation: 0,
596
+ port: state.configuredPort
597
+ };
598
+ managedBrowserScopes.set(owner, created);
599
+ return created;
600
+ }
601
+ function startManagedBrowserSession(owner) {
602
+ const scope = managedBrowserScope(owner);
603
+ scope.controller.abort(new DOMException("Chrome DevTools session replaced", "AbortError"));
604
+ scope.controller = new AbortController();
605
+ scope.generation += 1;
606
+ scope.port = state.configuredPort;
607
+ scope.lastLaunchAttempt = void 0;
608
+ }
609
+ function managedBrowserForOwner(owner) {
610
+ if (!owner) return state.managedBrowser;
611
+ const scopedBrowser = managedBrowserScope(owner).managedBrowser;
612
+ if (scopedBrowser) return scopedBrowser;
613
+ return state.managedBrowser?.sessionOwner === owner ? state.managedBrowser : void 0;
614
+ }
615
+ function setManagedBrowser(owner, browser) {
616
+ if (owner) managedBrowserScope(owner).managedBrowser = browser;
617
+ else state.managedBrowser = browser;
618
+ }
619
+ function launchPromiseForOwner(owner) {
620
+ return owner ? managedBrowserScope(owner).launchPromise : state.launchPromise;
621
+ }
622
+ function setLaunchPromise(owner, launchPromise) {
623
+ if (owner) managedBrowserScope(owner).launchPromise = launchPromise;
624
+ else state.launchPromise = launchPromise;
625
+ }
626
+ function managedBrowserPort(owner) {
627
+ return owner ? managedBrowserScope(owner).port : state.port;
628
+ }
629
+ function setManagedBrowserPort(owner, port) {
630
+ if (owner) managedBrowserScope(owner).port = port;
631
+ else state.port = port;
632
+ }
633
+ function lastLaunchAttempt(owner) {
634
+ return owner ? managedBrowserScope(owner).lastLaunchAttempt : state.lastLaunchAttempt;
635
+ }
636
+ function setLastLaunchAttempt(owner, attempt) {
637
+ if (owner) managedBrowserScope(owner).lastLaunchAttempt = attempt;
638
+ else state.lastLaunchAttempt = attempt;
639
+ }
486
640
  function formatError2(error) {
487
641
  return error instanceof Error ? error.message : String(error);
488
642
  }
@@ -492,26 +646,31 @@ function isNodeError2(error) {
492
646
  function normalizePathForComparison(value) {
493
647
  return process.platform === "win32" ? value.toLowerCase() : value;
494
648
  }
495
- async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS) {
649
+ async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS, callerSignal, owner) {
650
+ callerSignal?.throwIfAborted();
496
651
  if (extensionsConfigured()) {
497
652
  validateExtensionLaunchMode();
498
- await ensureManagedBrowserLaunched(waitMs);
653
+ await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
499
654
  return;
500
655
  }
501
656
  if (canAutoLaunchBrowser()) {
502
657
  try {
503
- await withEndpointRetry(() => fetchDevToolsJson("/json/version"), waitMs);
658
+ await withEndpointRetry(
659
+ () => fetchDevToolsJson("/json/version", { signal: callerSignal }, owner),
660
+ waitMs,
661
+ callerSignal
662
+ );
504
663
  return;
505
664
  } catch (error) {
506
665
  if (shouldAutoLaunchAfterEndpointError(error)) {
507
- await ensureManagedBrowserLaunched(waitMs);
666
+ await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
508
667
  return;
509
668
  }
510
669
  throw error;
511
670
  }
512
671
  }
513
672
  try {
514
- await fetchDevToolsJson("/json/version");
673
+ await fetchDevToolsJson("/json/version", { signal: callerSignal }, owner);
515
674
  } catch (error) {
516
675
  if (isRetryableEndpointError(error)) return;
517
676
  throw error;
@@ -534,76 +693,117 @@ function validateExtensionLaunchMode() {
534
693
  );
535
694
  }
536
695
  }
537
- async function ensureManagedBrowserLaunched(waitMs) {
538
- if (state.launchPromise) return state.launchPromise;
539
- if (state.managedBrowser && !state.managedBrowser.exited && state.managedBrowser.ready) return;
540
- const generation = state.sessionGeneration;
541
- const signal = state.sessionController.signal;
542
- throwIfBrowserLaunchCancelled(generation, signal);
543
- const launchPromise = prepareManagedBrowserLaunch(waitMs, generation, signal);
696
+ async function ensureManagedBrowserLaunched(waitMs, callerSignal, owner) {
697
+ const existingLaunch = launchPromiseForOwner(owner);
698
+ if (existingLaunch) {
699
+ await waitForCaller(existingLaunch, callerSignal);
700
+ return;
701
+ }
702
+ const existingBrowser = managedBrowserForOwner(owner);
703
+ if (existingBrowser && !existingBrowser.exited && existingBrowser.ready) return;
704
+ const generation = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
705
+ const signal = owner ? managedBrowserScope(owner).controller.signal : state.sessionController.signal;
706
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
707
+ const launchPromise = prepareManagedBrowserLaunch(waitMs, generation, signal, owner);
544
708
  const wrappedPromise = launchPromise.finally(() => {
545
- if (state.launchPromise === wrappedPromise) state.launchPromise = void 0;
709
+ if (launchPromiseForOwner(owner) === wrappedPromise) setLaunchPromise(owner, void 0);
546
710
  });
547
- state.launchPromise = wrappedPromise;
548
- return wrappedPromise;
711
+ setLaunchPromise(owner, wrappedPromise);
712
+ await waitForCaller(wrappedPromise, callerSignal);
549
713
  }
550
- async function prepareManagedBrowserLaunch(waitMs, generation, signal) {
551
- if (state.managedBrowser) {
552
- await shutdownManagedBrowser(state.managedBrowser, { awaitLaunch: false });
553
- throwIfBrowserLaunchCancelled(generation, signal);
714
+ function waitForCaller(operation, signal) {
715
+ if (!signal) return operation;
716
+ signal.throwIfAborted();
717
+ return new Promise((resolve3, reject) => {
718
+ let settled = false;
719
+ const onAbort = () => settle(() => reject(signal.reason));
720
+ const settle = (callback) => {
721
+ if (settled) return;
722
+ settled = true;
723
+ signal.removeEventListener("abort", onAbort);
724
+ callback();
725
+ };
726
+ signal.addEventListener("abort", onAbort, { once: true });
727
+ operation.then(
728
+ (value) => settle(() => resolve3(value)),
729
+ (error) => settle(() => reject(error))
730
+ );
731
+ });
732
+ }
733
+ async function prepareManagedBrowserLaunch(waitMs, generation, signal, owner) {
734
+ const existingBrowser = managedBrowserForOwner(owner);
735
+ if (existingBrowser) {
736
+ await shutdownManagedBrowser(existingBrowser, { awaitLaunch: false, owner });
737
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
554
738
  }
555
739
  if (state.portConfigured) {
556
- const available = await browserManagerOperations.isPortAvailable(state.host, state.port);
557
- throwIfBrowserLaunchCancelled(generation, signal);
740
+ const available = await browserManagerOperations.isPortAvailable(
741
+ state.host,
742
+ managedBrowserPort(owner)
743
+ );
744
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
558
745
  if (!available) {
559
746
  throw new DevToolsEndpointError(
560
- `Cannot launch the extension-owned browser because explicit port ${state.port} is already in use. Choose a free browser.endpoint port or reset the endpoint to use a dynamic managed port.`
747
+ `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
748
  );
562
749
  }
563
750
  }
564
- return launchManagedBrowser(waitMs, generation, signal);
751
+ return launchManagedBrowser(waitMs, generation, signal, owner);
565
752
  }
566
- async function launchManagedBrowser(waitMs, generation, signal) {
567
- throwIfBrowserLaunchCancelled(generation, signal);
753
+ async function launchManagedBrowser(waitMs, generation, signal, owner) {
754
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
568
755
  const candidateDefinitions = browserCandidateDefinitions();
569
- const candidates = await resolveBrowserCandidates(candidateDefinitions, generation, signal);
570
- throwIfBrowserLaunchCancelled(generation, signal);
571
- state.lastLaunchAttempt = {
756
+ const candidates = await resolveBrowserCandidates(
757
+ candidateDefinitions,
758
+ generation,
759
+ signal,
760
+ owner
761
+ );
762
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
763
+ setLastLaunchAttempt(owner, {
572
764
  candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
573
765
  mode: state.portConfigured ? "explicit-port" : "dynamic-port"
574
- };
766
+ });
575
767
  if (candidates.length === 0) {
576
768
  throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions));
577
769
  }
578
770
  let lastError;
579
771
  for (const candidate of candidates) {
580
- throwIfBrowserLaunchCancelled(generation, signal);
772
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
581
773
  try {
582
774
  if (extensionsConfigured())
583
- await requireSupportedExtensionBrowser(candidate, generation, signal);
584
- await launchBrowserCandidate(candidate, waitMs, generation, signal);
585
- throwIfBrowserLaunchCancelled(generation, signal);
586
- state.lastLaunchAttempt = {
587
- ...state.lastLaunchAttempt,
775
+ await requireSupportedExtensionBrowser(candidate, generation, signal, owner);
776
+ await launchBrowserCandidate(candidate, waitMs, generation, signal, owner);
777
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
778
+ setLastLaunchAttempt(owner, {
779
+ ...lastLaunchAttempt(owner),
780
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
781
+ mode: state.portConfigured ? "explicit-port" : "dynamic-port",
588
782
  selectedCandidate: formatBrowserCandidate(candidate),
589
- userDataDir: state.managedBrowser?.userDataDir
590
- };
783
+ userDataDir: managedBrowserForOwner(owner)?.userDataDir
784
+ });
591
785
  return;
592
786
  } catch (error) {
593
787
  try {
594
- throwIfBrowserLaunchCancelled(generation, signal);
788
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
595
789
  } catch (cancellation) {
596
- state.lastLaunchAttempt = {
597
- ...state.lastLaunchAttempt,
790
+ setLastLaunchAttempt(owner, {
791
+ ...lastLaunchAttempt(owner) ?? {
792
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
793
+ mode: state.portConfigured ? "explicit-port" : "dynamic-port"
794
+ },
598
795
  lastError: formatError2(cancellation)
599
- };
796
+ });
600
797
  throw cancellation;
601
798
  }
602
799
  lastError = error;
603
- state.lastLaunchAttempt = {
604
- ...state.lastLaunchAttempt,
800
+ setLastLaunchAttempt(owner, {
801
+ ...lastLaunchAttempt(owner) ?? {
802
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
803
+ mode: state.portConfigured ? "explicit-port" : "dynamic-port"
804
+ },
605
805
  lastError: formatError2(error)
606
- };
806
+ });
607
807
  }
608
808
  }
609
809
  if (lastError instanceof DevToolsEndpointError && extensionsConfigured()) throw lastError;
@@ -617,17 +817,17 @@ async function launchManagedBrowser(waitMs, generation, signal) {
617
817
  ].filter(Boolean).join("\n")
618
818
  );
619
819
  }
620
- async function requireSupportedExtensionBrowser(candidate, generation, signal) {
820
+ async function requireSupportedExtensionBrowser(candidate, generation, signal, owner) {
621
821
  let version;
622
822
  try {
623
823
  version = (await browserManagerOperations.inspectBrowserVersion(candidate.resolvedExecutable, signal)).trim();
624
824
  } catch (error) {
625
- throwIfBrowserLaunchCancelled(generation, signal);
825
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
626
826
  throw new DevToolsEndpointError(
627
827
  `Unable to identify the configured extension browser ${candidate.resolvedExecutable}: ${formatError2(error)}. Configure Chrome for Testing or Chromium.`
628
828
  );
629
829
  }
630
- throwIfBrowserLaunchCancelled(generation, signal);
830
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
631
831
  const classification = classifyExtensionBrowserVersion(version);
632
832
  if (!classification.supported) {
633
833
  throw new DevToolsEndpointError(
@@ -643,15 +843,15 @@ function classifyExtensionBrowserVersion(version) {
643
843
  const product = version.trim().replace(/\s+\d+(?:\.\d+)*.*$/, "") || "Unknown";
644
844
  return { supported: false, product };
645
845
  }
646
- async function launchBrowserCandidate(candidate, waitMs, generation, signal) {
647
- throwIfBrowserLaunchCancelled(generation, signal);
846
+ async function launchBrowserCandidate(candidate, waitMs, generation, signal, owner) {
847
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
648
848
  const userDataDir = await browserManagerOperations.mkdtemp(
649
849
  join2(tmpdir(), MANAGED_BROWSER_PROFILE_PREFIX)
650
850
  );
651
851
  let managedBrowser;
652
852
  try {
653
- throwIfBrowserLaunchCancelled(generation, signal);
654
- const portArgument = state.portConfigured ? String(state.port) : "0";
853
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
854
+ const portArgument = state.portConfigured ? String(managedBrowserPort(owner)) : "0";
655
855
  const args = buildManagedBrowserLaunchArguments(
656
856
  userDataDir,
657
857
  portArgument,
@@ -666,37 +866,42 @@ async function launchBrowserCandidate(candidate, waitMs, generation, signal) {
666
866
  userDataDir,
667
867
  exited: false,
668
868
  ready: false,
669
- ownerGeneration: generation
869
+ ownerGeneration: generation,
870
+ ...owner ? { sessionOwner: owner } : {}
670
871
  };
671
872
  managedBrowser = launchedBrowser;
672
- state.managedBrowser = launchedBrowser;
873
+ setManagedBrowser(owner, launchedBrowser);
673
874
  child.once("exit", () => {
875
+ if (managedBrowserForOwner(owner) === launchedBrowser && owner) {
876
+ invalidateWebMcpOperations(owner, "Chrome DevTools managed browser exited");
877
+ }
674
878
  launchedBrowser.exited = true;
675
879
  launchedBrowser.ready = false;
676
- if (!state.portConfigured && launchedBrowser.port === state.port) {
677
- state.port = state.configuredPort;
880
+ if (!state.portConfigured && launchedBrowser.port === managedBrowserPort(owner)) {
881
+ setManagedBrowserPort(owner, state.configuredPort);
678
882
  }
679
883
  });
680
884
  await waitForBrowserSpawn(child);
681
- throwIfBrowserLaunchCancelled(generation, signal);
885
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
682
886
  if (state.portConfigured) {
683
- launchedBrowser.port = state.port;
887
+ launchedBrowser.port = managedBrowserPort(owner);
684
888
  } else {
685
889
  launchedBrowser.port = await readManagedBrowserPort(
686
890
  userDataDir,
687
891
  launchedBrowser,
688
892
  waitMs,
689
893
  generation,
690
- signal
894
+ signal,
895
+ owner
691
896
  );
692
- throwIfBrowserLaunchCancelled(generation, signal);
693
- state.port = launchedBrowser.port;
897
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
898
+ setManagedBrowserPort(owner, launchedBrowser.port);
694
899
  }
695
- await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal);
696
- throwIfBrowserLaunchCancelled(generation, signal);
900
+ await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal, owner);
901
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
697
902
  launchedBrowser.ready = true;
698
903
  } catch (error) {
699
- if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false });
904
+ if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false, owner });
700
905
  else {
701
906
  await browserManagerOperations.rm(userDataDir, { recursive: true, force: true }).catch(() => void 0);
702
907
  }
@@ -734,7 +939,7 @@ function waitForBrowserSpawn(child) {
734
939
  child.once("spawn", onSpawn);
735
940
  });
736
941
  }
737
- async function readManagedBrowserPort(userDataDir, managedBrowser, waitMs, generation, signal) {
942
+ async function readManagedBrowserPort(userDataDir, managedBrowser, waitMs, generation, signal, owner) {
738
943
  const activePortFile = join2(userDataDir, DEVTOOLS_ACTIVE_PORT_FILE);
739
944
  const deadline = Date.now() + waitMs;
740
945
  while (true) {
@@ -743,7 +948,7 @@ async function readManagedBrowserPort(userDataDir, managedBrowser, waitMs, gener
743
948
  if (isNodeError2(error) && error.code === "ENOENT") return void 0;
744
949
  throw error;
745
950
  });
746
- throwIfBrowserLaunchCancelled(generation, signal);
951
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
747
952
  const portText = text?.split(/\r?\n/, 1)[0]?.trim();
748
953
  const port = Number(portText);
749
954
  if (Number.isInteger(port) && port > 0) return port;
@@ -760,23 +965,23 @@ async function readManagedBrowserPort(userDataDir, managedBrowser, waitMs, gener
760
965
  await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs), signal);
761
966
  }
762
967
  }
763
- async function waitForDevToolsEndpoint(waitMs, managedBrowser, generation, signal) {
968
+ async function waitForDevToolsEndpoint(waitMs, managedBrowser, generation, signal, owner) {
764
969
  const deadline = Date.now() + waitMs;
765
970
  while (true) {
766
971
  throwIfManagedBrowserExited(managedBrowser);
767
972
  try {
768
- await fetchDevToolsJson("/json/version", { signal });
769
- throwIfBrowserLaunchCancelled(generation, signal);
973
+ await fetchDevToolsJson("/json/version", { signal }, owner);
974
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
770
975
  return;
771
976
  } catch (error) {
772
977
  if (!isRetryableEndpointError(error)) throw error;
773
978
  }
774
- throwIfBrowserLaunchCancelled(generation, signal);
979
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
775
980
  const remainingMs = deadline - Date.now();
776
981
  if (remainingMs <= 0) {
777
982
  throw new DevToolsEndpointError(
778
983
  [
779
- `Timed out waiting for auto-launched browser at ${devToolsEndpoint()}.`,
984
+ `Timed out waiting for auto-launched browser at ${devToolsEndpoint(owner)}.`,
780
985
  launchHint()
781
986
  ].join("\n")
782
987
  );
@@ -788,28 +993,44 @@ function throwIfManagedBrowserExited(managedBrowser) {
788
993
  if (!managedBrowser.exited) return;
789
994
  throw new DevToolsEndpointError("Auto-launched browser exited before DevTools became available.");
790
995
  }
791
- function throwIfBrowserLaunchCancelled(generation, signal) {
792
- if (!state.shuttingDown && generation === state.sessionGeneration && !signal.aborted) {
793
- return;
794
- }
996
+ function throwIfBrowserLaunchCancelled(generation, signal, owner) {
997
+ const currentGeneration = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
998
+ const shuttingDown = owner ? signal.aborted : state.shuttingDown;
999
+ if (!shuttingDown && generation === currentGeneration && !signal.aborted) return;
795
1000
  throw new DevToolsEndpointError(
796
- generation === state.sessionGeneration ? "Chrome DevTools browser launch cancelled during shutdown." : "Chrome DevTools browser launch cancelled because the session was replaced."
1001
+ generation === currentGeneration ? "Chrome DevTools browser launch cancelled during shutdown." : "Chrome DevTools browser launch cancelled because the session was replaced."
797
1002
  );
798
1003
  }
799
- async function shutdownManagedBrowser(managedBrowser = state.managedBrowser, options = {}) {
800
- if (options.cancelLaunch) state.shuttingDown = true;
1004
+ async function shutdownManagedBrowser(managedBrowser, options = {}) {
1005
+ const owner = options.owner ?? managedBrowser?.sessionOwner;
1006
+ if (options.cancelLaunch) {
1007
+ if (owner) {
1008
+ const scope = managedBrowserScope(owner);
1009
+ scope.controller.abort(
1010
+ new DOMException("Chrome DevTools browser launch cancelled during shutdown", "AbortError")
1011
+ );
1012
+ scope.generation += 1;
1013
+ } else {
1014
+ state.shuttingDown = true;
1015
+ }
1016
+ }
1017
+ managedBrowser ??= managedBrowserForOwner(owner);
801
1018
  if (options.awaitLaunch !== false) {
802
- await state.launchPromise?.catch(() => void 0);
803
- managedBrowser = managedBrowser ?? state.managedBrowser;
1019
+ await launchPromiseForOwner(owner)?.catch(() => void 0);
1020
+ managedBrowser ??= managedBrowserForOwner(owner);
804
1021
  }
805
1022
  if (!managedBrowser) return;
806
1023
  if (managedBrowser.cleanupPromise) return managedBrowser.cleanupPromise;
807
- const cleanup = cleanupManagedBrowser(managedBrowser);
1024
+ const cleanup = cleanupManagedBrowser(managedBrowser, owner);
808
1025
  managedBrowser.cleanupPromise = cleanup;
809
1026
  return cleanup;
810
1027
  }
811
- async function cleanupManagedBrowser(managedBrowser) {
812
- if (state.managedBrowser === managedBrowser) state.managedBrowser = void 0;
1028
+ async function cleanupManagedBrowser(managedBrowser, owner) {
1029
+ if (managedBrowserForOwner(owner) === managedBrowser) {
1030
+ if (owner) invalidateWebMcpOperations(owner, "Chrome DevTools managed browser closed");
1031
+ if (state.managedBrowser === managedBrowser) state.managedBrowser = void 0;
1032
+ else setManagedBrowser(owner, void 0);
1033
+ }
813
1034
  if (!managedBrowser.exited) {
814
1035
  killManagedBrowserProcess(managedBrowser);
815
1036
  await waitForManagedBrowserExit(managedBrowser, BROWSER_SHUTDOWN_WAIT_MS).catch(async () => {
@@ -820,8 +1041,8 @@ async function cleanupManagedBrowser(managedBrowser) {
820
1041
  });
821
1042
  }
822
1043
  await browserManagerOperations.rm(managedBrowser.userDataDir, { recursive: true, force: true }).catch(() => void 0);
823
- if (!state.portConfigured && managedBrowser.port === state.port) {
824
- state.port = state.configuredPort;
1044
+ if (!state.portConfigured && managedBrowser.port === managedBrowserPort(owner)) {
1045
+ setManagedBrowserPort(owner, state.configuredPort);
825
1046
  }
826
1047
  }
827
1048
  function killManagedBrowserProcess(managedBrowser, signal) {
@@ -851,8 +1072,8 @@ function waitForManagedBrowserExit(managedBrowser, waitMs) {
851
1072
  managedBrowser.process.once("close", onExitOrClose);
852
1073
  });
853
1074
  }
854
- async function fetchDevToolsJson(path, init) {
855
- const url = `${devToolsEndpoint()}${path}`;
1075
+ async function fetchDevToolsJson(path, init, owner) {
1076
+ const url = `${devToolsEndpoint(owner)}${path}`;
856
1077
  let response;
857
1078
  try {
858
1079
  const timeoutSignal = AbortSignal.timeout(DEFAULT_HTTP_TIMEOUT_MS);
@@ -861,7 +1082,7 @@ async function fetchDevToolsJson(path, init) {
861
1082
  signal: init?.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
862
1083
  });
863
1084
  } catch (error) {
864
- throw new DevToolsEndpointError(endpointConnectionErrorMessage(error), {
1085
+ throw new DevToolsEndpointError(endpointConnectionErrorMessage(error, owner), {
865
1086
  launchable: true,
866
1087
  retryable: true
867
1088
  });
@@ -888,16 +1109,20 @@ async function fetchDevToolsJson(path, init) {
888
1109
  );
889
1110
  }
890
1111
  }
891
- async function withEndpointRetry(operation, waitMs) {
1112
+ async function withEndpointRetry(operation, waitMs, signal) {
892
1113
  const deadline = Date.now() + waitMs;
893
1114
  while (true) {
1115
+ signal?.throwIfAborted();
894
1116
  try {
895
1117
  return await operation();
896
1118
  } catch (error) {
897
1119
  if (!isRetryableEndpointError(error)) throw error;
898
1120
  const remainingMs = deadline - Date.now();
899
1121
  if (remainingMs <= 0) throw error;
900
- await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs));
1122
+ await browserManagerOperations.sleep(
1123
+ Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs),
1124
+ signal
1125
+ );
901
1126
  }
902
1127
  }
903
1128
  }
@@ -918,10 +1143,10 @@ function canAutoLaunchBrowser() {
918
1143
  function extensionsConfigured() {
919
1144
  return state.extensionPaths.length > 0;
920
1145
  }
921
- function endpointConnectionErrorMessage(error) {
1146
+ function endpointConnectionErrorMessage(error, owner) {
922
1147
  const reason = isTimeoutError(error) ? "request timed out" : "connection failed";
923
1148
  return [
924
- `Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint()} (${reason}).`,
1149
+ `Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint(owner)} (${reason}).`,
925
1150
  launchHint(),
926
1151
  endpointConfigHint()
927
1152
  ].join("\n");
@@ -929,8 +1154,8 @@ function endpointConnectionErrorMessage(error) {
929
1154
  function isTimeoutError(error) {
930
1155
  return error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
931
1156
  }
932
- function devToolsEndpoint() {
933
- return `http://${formatHostForUrl(state.host)}:${state.port}`;
1157
+ function devToolsEndpoint(owner) {
1158
+ return `http://${formatHostForUrl(state.host)}:${managedBrowserPort(owner)}`;
934
1159
  }
935
1160
  function formatHostForUrl(host) {
936
1161
  if (host.startsWith("[") && host.endsWith("]")) return host;
@@ -939,43 +1164,41 @@ function formatHostForUrl(host) {
939
1164
  function endpointSourceLabel() {
940
1165
  return state.endpointSource;
941
1166
  }
942
- function browserLifecycleState() {
943
- if (state.launchPromise) return "starting";
944
- if (state.managedBrowser?.exited) return "exited";
945
- if (state.managedBrowser?.ready) return "running";
946
- if (state.lastLaunchAttempt?.lastError) return "failed";
1167
+ function browserLifecycleState(owner) {
1168
+ if (launchPromiseForOwner(owner)) return "starting";
1169
+ if (managedBrowserForOwner(owner)?.exited) return "exited";
1170
+ if (managedBrowserForOwner(owner)?.ready) return "running";
1171
+ if (lastLaunchAttempt(owner)?.lastError) return "failed";
947
1172
  return "unobserved";
948
1173
  }
949
- function launchModeLabel() {
1174
+ function launchModeLabel(owner) {
1175
+ const managedBrowser = managedBrowserForOwner(owner);
950
1176
  if (extensionsConfigured()) {
951
1177
  if (!isLocalDevToolsHost(state.host)) return "invalid; extensions require a local endpoint";
952
1178
  if (!state.autoLaunchEnabled) return "invalid; extensions require auto-launch";
953
- if (state.managedBrowser && !state.managedBrowser.exited) {
1179
+ if (managedBrowser && !managedBrowser.exited) {
954
1180
  return state.portConfigured ? "extension-owned browser on explicit port" : "extension-owned browser on dynamic port";
955
1181
  }
956
1182
  return state.portConfigured ? "force extension-owned launch on explicit port" : "force extension-owned launch on dynamic port";
957
1183
  }
958
1184
  if (!isLocalDevToolsHost(state.host)) return "manual remote endpoint";
959
1185
  if (!state.autoLaunchEnabled) return "manual; auto-launch disabled";
960
- if (state.managedBrowser && !state.managedBrowser.exited) {
1186
+ if (managedBrowser && !managedBrowser.exited) {
961
1187
  return state.portConfigured ? "auto-launched on explicit port" : "auto-launched on dynamic port";
962
1188
  }
963
1189
  return state.portConfigured ? "attach first; auto-launch explicit port" : "attach first; auto-launch dynamic port";
964
1190
  }
965
- function launchAttemptLines() {
966
- if (!state.lastLaunchAttempt) return [];
967
- const lines = [`Last launch attempt: ${state.lastLaunchAttempt.mode}`];
968
- if (state.lastLaunchAttempt.selectedCandidate) {
969
- lines.push(`Launched browser: ${state.lastLaunchAttempt.selectedCandidate}`);
1191
+ function launchAttemptLines(owner) {
1192
+ const attempt = lastLaunchAttempt(owner);
1193
+ if (!attempt) return [];
1194
+ const lines = [`Last launch attempt: ${attempt.mode}`];
1195
+ if (attempt.selectedCandidate) {
1196
+ lines.push(`Launched browser: ${attempt.selectedCandidate}`);
970
1197
  } else {
971
- lines.push(`Tried browser candidates: ${state.lastLaunchAttempt.candidateLabels.join(", ")}`);
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}`);
1198
+ lines.push(`Tried browser candidates: ${attempt.candidateLabels.join(", ")}`);
978
1199
  }
1200
+ if (attempt.userDataDir) lines.push(`Managed browser profile: ${attempt.userDataDir}`);
1201
+ if (attempt.lastError) lines.push(`Last launch error: ${attempt.lastError}`);
979
1202
  return lines;
980
1203
  }
981
1204
  function launchHint() {
@@ -1111,11 +1334,11 @@ function uniqueBrowserCandidates(candidates) {
1111
1334
  return true;
1112
1335
  });
1113
1336
  }
1114
- async function resolveBrowserCandidates(definitions, generation, signal) {
1337
+ async function resolveBrowserCandidates(definitions, generation, signal, owner) {
1115
1338
  const candidates = [];
1116
1339
  for (const definition of definitions) {
1117
1340
  const resolvedExecutable = await resolveBrowserExecutable(definition.executable);
1118
- throwIfBrowserLaunchCancelled(generation, signal);
1341
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
1119
1342
  if (!resolvedExecutable) continue;
1120
1343
  candidates.push({ ...definition, resolvedExecutable });
1121
1344
  }
@@ -1249,412 +1472,44 @@ var DevToolsEndpointError = class extends Error {
1249
1472
  }
1250
1473
  };
1251
1474
 
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
1475
  export {
1476
+ CORE_CHROME_DEVTOOLS_TOOL_NAMES,
1477
+ WEBMCP_TOOL_NAMES,
1626
1478
  CHROME_DEVTOOLS_TOOL_NAMES,
1479
+ isWebMcpToolName,
1627
1480
  waitForSettingsWrites,
1628
1481
  loadSettings,
1629
1482
  parseBrowserEndpoint,
1483
+ saveSettings,
1484
+ saveWebMcpSettings,
1630
1485
  saveBrowserSettings,
1486
+ settingsFilePath,
1631
1487
  DEFAULT_TIMEOUT_MS,
1632
1488
  DEFAULT_ENDPOINT_WAIT_MS,
1633
1489
  state,
1490
+ setWebMcpSessionOwner,
1491
+ beginWebMcpOperation,
1492
+ invalidateWebMcpOperations,
1493
+ currentWebMcpGeneration,
1494
+ webMcpSessionSignal,
1495
+ applyRuntimeWebMcpSetting,
1496
+ webMcpEnabled,
1497
+ webMcpOperationIsCurrent,
1634
1498
  applyRuntimeBrowserSettings,
1499
+ startManagedBrowserSession,
1500
+ managedBrowserForOwner,
1635
1501
  ensureDevToolsEndpoint,
1636
1502
  shutdownManagedBrowser,
1637
1503
  fetchDevToolsJson,
1638
1504
  withEndpointRetry,
1639
1505
  devToolsEndpoint,
1506
+ endpointSourceLabel,
1640
1507
  browserLifecycleState,
1641
1508
  launchModeLabel,
1509
+ launchAttemptLines,
1642
1510
  launchHint,
1643
- formatPageListItem,
1644
- initializeAvailableChromeDevtoolsTools,
1645
- configureChromeDevtoolsToolExposure,
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
1511
+ browserCandidateHint,
1512
+ endpointConfigHint,
1513
+ formatPageListItem
1659
1514
  };
1660
- //# sourceMappingURL=chunk-RH6KESL3.js.map
1515
+ //# sourceMappingURL=chunk-GVBXT3FB.js.map