@narumitw/pi-chrome-devtools 0.53.4 → 0.54.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.
Files changed (31) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +34 -16
  3. package/dist/chunks/{browser-settings-menu-FVMCCUPM.ts → browser-settings-menu-7M3VM7QN.ts} +25 -3
  4. package/dist/chunks/browser-settings-menu-7M3VM7QN.ts.map +7 -0
  5. package/dist/chunks/{chunk-V7N3WI5B.ts → chunk-JKPJYUVG.ts} +340 -108
  6. package/dist/chunks/chunk-JKPJYUVG.ts.map +7 -0
  7. package/dist/chunks/{chunk-4B3EQHOH.ts → chunk-JLEJ6WFO.ts} +2 -2
  8. package/dist/chunks/{chunk-LSJSDY2M.ts → chunk-WTMZID3G.ts} +382 -246
  9. package/dist/chunks/chunk-WTMZID3G.ts.map +7 -0
  10. package/dist/chunks/{menu-MTPSLDHZ.ts → menu-HGG26E3X.ts} +7 -7
  11. package/dist/chunks/{menu-MTPSLDHZ.ts.map → menu-HGG26E3X.ts.map} +1 -1
  12. package/dist/chunks/{tools-OVNH45IJ.ts → tools-O5N4KIAK.ts} +3 -3
  13. package/dist/index.ts +45 -21
  14. package/dist/index.ts.map +2 -2
  15. package/docs/browser-setup.md +7 -1
  16. package/package.json +6 -5
  17. package/src/browser-candidates.ts +184 -0
  18. package/src/browser-manager.ts +93 -276
  19. package/src/browser-settings-menu.ts +23 -0
  20. package/src/browser-startup.ts +122 -0
  21. package/src/chrome-devtools.ts +48 -15
  22. package/src/lazy-tools.ts +358 -48
  23. package/src/menu.ts +3 -3
  24. package/src/runtime.ts +2 -2
  25. package/src/settings.ts +65 -1
  26. package/src/tool-selector.ts +95 -40
  27. package/dist/chunks/browser-settings-menu-FVMCCUPM.ts.map +0 -7
  28. package/dist/chunks/chunk-LSJSDY2M.ts.map +0 -7
  29. package/dist/chunks/chunk-V7N3WI5B.ts.map +0 -7
  30. /package/dist/chunks/{chunk-4B3EQHOH.ts.map → chunk-JLEJ6WFO.ts.map} +0 -0
  31. /package/dist/chunks/{tools-OVNH45IJ.ts.map → tools-O5N4KIAK.ts.map} +0 -0
@@ -0,0 +1,122 @@
1
+ import type { ChildProcess } from "node:child_process";
2
+ import type { EventEmitter } from "node:events";
3
+
4
+ const MAX_STARTUP_STDERR_BYTES = 64 * 1024;
5
+
6
+ function handleErrorsUntilClose(emitter: EventEmitter) {
7
+ // Cancellation or readiness can settle before an already queued process/pipe error.
8
+ // Keep the resource safe until close, then remove this lifetime-only handler.
9
+ const ignoreError = () => undefined;
10
+ emitter.on("error", ignoreError);
11
+ emitter.once("close", () => emitter.off("error", ignoreError));
12
+ }
13
+
14
+ export function browserWebSocketIdentity(value: unknown, port: number): string | undefined {
15
+ if (typeof value !== "string") return undefined;
16
+ try {
17
+ const url = new URL(value);
18
+ if (
19
+ url.protocol !== "ws:" ||
20
+ !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) ||
21
+ Number(url.port || "80") !== port ||
22
+ url.username ||
23
+ url.password ||
24
+ url.search ||
25
+ url.hash ||
26
+ !/^\/devtools\/browser\/[^/]+$/.test(url.pathname)
27
+ ) {
28
+ return undefined;
29
+ }
30
+ return url.pathname;
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
35
+
36
+ // A free-port probe cannot reserve the port through Chrome's startup. Use the owned child's
37
+ // announcement, not an arbitrary responding HTTP server, as the browser identity authority.
38
+ export function waitForBrowserStartup(
39
+ child: ChildProcess,
40
+ port: number,
41
+ waitMs: number,
42
+ signal: AbortSignal,
43
+ ): Promise<string> {
44
+ handleErrorsUntilClose(child);
45
+ const stderr = child.stderr;
46
+ if (stderr && !stderr.closed) handleErrorsUntilClose(stderr);
47
+ signal.throwIfAborted();
48
+ if (!stderr) return Promise.reject(new Error("Auto-launched browser has no startup stderr stream."));
49
+
50
+ return new Promise<string>((resolve, reject) => {
51
+ let spawned = false;
52
+ let identity: string | undefined;
53
+ let receivedBytes = 0;
54
+ let pendingLine = "";
55
+ let settled = false;
56
+ let timeout: ReturnType<typeof setTimeout> | undefined;
57
+
58
+ const settle = (error?: Error) => {
59
+ if (settled) return;
60
+ if (!error && (!spawned || !identity)) return;
61
+ settled = true;
62
+ clearTimeout(timeout);
63
+ child.off("spawn", onSpawn);
64
+ child.off("error", onError);
65
+ child.off("exit", onExit);
66
+ child.off("close", onExit);
67
+ stderr.off("data", onData);
68
+ stderr.off("end", onEnd);
69
+ stderr.off("error", onError);
70
+ signal.removeEventListener("abort", onAbort);
71
+ // Keep the pipe drained after startup without retaining or displaying browser logs.
72
+ stderr.resume();
73
+ if (error) reject(error);
74
+ else resolve(identity as string);
75
+ };
76
+ const onError = (error: Error) => settle(error);
77
+ const onAbort = () => settle(new Error("Chrome DevTools browser startup cancelled.", { cause: signal.reason }));
78
+ const onExit = () => settle(new Error("Auto-launched browser exited before DevTools became available."));
79
+ const onSpawn = () => {
80
+ spawned = true;
81
+ timeout = setTimeout(
82
+ () => settle(new Error("Timed out waiting for auto-launched browser DevTools startup announcement.")),
83
+ waitMs,
84
+ );
85
+ settle();
86
+ };
87
+ const inspectLine = (line: string) => {
88
+ const match = /^DevTools listening on (\S+)$/.exec(line.trim());
89
+ if (!match) return;
90
+ identity = browserWebSocketIdentity(match[1], port);
91
+ if (!identity) settle(new Error("Auto-launched browser announced an unexpected DevTools endpoint."));
92
+ else settle();
93
+ };
94
+ const onData = (data: Buffer | string) => {
95
+ receivedBytes += Buffer.byteLength(data);
96
+ if (receivedBytes > MAX_STARTUP_STDERR_BYTES) {
97
+ settle(new Error("Auto-launched browser startup stderr exceeded 64 KB before readiness."));
98
+ return;
99
+ }
100
+ pendingLine += data.toString();
101
+ const lines = pendingLine.split("\n");
102
+ pendingLine = lines.pop() ?? "";
103
+ for (const line of lines) {
104
+ inspectLine(line);
105
+ if (settled) break;
106
+ }
107
+ };
108
+ const onEnd = () => {
109
+ inspectLine(pendingLine);
110
+ if (!identity) settle(new Error("Auto-launched browser closed stderr before its DevTools startup announcement."));
111
+ };
112
+
113
+ child.once("spawn", onSpawn);
114
+ child.once("error", onError);
115
+ child.once("exit", onExit);
116
+ child.once("close", onExit);
117
+ stderr.on("data", onData);
118
+ stderr.once("end", onEnd);
119
+ stderr.once("error", onError);
120
+ signal.addEventListener("abort", onAbort, { once: true });
121
+ });
122
+ }
@@ -3,9 +3,11 @@ import { shutdownManagedBrowser, startManagedBrowserSession, syncManagedBrowserS
3
3
  import { setActivePageId } from "./cdp-client.js";
4
4
  import {
5
5
  availableChromeDevtoolsTools,
6
+ chromeDevtoolsToolMode,
6
7
  configureChromeDevtoolsToolExposure,
7
- createChromeDevtoolsLoadTool,
8
8
  initializeAvailableChromeDevtoolsTools,
9
+ previousChromeDevtoolsTools,
10
+ registerChromeDevtoolsCapabilities,
9
11
  requireEagerChromeDevtoolsToolExposure,
10
12
  setChromeDevtoolsSessionOwner,
11
13
  supportsNativeDeferredToolLoading,
@@ -55,14 +57,15 @@ const COMMAND_COMPLETIONS = [
55
57
  { value: "off", label: "off", description: "Compatibility alias for disable" },
56
58
  ];
57
59
  export default function chromeDevtools(pi: ExtensionAPI) {
58
- pi.registerTool(listPagesTool);
59
- pi.registerTool(selectPageTool);
60
- pi.registerTool(navigateTool);
61
- pi.registerTool(evaluateTool);
62
- pi.registerTool(screenshotTool);
63
- pi.registerTool(webMcpListToolsTool);
64
- pi.registerTool(webMcpCallTool);
65
- pi.registerTool(createChromeDevtoolsLoadTool(pi));
60
+ registerChromeDevtoolsCapabilities(pi, [
61
+ listPagesTool,
62
+ selectPageTool,
63
+ navigateTool,
64
+ evaluateTool,
65
+ screenshotTool,
66
+ webMcpListToolsTool,
67
+ webMcpCallTool,
68
+ ]);
66
69
 
67
70
  pi.registerCommand("chrome-devtools", {
68
71
  description: "Open Chrome DevTools help and tool controls",
@@ -77,7 +80,11 @@ export default function chromeDevtools(pi: ExtensionAPI) {
77
80
 
78
81
  pi.on("session_start", async (_event, ctx) => {
79
82
  const generation = ++state.sessionGeneration;
80
- setChromeDevtoolsSessionOwner(pi, ctx.sessionManager);
83
+ setChromeDevtoolsSessionOwner(pi, ctx.sessionManager, ctx.sessionManager.getBranch());
84
+ // Capture before default initialization: unknown invalid files must not
85
+ // broaden availability, while a restored/current policy survives reload.
86
+ const previousAvailableTools = previousChromeDevtoolsTools(pi);
87
+ const previousMode = chromeDevtoolsToolMode(pi);
81
88
  initializeAvailableChromeDevtoolsTools(pi);
82
89
  setWebMcpSessionOwner(ctx.sessionManager);
83
90
  replaceSessionController("Chrome DevTools session replaced");
@@ -95,6 +102,8 @@ export default function chromeDevtools(pi: ExtensionAPI) {
95
102
  state.activePageId = undefined;
96
103
  state.lastLaunchAttempt = undefined;
97
104
  const projectTrusted = ctx.isProjectTrusted();
105
+ await waitForChromeDevtoolsSettings();
106
+ if (generation !== state.sessionGeneration) return;
98
107
  const settings = await loadSettings({ cwd: ctx.cwd, projectTrusted });
99
108
  if (generation !== state.sessionGeneration) return;
100
109
  applyRuntimeBrowserSettings(settings.effectiveBrowser, settings.paths, projectTrusted);
@@ -110,17 +119,41 @@ export default function chromeDevtools(pi: ExtensionAPI) {
110
119
  "warning",
111
120
  );
112
121
  }
113
- const availableTools =
114
- settings.kind === "loaded" && settings.settings.tools
122
+ // Tool mode/catalog are user-owned; a valid project browser section can
123
+ // make the aggregate result loaded even when its user source is invalid.
124
+ const preserveToolPolicy = settings.userFile.kind === "invalid";
125
+ const availableTools = preserveToolPolicy
126
+ ? previousAvailableTools
127
+ : settings.kind === "loaded" && settings.settings.tools
115
128
  ? settings.settings.tools
116
- : availableChromeDevtoolsTools(pi);
117
- configureChromeDevtoolsToolExposure(pi, availableTools, ctx.model);
129
+ : allChromeDevtoolsTools(ctx.sessionManager);
130
+ const provenanceWarning = configureChromeDevtoolsToolExposure(
131
+ pi,
132
+ availableTools,
133
+ ctx.model,
134
+ preserveToolPolicy ? previousMode : settings.kind === "loaded" ? settings.settings.toolMode : "codemode",
135
+ );
136
+ if (generation !== state.sessionGeneration) return;
137
+ if (provenanceWarning) ctx.ui.notify(provenanceWarning, "warning");
138
+ const active = new Set(pi.getActiveTools());
139
+ if (
140
+ chromeDevtoolsToolMode(pi) === "codemode" &&
141
+ !active.has("codemode") &&
142
+ availableChromeDevtoolsTools(pi).some((name) => !active.has(name))
143
+ ) {
144
+ ctx.ui.notify(
145
+ "Chrome DevTools capabilities require Pi's codemode tool, but it is not active. Add '+codemode' to defaultTools and reload, or choose direct/lazy mode in Chrome DevTools settings. Chrome DevTools does not activate host tools automatically.",
146
+ "warning",
147
+ );
148
+ }
118
149
  });
119
150
 
120
151
  pi.on("model_select", (event, ctx) => {
152
+ const generation = state.sessionGeneration;
121
153
  invalidateWebMcpOperations(ctx.sessionManager, "Chrome DevTools model and tool exposure changed");
122
154
  if (!supportsNativeDeferredToolLoading(event.model)) {
123
- requireEagerChromeDevtoolsToolExposure(pi);
155
+ const provenanceWarning = requireEagerChromeDevtoolsToolExposure(pi);
156
+ if (generation === state.sessionGeneration && provenanceWarning) ctx.ui.notify(provenanceWarning, "warning");
124
157
  }
125
158
  });
126
159