@narumitw/pi-chrome-devtools 0.52.0 → 0.52.2
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 +120 -145
- package/dist/chunks/browser-settings-menu-VI2I3TXI.js +266 -0
- package/dist/chunks/browser-settings-menu-VI2I3TXI.js.map +7 -0
- package/dist/chunks/chunk-CC37WUUE.js +1607 -0
- package/dist/chunks/chunk-CC37WUUE.js.map +7 -0
- package/dist/chunks/menu-LLR455UX.js +397 -0
- package/dist/chunks/menu-LLR455UX.js.map +7 -0
- package/dist/index.ts +762 -0
- package/dist/index.ts.map +7 -0
- package/package.json +12 -8
package/dist/index.ts
ADDED
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
+
// @ts-nocheck -- the generated entry uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
import {
|
|
4
|
+
CHROME_DEVTOOLS_TOOL_NAMES,
|
|
5
|
+
DEFAULT_ENDPOINT_WAIT_MS,
|
|
6
|
+
DEFAULT_TIMEOUT_MS,
|
|
7
|
+
allChromeDevtoolsTools,
|
|
8
|
+
applyRuntimeBrowserSettings,
|
|
9
|
+
availableChromeDevtoolsTools,
|
|
10
|
+
buildCommandGuide,
|
|
11
|
+
buildQuickstartMessage,
|
|
12
|
+
buildToolStatusMessage,
|
|
13
|
+
configureLazyChromeDevtoolsTools,
|
|
14
|
+
createChromeDevtoolsLoadTool,
|
|
15
|
+
devToolsEndpoint,
|
|
16
|
+
ensureDevToolsEndpoint,
|
|
17
|
+
fetchDevToolsJson,
|
|
18
|
+
formatPageListItem,
|
|
19
|
+
initializeAvailableChromeDevtoolsTools,
|
|
20
|
+
launchHint,
|
|
21
|
+
loadSettings,
|
|
22
|
+
sanitizeChromeDevtoolsDisplay,
|
|
23
|
+
shutdownManagedBrowser,
|
|
24
|
+
state,
|
|
25
|
+
updateChromeDevtoolsTools,
|
|
26
|
+
waitForChromeDevtoolsSettings,
|
|
27
|
+
waitForSettingsWrites,
|
|
28
|
+
withEndpointRetry
|
|
29
|
+
} from "./chunks/chunk-CC37WUUE.js";
|
|
30
|
+
|
|
31
|
+
// src/tools.ts
|
|
32
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
33
|
+
import { Type } from "typebox";
|
|
34
|
+
|
|
35
|
+
// src/cdp-client.ts
|
|
36
|
+
async function listPages(options = {}) {
|
|
37
|
+
const waitMs = options.waitMs ?? DEFAULT_ENDPOINT_WAIT_MS;
|
|
38
|
+
await ensureDevToolsEndpoint(waitMs);
|
|
39
|
+
return withEndpointRetry(async () => {
|
|
40
|
+
const pages = await fetchDevToolsJson("/json/list");
|
|
41
|
+
return pages.filter((page) => page.type === "page" && page.webSocketDebuggerUrl);
|
|
42
|
+
}, waitMs);
|
|
43
|
+
}
|
|
44
|
+
async function getPage(pageId) {
|
|
45
|
+
const pages = await listPages();
|
|
46
|
+
return requirePage(pageId, pages);
|
|
47
|
+
}
|
|
48
|
+
async function resolvePage(pageId) {
|
|
49
|
+
const pages = await listPages();
|
|
50
|
+
if (pageId) return requirePage(pageId, pages);
|
|
51
|
+
const page = resolveDefaultPage(pages);
|
|
52
|
+
if (!page) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
[
|
|
55
|
+
`No Chrome pages found at ${devToolsEndpoint()}.`,
|
|
56
|
+
"Use chrome_devtools_navigate with a URL to create a page, or open a Chrome tab manually.",
|
|
57
|
+
launchHint()
|
|
58
|
+
].join("\n")
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return page;
|
|
62
|
+
}
|
|
63
|
+
async function resolvePageForNavigation(pageId) {
|
|
64
|
+
const pages = await listPages();
|
|
65
|
+
if (pageId) return { created: false, page: requirePage(pageId, pages) };
|
|
66
|
+
const page = resolveDefaultPage(pages);
|
|
67
|
+
if (page) return { created: false, page };
|
|
68
|
+
return { created: true, page: await createPage("about:blank") };
|
|
69
|
+
}
|
|
70
|
+
function resolveDefaultPage(pages) {
|
|
71
|
+
if (!state.activePageId) return pages[0];
|
|
72
|
+
const activePage = pages.find((candidate) => candidate.id === state.activePageId);
|
|
73
|
+
if (activePage) return activePage;
|
|
74
|
+
state.activePageId = void 0;
|
|
75
|
+
return pages[0];
|
|
76
|
+
}
|
|
77
|
+
function requirePage(pageId, pages) {
|
|
78
|
+
const page = pages.find((candidate) => candidate.id === pageId);
|
|
79
|
+
if (page) return page;
|
|
80
|
+
const availablePages = pages.map(formatPageListItem).join("\n");
|
|
81
|
+
throw new Error(
|
|
82
|
+
[
|
|
83
|
+
`Chrome DevTools page not found: ${pageId}.`,
|
|
84
|
+
availablePages ? `Available pages:
|
|
85
|
+
${availablePages}` : "No inspectable Chrome pages are currently available."
|
|
86
|
+
].join("\n")
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
async function createPage(url, options = {}) {
|
|
90
|
+
const waitMs = options.waitMs ?? DEFAULT_ENDPOINT_WAIT_MS;
|
|
91
|
+
await ensureDevToolsEndpoint(waitMs);
|
|
92
|
+
const page = await withEndpointRetry(
|
|
93
|
+
() => fetchDevToolsJson(`/json/new?${encodeURIComponent(url)}`, {
|
|
94
|
+
method: "PUT"
|
|
95
|
+
}),
|
|
96
|
+
waitMs
|
|
97
|
+
);
|
|
98
|
+
if (page.type !== "page" || !page.webSocketDebuggerUrl) {
|
|
99
|
+
throw new Error("Chrome DevTools created a target that is not an inspectable page.");
|
|
100
|
+
}
|
|
101
|
+
return page;
|
|
102
|
+
}
|
|
103
|
+
function formatPage(page) {
|
|
104
|
+
return {
|
|
105
|
+
id: page.id,
|
|
106
|
+
type: page.type,
|
|
107
|
+
title: page.title,
|
|
108
|
+
url: page.url
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function textResult(text, details) {
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text }],
|
|
114
|
+
details
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async function withCdp(page, callback) {
|
|
118
|
+
if (!page.webSocketDebuggerUrl) throw new Error(`Page has no webSocketDebuggerUrl: ${page.id}`);
|
|
119
|
+
const client = await CdpClient.connect(page.webSocketDebuggerUrl);
|
|
120
|
+
try {
|
|
121
|
+
return await callback(client);
|
|
122
|
+
} finally {
|
|
123
|
+
client.close();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
var CdpClient = class _CdpClient {
|
|
127
|
+
#nextId = 1;
|
|
128
|
+
#pending = /* @__PURE__ */ new Map();
|
|
129
|
+
socket;
|
|
130
|
+
constructor(socket) {
|
|
131
|
+
this.socket = socket;
|
|
132
|
+
socket.addEventListener("message", (event) => {
|
|
133
|
+
const response = JSON.parse(String(event.data));
|
|
134
|
+
if (typeof response.id !== "number") return;
|
|
135
|
+
const pending = this.#pending.get(response.id);
|
|
136
|
+
if (!pending) return;
|
|
137
|
+
clearTimeout(pending.timeout);
|
|
138
|
+
this.#pending.delete(response.id);
|
|
139
|
+
if (response.error) {
|
|
140
|
+
pending.reject(new Error(`CDP error ${response.error.code}: ${response.error.message}`));
|
|
141
|
+
} else {
|
|
142
|
+
pending.resolve(response.result);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
socket.addEventListener("close", () => {
|
|
146
|
+
this.rejectAll(new Error("Chrome DevTools WebSocket closed"));
|
|
147
|
+
});
|
|
148
|
+
socket.addEventListener("error", () => {
|
|
149
|
+
this.rejectAll(new Error("Chrome DevTools WebSocket error"));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
static connect(url) {
|
|
153
|
+
return new Promise((resolve2, reject) => {
|
|
154
|
+
const socket = new WebSocket(url);
|
|
155
|
+
const timeout = setTimeout(() => {
|
|
156
|
+
socket.close();
|
|
157
|
+
reject(new Error(`Timed out connecting to Chrome DevTools WebSocket: ${url}`));
|
|
158
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
159
|
+
socket.addEventListener("open", () => {
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
resolve2(new _CdpClient(socket));
|
|
162
|
+
});
|
|
163
|
+
socket.addEventListener("error", () => {
|
|
164
|
+
clearTimeout(timeout);
|
|
165
|
+
reject(new Error(`Failed to connect to Chrome DevTools WebSocket: ${url}`));
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
send(method, params) {
|
|
170
|
+
const id = this.#nextId;
|
|
171
|
+
this.#nextId += 1;
|
|
172
|
+
return new Promise((resolve2, reject) => {
|
|
173
|
+
const timeout = setTimeout(() => {
|
|
174
|
+
this.#pending.delete(id);
|
|
175
|
+
reject(new Error(`Timed out waiting for CDP response: ${method}`));
|
|
176
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
177
|
+
this.#pending.set(id, { resolve: resolve2, reject, timeout });
|
|
178
|
+
this.socket.send(JSON.stringify({ id, method, params: params ?? {} }));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
close() {
|
|
182
|
+
this.socket.close();
|
|
183
|
+
}
|
|
184
|
+
rejectAll(error) {
|
|
185
|
+
for (const [id, pending] of this.#pending) {
|
|
186
|
+
clearTimeout(pending.timeout);
|
|
187
|
+
pending.reject(error);
|
|
188
|
+
this.#pending.delete(id);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/render.ts
|
|
194
|
+
var STATUS_KEY = "chrome-devtools";
|
|
195
|
+
function renderToolCall(action) {
|
|
196
|
+
return () => new PiTextComponent(`Chrome DevTools: ${action}`);
|
|
197
|
+
}
|
|
198
|
+
function renderTextResult(result, options, theme) {
|
|
199
|
+
const output = formatCollapsibleOutput(textContent(result), options);
|
|
200
|
+
return new PiTextComponent(output.text, theme, output.color);
|
|
201
|
+
}
|
|
202
|
+
function renderScreenshotResult(result, options, theme) {
|
|
203
|
+
const output = formatCollapsibleOutput(screenshotTextContent(result), options);
|
|
204
|
+
return new PiTextComponent(output.text, theme, output.color);
|
|
205
|
+
}
|
|
206
|
+
function textContent(result) {
|
|
207
|
+
return result.content.flatMap((content) => content.type === "text" ? [content.text] : []).join("\n");
|
|
208
|
+
}
|
|
209
|
+
function screenshotTextContent(result) {
|
|
210
|
+
const text = textContent(result);
|
|
211
|
+
if (text.trim()) return text;
|
|
212
|
+
const details = result.details;
|
|
213
|
+
if (typeof details?.savedPath !== "string") return text;
|
|
214
|
+
const bytes = typeof details.bytes === "number" ? ` (${details.bytes} bytes)` : "";
|
|
215
|
+
return `Saved screenshot to ${details.savedPath}${bytes}`;
|
|
216
|
+
}
|
|
217
|
+
function formatCollapsibleOutput(text, options) {
|
|
218
|
+
if (options.isPartial) return { text: "Running...", color: "warning" };
|
|
219
|
+
if (options.expanded) return { text, color: "toolOutput" };
|
|
220
|
+
return { text: "" };
|
|
221
|
+
}
|
|
222
|
+
var PiTextComponent = class {
|
|
223
|
+
text;
|
|
224
|
+
theme;
|
|
225
|
+
color;
|
|
226
|
+
constructor(text = "", theme, color) {
|
|
227
|
+
this.text = text;
|
|
228
|
+
this.theme = theme;
|
|
229
|
+
this.color = color;
|
|
230
|
+
}
|
|
231
|
+
setText(text) {
|
|
232
|
+
this.text = text;
|
|
233
|
+
}
|
|
234
|
+
invalidate() {
|
|
235
|
+
}
|
|
236
|
+
render(width) {
|
|
237
|
+
if (!this.text.trim()) return [];
|
|
238
|
+
return this.text.replace(/\t/g, " ").split(/\r?\n/).map((line) => {
|
|
239
|
+
const truncatedLine = truncateLine(line, Math.max(1, width));
|
|
240
|
+
return this.theme && this.color ? this.theme.fg(this.color, truncatedLine) : truncatedLine;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
function truncateLine(line, maxWidth) {
|
|
245
|
+
return Array.from(line).slice(0, maxWidth).join("");
|
|
246
|
+
}
|
|
247
|
+
async function withStatus(ctx, status, callback) {
|
|
248
|
+
ctx.ui.setStatus(STATUS_KEY, status);
|
|
249
|
+
try {
|
|
250
|
+
return await callback();
|
|
251
|
+
} finally {
|
|
252
|
+
ctx.ui.setStatus(STATUS_KEY, void 0);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/screenshot.ts
|
|
257
|
+
import { randomUUID } from "node:crypto";
|
|
258
|
+
import { lstat, mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
259
|
+
import { tmpdir } from "node:os";
|
|
260
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
261
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
262
|
+
function unique(values) {
|
|
263
|
+
return Array.from(new Set(values));
|
|
264
|
+
}
|
|
265
|
+
function isNodeError(error) {
|
|
266
|
+
return error instanceof Error && "code" in error;
|
|
267
|
+
}
|
|
268
|
+
async function saveScreenshot(base64Png, savePath, cwd, signal) {
|
|
269
|
+
const resolvedPath = resolveScreenshotPath(savePath, cwd);
|
|
270
|
+
const pngBytes = Buffer.from(base64Png, "base64");
|
|
271
|
+
await withFileMutationQueue(resolvedPath.path, async () => {
|
|
272
|
+
throwIfAborted(signal);
|
|
273
|
+
await ensureSafeScreenshotParent(resolvedPath);
|
|
274
|
+
await assertSafeScreenshotTargetPath(resolvedPath);
|
|
275
|
+
await writeScreenshotFileSafely(resolvedPath, pngBytes, signal);
|
|
276
|
+
});
|
|
277
|
+
return {
|
|
278
|
+
savedPath: resolvedPath.path,
|
|
279
|
+
bytes: pngBytes.byteLength,
|
|
280
|
+
isDefaultPath: resolvedPath.isDefault
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function resolveScreenshotPath(savePath, cwd) {
|
|
284
|
+
const cwdRoot = resolve(cwd);
|
|
285
|
+
const tempRoot = resolve(tmpdir());
|
|
286
|
+
if (savePath === void 0) {
|
|
287
|
+
return {
|
|
288
|
+
path: join(tempRoot, `pi-chrome-devtools-screenshot-${randomUUID()}.png`),
|
|
289
|
+
allowedRoots: [tempRoot],
|
|
290
|
+
isDefault: true
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const normalizedPath = stripLeadingAtPath(savePath);
|
|
294
|
+
if (!normalizedPath.trim()) {
|
|
295
|
+
throw new Error("Screenshot savePath must not be empty.");
|
|
296
|
+
}
|
|
297
|
+
if (normalizedPath.includes("\0")) {
|
|
298
|
+
throw new Error("Screenshot savePath must not contain NUL bytes.");
|
|
299
|
+
}
|
|
300
|
+
if (hasParentPathSegment(normalizedPath)) {
|
|
301
|
+
throw new Error("Screenshot savePath must not contain '..' path segments.");
|
|
302
|
+
}
|
|
303
|
+
const isAbsolutePath = isAbsolute(normalizedPath);
|
|
304
|
+
const path = isAbsolutePath ? resolve(normalizedPath) : resolve(cwdRoot, normalizedPath);
|
|
305
|
+
const allowedRoots = isAbsolutePath ? unique([cwdRoot, tempRoot]) : [cwdRoot];
|
|
306
|
+
if (!allowedRoots.some((root) => isPathInsideRoot(path, root))) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
"Screenshot savePath must be relative to the current working directory, or an absolute path inside the current working directory or OS temp directory."
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
return { path, allowedRoots, isDefault: false };
|
|
312
|
+
}
|
|
313
|
+
function stripLeadingAtPath(path) {
|
|
314
|
+
return path.startsWith("@") ? path.slice(1) : path;
|
|
315
|
+
}
|
|
316
|
+
function hasParentPathSegment(path) {
|
|
317
|
+
return path.split(/[\\/]+/).some((part) => part === "..");
|
|
318
|
+
}
|
|
319
|
+
async function ensureSafeScreenshotParent(resolvedPath) {
|
|
320
|
+
const parentPath = dirname(resolvedPath.path);
|
|
321
|
+
const rootPath = selectAllowedRoot(parentPath, resolvedPath.allowedRoots);
|
|
322
|
+
if (!rootPath) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
"Screenshot savePath parent must stay inside the current working directory or OS temp directory."
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const realRootPath = await realpath(rootPath);
|
|
328
|
+
let currentPath = rootPath;
|
|
329
|
+
const parentSegments = relative(rootPath, parentPath).split(/[\\/]+/).filter((part) => part.length > 0);
|
|
330
|
+
for (const segment of parentSegments) {
|
|
331
|
+
currentPath = join(currentPath, segment);
|
|
332
|
+
await ensureSafeDirectorySegment(currentPath, realRootPath);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function selectAllowedRoot(path, roots) {
|
|
336
|
+
const matchingRoots = roots.filter((root) => isPathInsideRoot(path, root));
|
|
337
|
+
matchingRoots.sort(
|
|
338
|
+
(left, right) => normalizePathForComparison(resolve(right)).length - normalizePathForComparison(resolve(left)).length
|
|
339
|
+
);
|
|
340
|
+
return matchingRoots[0];
|
|
341
|
+
}
|
|
342
|
+
async function ensureSafeDirectorySegment(path, realRootPath) {
|
|
343
|
+
const existingDirectory = await lstat(path).catch(async (error) => {
|
|
344
|
+
if (!isNodeError(error) || error.code !== "ENOENT") throw error;
|
|
345
|
+
await mkdir(path).catch((mkdirError) => {
|
|
346
|
+
if (!isNodeError(mkdirError) || mkdirError.code !== "EEXIST") throw mkdirError;
|
|
347
|
+
});
|
|
348
|
+
return lstat(path);
|
|
349
|
+
});
|
|
350
|
+
if (existingDirectory.isSymbolicLink()) {
|
|
351
|
+
throw new Error("Screenshot savePath parent directories must not contain symbolic links.");
|
|
352
|
+
}
|
|
353
|
+
if (!existingDirectory.isDirectory()) {
|
|
354
|
+
throw new Error("Screenshot savePath parent must be a directory.");
|
|
355
|
+
}
|
|
356
|
+
await assertPathWithinRealRoot(path, realRootPath);
|
|
357
|
+
}
|
|
358
|
+
async function assertSafeScreenshotTargetPath(resolvedPath) {
|
|
359
|
+
const existingTarget = await lstat(resolvedPath.path).catch((error) => {
|
|
360
|
+
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
361
|
+
throw error;
|
|
362
|
+
});
|
|
363
|
+
if (existingTarget?.isSymbolicLink()) {
|
|
364
|
+
throw new Error("Screenshot savePath must not point to a symbolic link.");
|
|
365
|
+
}
|
|
366
|
+
if (existingTarget?.isDirectory()) {
|
|
367
|
+
throw new Error("Screenshot savePath must point to a file, not a directory.");
|
|
368
|
+
}
|
|
369
|
+
if (existingTarget && !existingTarget.isFile()) {
|
|
370
|
+
throw new Error("Screenshot savePath may only replace regular files.");
|
|
371
|
+
}
|
|
372
|
+
const realAllowedRoots = await Promise.all(resolvedPath.allowedRoots.map(realpathOrResolvedPath));
|
|
373
|
+
const realParent = await realpath(dirname(resolvedPath.path));
|
|
374
|
+
const realTargetPath = join(realParent, basename(resolvedPath.path));
|
|
375
|
+
if (!realAllowedRoots.some((root) => isPathInsideRoot(realTargetPath, root))) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
"Screenshot savePath resolves outside the current working directory or OS temp directory."
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function assertPathWithinRealRoot(path, realRootPath) {
|
|
382
|
+
const realPath = await realpath(path);
|
|
383
|
+
if (!isPathInsideRoot(realPath, realRootPath)) {
|
|
384
|
+
throw new Error(
|
|
385
|
+
"Screenshot savePath parent resolves outside the current working directory or OS temp directory."
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function writeScreenshotFileSafely(resolvedPath, pngBytes, signal) {
|
|
390
|
+
const tempFile = join(
|
|
391
|
+
dirname(resolvedPath.path),
|
|
392
|
+
`.${basename(resolvedPath.path)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`
|
|
393
|
+
);
|
|
394
|
+
try {
|
|
395
|
+
await writeFile(tempFile, pngBytes, { flag: "wx", signal });
|
|
396
|
+
throwIfAborted(signal);
|
|
397
|
+
await replaceScreenshotFile(resolvedPath, tempFile, signal);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
await rm(tempFile, { force: true }).catch(() => void 0);
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function replaceScreenshotFile(resolvedPath, tempFile, signal) {
|
|
404
|
+
try {
|
|
405
|
+
await rename(tempFile, resolvedPath.path);
|
|
406
|
+
return;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (!shouldRetryRenameAfterRemovingDestination(error)) throw error;
|
|
409
|
+
}
|
|
410
|
+
await assertSafeScreenshotTargetPath(resolvedPath);
|
|
411
|
+
throwIfAborted(signal);
|
|
412
|
+
await rm(resolvedPath.path, { force: true });
|
|
413
|
+
await rename(tempFile, resolvedPath.path);
|
|
414
|
+
}
|
|
415
|
+
function shouldRetryRenameAfterRemovingDestination(error) {
|
|
416
|
+
return process.platform === "win32" && isNodeError(error) && ["EACCES", "EEXIST", "EPERM"].includes(error.code ?? "");
|
|
417
|
+
}
|
|
418
|
+
async function realpathOrResolvedPath(path) {
|
|
419
|
+
return realpath(path).catch(() => resolve(path));
|
|
420
|
+
}
|
|
421
|
+
function isPathInsideRoot(path, root) {
|
|
422
|
+
const normalizedPath = normalizePathForComparison(resolve(path));
|
|
423
|
+
const normalizedRoot = normalizePathForComparison(resolve(root));
|
|
424
|
+
if (normalizedPath === normalizedRoot) return true;
|
|
425
|
+
const rootWithSeparator = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
|
|
426
|
+
return normalizedPath.startsWith(rootWithSeparator);
|
|
427
|
+
}
|
|
428
|
+
function normalizePathForComparison(path) {
|
|
429
|
+
return process.platform === "win32" ? path.toLowerCase() : path;
|
|
430
|
+
}
|
|
431
|
+
function throwIfAborted(signal) {
|
|
432
|
+
if (!signal?.aborted) return;
|
|
433
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("Screenshot capture cancelled.");
|
|
434
|
+
}
|
|
435
|
+
function formatScreenshotText(page, screenshot) {
|
|
436
|
+
const pathLabel = screenshot.isDefaultPath ? "Saved to temp file" : "Saved to";
|
|
437
|
+
return [
|
|
438
|
+
`Captured PNG screenshot from ${page.title || page.url || page.id}.`,
|
|
439
|
+
`${pathLabel}: ${screenshot.savedPath}`,
|
|
440
|
+
`Bytes: ${screenshot.bytes}`,
|
|
441
|
+
`Use read({ path: ${JSON.stringify(screenshot.savedPath)} }) to inspect the saved screenshot if inline image content is not available.`
|
|
442
|
+
].join("\n");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/tools.ts
|
|
446
|
+
var listPagesTool = defineTool({
|
|
447
|
+
name: CHROME_DEVTOOLS_TOOL_NAMES[0],
|
|
448
|
+
label: "Chrome DevTools: List Pages",
|
|
449
|
+
description: "List Chrome tabs/pages from a running Chrome DevTools Protocol endpoint.",
|
|
450
|
+
parameters: Type.Object({}),
|
|
451
|
+
renderCall: renderToolCall("list pages"),
|
|
452
|
+
renderResult: renderTextResult,
|
|
453
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
454
|
+
return withStatus(ctx, "list pages", async () => {
|
|
455
|
+
const pages = await listPages();
|
|
456
|
+
return textResult(JSON.stringify(pages.map(formatPage), null, 2), { pages });
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
var selectPageTool = defineTool({
|
|
461
|
+
name: CHROME_DEVTOOLS_TOOL_NAMES[1],
|
|
462
|
+
label: "Chrome DevTools: Select Page",
|
|
463
|
+
description: "Select the active Chrome page for later chrome_devtools_* tool calls.",
|
|
464
|
+
parameters: Type.Object({
|
|
465
|
+
pageId: Type.String({ description: "Page id from chrome_devtools_list_pages." })
|
|
466
|
+
}),
|
|
467
|
+
renderCall: renderToolCall("select page"),
|
|
468
|
+
renderResult: renderTextResult,
|
|
469
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
470
|
+
return withStatus(ctx, "select page", async () => {
|
|
471
|
+
const page = await getPage(params.pageId);
|
|
472
|
+
state.activePageId = page.id;
|
|
473
|
+
return textResult(`Selected page ${page.id}: ${page.title}
|
|
474
|
+
${page.url}`, {
|
|
475
|
+
page: formatPage(page)
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
var navigateTool = defineTool({
|
|
481
|
+
name: CHROME_DEVTOOLS_TOOL_NAMES[2],
|
|
482
|
+
label: "Chrome DevTools: Navigate",
|
|
483
|
+
description: "Navigate a Chrome page to a URL through Chrome DevTools Protocol, creating a page first if none is available.",
|
|
484
|
+
parameters: Type.Object({
|
|
485
|
+
url: Type.String({ description: "URL to navigate to." }),
|
|
486
|
+
pageId: Type.Optional(
|
|
487
|
+
Type.String({ description: "Optional page id. Defaults to selected or first page." })
|
|
488
|
+
)
|
|
489
|
+
}),
|
|
490
|
+
renderCall: renderToolCall("navigate"),
|
|
491
|
+
renderResult: renderTextResult,
|
|
492
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
493
|
+
return withStatus(ctx, "navigate", async () => {
|
|
494
|
+
const { created, page } = await resolvePageForNavigation(params.pageId);
|
|
495
|
+
const result = await withCdp(page, async (client) => {
|
|
496
|
+
await client.send("Page.enable");
|
|
497
|
+
return client.send("Page.navigate", { url: params.url });
|
|
498
|
+
});
|
|
499
|
+
state.activePageId = page.id;
|
|
500
|
+
const action = created ? "Created page and navigated" : "Navigated";
|
|
501
|
+
return textResult(`${action} ${page.id} to ${params.url}`, {
|
|
502
|
+
created,
|
|
503
|
+
page: formatPage(page),
|
|
504
|
+
result
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
var evaluateTool = defineTool({
|
|
510
|
+
name: CHROME_DEVTOOLS_TOOL_NAMES[3],
|
|
511
|
+
label: "Chrome DevTools: Evaluate",
|
|
512
|
+
description: "Evaluate JavaScript in a Chrome page through Chrome DevTools Protocol.",
|
|
513
|
+
parameters: Type.Object({
|
|
514
|
+
expression: Type.String({ description: "JavaScript expression to evaluate." }),
|
|
515
|
+
pageId: Type.Optional(
|
|
516
|
+
Type.String({ description: "Optional page id. Defaults to selected or first page." })
|
|
517
|
+
),
|
|
518
|
+
awaitPromise: Type.Optional(
|
|
519
|
+
Type.Boolean({ description: "Whether to await a returned Promise. Defaults to true." })
|
|
520
|
+
)
|
|
521
|
+
}),
|
|
522
|
+
renderCall: renderToolCall("evaluate"),
|
|
523
|
+
renderResult: renderTextResult,
|
|
524
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
525
|
+
return withStatus(ctx, "evaluate", async () => {
|
|
526
|
+
const page = await resolvePage(params.pageId);
|
|
527
|
+
const result = await withCdp(
|
|
528
|
+
page,
|
|
529
|
+
(client) => client.send("Runtime.evaluate", {
|
|
530
|
+
expression: params.expression,
|
|
531
|
+
awaitPromise: params.awaitPromise ?? true,
|
|
532
|
+
returnByValue: true
|
|
533
|
+
})
|
|
534
|
+
);
|
|
535
|
+
state.activePageId = page.id;
|
|
536
|
+
return textResult(JSON.stringify(result, null, 2), { page: formatPage(page), result });
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
var screenshotTool = defineTool({
|
|
541
|
+
name: CHROME_DEVTOOLS_TOOL_NAMES[4],
|
|
542
|
+
label: "Chrome DevTools: Screenshot",
|
|
543
|
+
description: "Capture a PNG screenshot from a Chrome page through Chrome DevTools Protocol.",
|
|
544
|
+
parameters: Type.Object({
|
|
545
|
+
pageId: Type.Optional(
|
|
546
|
+
Type.String({ description: "Optional page id. Defaults to selected or first page." })
|
|
547
|
+
),
|
|
548
|
+
fullPage: Type.Optional(
|
|
549
|
+
Type.Boolean({ description: "Capture the full document, not just the viewport." })
|
|
550
|
+
),
|
|
551
|
+
savePath: Type.Optional(
|
|
552
|
+
Type.String({
|
|
553
|
+
description: "Screenshot is always saved as a PNG file. Optional output path; omitted defaults to a unique temp file. Relative paths resolve from the current working directory. A single leading @ is stripped to match Pi file-mention paths. Existing regular files are replaced."
|
|
554
|
+
})
|
|
555
|
+
)
|
|
556
|
+
}),
|
|
557
|
+
renderCall: renderToolCall("screenshot"),
|
|
558
|
+
renderResult: renderScreenshotResult,
|
|
559
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
560
|
+
return withStatus(ctx, "screenshot", async () => {
|
|
561
|
+
const page = await resolvePage(params.pageId);
|
|
562
|
+
const result = await withCdp(page, async (client) => {
|
|
563
|
+
throwIfAborted(signal);
|
|
564
|
+
await client.send("Page.enable");
|
|
565
|
+
if (!params.fullPage) {
|
|
566
|
+
return client.send("Page.captureScreenshot", { format: "png" });
|
|
567
|
+
}
|
|
568
|
+
const metrics = await client.send("Page.getLayoutMetrics");
|
|
569
|
+
throwIfAborted(signal);
|
|
570
|
+
return client.send("Page.captureScreenshot", {
|
|
571
|
+
captureBeyondViewport: true,
|
|
572
|
+
format: "png",
|
|
573
|
+
clip: {
|
|
574
|
+
x: metrics.contentSize.x,
|
|
575
|
+
y: metrics.contentSize.y,
|
|
576
|
+
width: metrics.contentSize.width,
|
|
577
|
+
height: metrics.contentSize.height,
|
|
578
|
+
scale: 1
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
state.activePageId = page.id;
|
|
583
|
+
const savedScreenshot = await saveScreenshot(result.data, params.savePath, ctx.cwd, signal);
|
|
584
|
+
return {
|
|
585
|
+
content: [
|
|
586
|
+
{
|
|
587
|
+
type: "text",
|
|
588
|
+
text: formatScreenshotText(page, savedScreenshot)
|
|
589
|
+
},
|
|
590
|
+
{ type: "image", data: result.data, mimeType: "image/png" }
|
|
591
|
+
],
|
|
592
|
+
details: {
|
|
593
|
+
page: formatPage(page),
|
|
594
|
+
bytes: savedScreenshot.bytes,
|
|
595
|
+
savedPath: savedScreenshot.savedPath,
|
|
596
|
+
isDefaultPath: savedScreenshot.isDefaultPath
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// src/chrome-devtools.ts
|
|
604
|
+
var STATUS_KEY2 = "chrome-devtools";
|
|
605
|
+
var COMMAND_COMPLETIONS = [
|
|
606
|
+
{ value: "help", label: "help", description: "Show command usage" },
|
|
607
|
+
{ value: "quickstart", label: "quickstart", description: "Show endpoint and launch help" },
|
|
608
|
+
{ value: "status", label: "status", description: "Show tool and settings status" },
|
|
609
|
+
{ value: "settings", label: "settings", description: "Edit browser connection settings" },
|
|
610
|
+
{ value: "tools", label: "tools", description: "Choose lazy-loadable Chrome DevTools tools" },
|
|
611
|
+
{ value: "toggle", label: "toggle", description: "Alias for tools" },
|
|
612
|
+
{ value: "select", label: "select", description: "Compatibility alias for tools" },
|
|
613
|
+
{ value: "enable", label: "enable", description: "Make all Chrome DevTools tools available" },
|
|
614
|
+
{ value: "on", label: "on", description: "Compatibility alias for enable" },
|
|
615
|
+
{ value: "disable", label: "disable", description: "Make all Chrome DevTools tools unavailable" },
|
|
616
|
+
{ value: "off", label: "off", description: "Compatibility alias for disable" }
|
|
617
|
+
];
|
|
618
|
+
function chromeDevtools(pi) {
|
|
619
|
+
pi.registerTool(listPagesTool);
|
|
620
|
+
pi.registerTool(selectPageTool);
|
|
621
|
+
pi.registerTool(navigateTool);
|
|
622
|
+
pi.registerTool(evaluateTool);
|
|
623
|
+
pi.registerTool(screenshotTool);
|
|
624
|
+
pi.registerTool(createChromeDevtoolsLoadTool(pi));
|
|
625
|
+
pi.registerCommand("chrome-devtools", {
|
|
626
|
+
description: "Open Chrome DevTools help and tool controls",
|
|
627
|
+
getArgumentCompletions: (prefix) => commandCompletions(prefix),
|
|
628
|
+
handler: async (args, ctx) => {
|
|
629
|
+
initializeAvailableChromeDevtoolsTools(pi);
|
|
630
|
+
const generation = state.sessionGeneration;
|
|
631
|
+
await handleChromeDevtoolsCommand(pi, args, ctx, generation);
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
635
|
+
const generation = ++state.sessionGeneration;
|
|
636
|
+
initializeAvailableChromeDevtoolsTools(pi);
|
|
637
|
+
replaceSessionController("Chrome DevTools session replaced");
|
|
638
|
+
state.shuttingDown = false;
|
|
639
|
+
state.settingsNotice = void 0;
|
|
640
|
+
ctx.ui.setStatus(STATUS_KEY2, void 0);
|
|
641
|
+
await shutdownManagedBrowser();
|
|
642
|
+
if (generation !== state.sessionGeneration) return;
|
|
643
|
+
state.activePageId = void 0;
|
|
644
|
+
state.lastLaunchAttempt = void 0;
|
|
645
|
+
const projectTrusted = ctx.isProjectTrusted();
|
|
646
|
+
const settings = await loadSettings({ cwd: ctx.cwd, projectTrusted });
|
|
647
|
+
if (generation !== state.sessionGeneration) return;
|
|
648
|
+
applyRuntimeBrowserSettings(settings.effectiveBrowser, settings.paths, projectTrusted);
|
|
649
|
+
state.settingsNotice = settings.notice;
|
|
650
|
+
for (const warning of settings.warnings) {
|
|
651
|
+
ctx.ui.notify(sanitizeChromeDevtoolsDisplay(warning), "warning");
|
|
652
|
+
}
|
|
653
|
+
const availableTools = settings.kind === "loaded" && settings.settings.tools ? settings.settings.tools : availableChromeDevtoolsTools(pi);
|
|
654
|
+
configureLazyChromeDevtoolsTools(pi, availableTools);
|
|
655
|
+
});
|
|
656
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
657
|
+
state.sessionGeneration += 1;
|
|
658
|
+
replaceSessionController("Chrome DevTools session shut down");
|
|
659
|
+
ctx.ui.setStatus(STATUS_KEY2, void 0);
|
|
660
|
+
const browserShutdown = shutdownManagedBrowser(void 0, { cancelLaunch: true });
|
|
661
|
+
await waitForChromeDevtoolsSettings();
|
|
662
|
+
await waitForSettingsWrites();
|
|
663
|
+
await browserShutdown;
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
async function handleChromeDevtoolsCommand(pi, args, ctx, generation) {
|
|
667
|
+
const command = parseCommand(args);
|
|
668
|
+
switch (command) {
|
|
669
|
+
case "menu":
|
|
670
|
+
await showMenu(pi, ctx, generation);
|
|
671
|
+
return;
|
|
672
|
+
case "help":
|
|
673
|
+
requireObservableUi(ctx, "help");
|
|
674
|
+
ctx.ui.notify(buildCommandGuide(), "info");
|
|
675
|
+
return;
|
|
676
|
+
case "quickstart":
|
|
677
|
+
requireObservableUi(ctx, "quickstart");
|
|
678
|
+
ctx.ui.notify(buildQuickstartMessage(), "info");
|
|
679
|
+
return;
|
|
680
|
+
case "status": {
|
|
681
|
+
requireObservableUi(ctx, "status");
|
|
682
|
+
const status = await buildToolStatusMessage(pi);
|
|
683
|
+
if (generation !== state.sessionGeneration) return;
|
|
684
|
+
ctx.ui.notify(status, "info");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
case "settings": {
|
|
688
|
+
if (!ctx.hasUI || ctx.mode !== "tui" && ctx.mode !== "rpc") {
|
|
689
|
+
throw new Error("/chrome-devtools settings requires TUI or RPC mode");
|
|
690
|
+
}
|
|
691
|
+
const { showChromeDevtoolsBrowserSettings } = await import("./chunks/browser-settings-menu-VI2I3TXI.js");
|
|
692
|
+
if (generation !== state.sessionGeneration) return;
|
|
693
|
+
await showChromeDevtoolsBrowserSettings(ctx, generation);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
case "tools": {
|
|
697
|
+
if (!ctx.hasUI || ctx.mode !== "tui" && ctx.mode !== "rpc") {
|
|
698
|
+
throw new Error("/chrome-devtools tools requires TUI or RPC mode");
|
|
699
|
+
}
|
|
700
|
+
const { showChromeDevtoolsToolWorkflow } = await import("./chunks/menu-LLR455UX.js");
|
|
701
|
+
if (generation !== state.sessionGeneration) return;
|
|
702
|
+
await showChromeDevtoolsToolWorkflow(pi, ctx, generation);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
case "enable":
|
|
706
|
+
await updateChromeDevtoolsTools(pi, ctx, allChromeDevtoolsTools(), "made all available");
|
|
707
|
+
return;
|
|
708
|
+
case "disable":
|
|
709
|
+
await updateChromeDevtoolsTools(pi, ctx, [], "made all unavailable");
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (!ctx.hasUI || ctx.mode !== "tui" && ctx.mode !== "rpc") {
|
|
713
|
+
throw new Error(`Unknown /chrome-devtools command: ${args.trim()}`);
|
|
714
|
+
}
|
|
715
|
+
ctx.ui.notify(
|
|
716
|
+
`Unknown /chrome-devtools command: ${args.trim()}
|
|
717
|
+
|
|
718
|
+
${buildCommandGuide()}`,
|
|
719
|
+
"warning"
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
function requireObservableUi(ctx, route) {
|
|
723
|
+
if (!ctx.hasUI || ctx.mode !== "tui" && ctx.mode !== "rpc") {
|
|
724
|
+
throw new Error(`/chrome-devtools ${route} requires TUI or RPC mode`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
async function showMenu(pi, ctx, generation) {
|
|
728
|
+
if (!ctx.hasUI || ctx.mode !== "tui" && ctx.mode !== "rpc") {
|
|
729
|
+
throw new Error("/chrome-devtools menu requires TUI or RPC mode; use a direct subcommand");
|
|
730
|
+
}
|
|
731
|
+
const { showChromeDevtoolsMenu } = await import("./chunks/menu-LLR455UX.js");
|
|
732
|
+
if (generation !== state.sessionGeneration) return;
|
|
733
|
+
await showChromeDevtoolsMenu(pi, ctx, generation);
|
|
734
|
+
}
|
|
735
|
+
function replaceSessionController(reason) {
|
|
736
|
+
state.sessionController.abort(new DOMException(reason, "AbortError"));
|
|
737
|
+
state.sessionController = new AbortController();
|
|
738
|
+
}
|
|
739
|
+
function parseCommand(args) {
|
|
740
|
+
const command = args.trim().toLowerCase();
|
|
741
|
+
if (!command) return "menu";
|
|
742
|
+
if (command === "help") return "help";
|
|
743
|
+
if (command === "quickstart") return "quickstart";
|
|
744
|
+
if (command === "status") return "status";
|
|
745
|
+
if (command === "settings") return "settings";
|
|
746
|
+
if (command === "tools" || command === "select" || command === "toggle") return "tools";
|
|
747
|
+
if (command === "enable" || command === "on") return "enable";
|
|
748
|
+
if (command === "disable" || command === "off") return "disable";
|
|
749
|
+
return "unknown";
|
|
750
|
+
}
|
|
751
|
+
function commandCompletions(prefix) {
|
|
752
|
+
const normalized = prefix.trimStart().toLowerCase();
|
|
753
|
+
if (/\s/.test(normalized)) return null;
|
|
754
|
+
const matches = COMMAND_COMPLETIONS.filter(
|
|
755
|
+
(completion) => completion.value.startsWith(normalized)
|
|
756
|
+
);
|
|
757
|
+
return matches.length > 0 ? matches : null;
|
|
758
|
+
}
|
|
759
|
+
export {
|
|
760
|
+
chromeDevtools as default
|
|
761
|
+
};
|
|
762
|
+
//# sourceMappingURL=index.ts.map
|