@cotestdev/mcp_playwright 0.0.50 → 0.0.52

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 (34) hide show
  1. package/lib/mcp/browser/browserContextFactory.js +11 -4
  2. package/lib/mcp/browser/browserServerBackend.js +2 -4
  3. package/lib/mcp/browser/config.js +71 -47
  4. package/lib/mcp/browser/context.js +65 -4
  5. package/lib/mcp/browser/logFile.js +96 -0
  6. package/lib/mcp/browser/response.js +107 -104
  7. package/lib/mcp/browser/sessionLog.js +1 -1
  8. package/lib/mcp/browser/tab.js +73 -18
  9. package/lib/mcp/browser/tools/config.js +41 -0
  10. package/lib/mcp/browser/tools/console.js +6 -2
  11. package/lib/mcp/browser/tools/cookies.js +152 -0
  12. package/lib/mcp/browser/tools/install.js +1 -0
  13. package/lib/mcp/browser/tools/network.js +25 -11
  14. package/lib/mcp/browser/tools/pdf.js +3 -4
  15. package/lib/mcp/browser/tools/route.js +140 -0
  16. package/lib/mcp/browser/tools/runCode.js +0 -2
  17. package/lib/mcp/browser/tools/screenshot.js +6 -7
  18. package/lib/mcp/browser/tools/storage.js +3 -4
  19. package/lib/mcp/browser/tools/tracing.js +10 -9
  20. package/lib/mcp/browser/tools/utils.js +0 -6
  21. package/lib/mcp/browser/tools/video.js +31 -13
  22. package/lib/mcp/browser/tools/webstorage.js +223 -0
  23. package/lib/mcp/browser/tools.js +11 -3
  24. package/lib/mcp/extension/cdpRelay.js +7 -7
  25. package/lib/mcp/extension/extensionContextFactory.js +4 -2
  26. package/lib/mcp/program.js +19 -12
  27. package/lib/mcp/terminal/cli.js +23 -2
  28. package/lib/mcp/terminal/command.js +34 -30
  29. package/lib/mcp/terminal/commands.js +310 -38
  30. package/lib/mcp/terminal/daemon.js +23 -38
  31. package/lib/mcp/terminal/helpGenerator.js +8 -6
  32. package/lib/mcp/terminal/program.js +482 -199
  33. package/lib/mcp/terminal/socketConnection.js +17 -2
  34. package/package.json +2 -2
@@ -32,43 +32,66 @@ __export(response_exports, {
32
32
  parseResponse: () => parseResponse,
33
33
  renderTabMarkdown: () => renderTabMarkdown,
34
34
  renderTabsMarkdown: () => renderTabsMarkdown,
35
- requestDebug: () => requestDebug,
36
- serializeResponse: () => serializeResponse
35
+ requestDebug: () => requestDebug
37
36
  });
38
37
  module.exports = __toCommonJS(response_exports);
39
38
  var import_fs = __toESM(require("fs"));
40
39
  var import_path = __toESM(require("path"));
41
40
  var import_utilsBundle = require("playwright-core/lib/utilsBundle");
42
41
  var import_tab = require("./tab");
43
- var import_utils = require("./tools/utils");
44
42
  var import_screenshot = require("./tools/screenshot");
45
43
  const requestDebug = (0, import_utilsBundle.debug)("pw:mcp:request");
46
44
  class Response {
47
- constructor(ordinal, context, toolName, toolArgs) {
45
+ constructor(context, toolName, toolArgs, relativeTo) {
48
46
  this._results = [];
49
47
  this._errors = [];
50
48
  this._code = [];
51
49
  this._includeSnapshot = "none";
50
+ this._imageResults = [];
52
51
  this._context = context;
53
52
  this.toolName = toolName;
54
53
  this.toolArgs = toolArgs;
54
+ this._clientWorkspace = relativeTo ?? context.firstRootPath();
55
55
  }
56
- static {
57
- this._ordinal = 0;
56
+ _computRelativeTo(fileName) {
57
+ if (this._clientWorkspace)
58
+ return import_path.default.relative(this._clientWorkspace, fileName);
59
+ return fileName;
58
60
  }
59
- static create(context, toolName, toolArgs) {
60
- return new Response(++Response._ordinal, context, toolName, toolArgs);
61
+ async resolveClientFile(template, title) {
62
+ let fileName;
63
+ if (template.suggestedFilename)
64
+ fileName = await this._context.workspaceFile(template.suggestedFilename, this._clientWorkspace);
65
+ else
66
+ fileName = await this._context.outputFile(template, { origin: "llm" });
67
+ const relativeName = this._computRelativeTo(fileName);
68
+ const printableLink = `- [${title}](${relativeName})`;
69
+ return { fileName, relativeName, printableLink };
61
70
  }
62
71
  addTextResult(text) {
63
- this._results.push({ title: "", text });
72
+ this._results.push(text);
64
73
  }
65
- addResult(title, data, file) {
66
- this._results.push({
67
- text: typeof data === "string" ? data : void 0,
68
- data: typeof data === "string" ? void 0 : data,
69
- title,
70
- file
71
- });
74
+ async addResult(title, data, file) {
75
+ if (this._context.config.outputMode === "file" || file.suggestedFilename || typeof data !== "string") {
76
+ const resolvedFile = await this.resolveClientFile(file, title);
77
+ await this.addFileResult(resolvedFile, data);
78
+ } else {
79
+ this.addTextResult(data);
80
+ }
81
+ }
82
+ async addFileResult(resolvedFile, data) {
83
+ if (typeof data === "string")
84
+ await import_fs.default.promises.writeFile(resolvedFile.fileName, data, "utf-8");
85
+ else if (data)
86
+ await import_fs.default.promises.writeFile(resolvedFile.fileName, data);
87
+ this.addTextResult(resolvedFile.printableLink);
88
+ }
89
+ addFileLink(title, fileName) {
90
+ const relativeName = this._computRelativeTo(fileName);
91
+ this.addTextResult(`- [${title}](${relativeName})`);
92
+ }
93
+ async registerImageResult(data, imageType) {
94
+ this._imageResults.push({ data, imageType });
72
95
  }
73
96
  addError(error) {
74
97
  this._errors.push(error);
@@ -83,61 +106,90 @@ class Response {
83
106
  this._includeSnapshot = "full";
84
107
  this._includeSnapshotFileName = includeSnapshotFileName;
85
108
  }
86
- async build() {
87
- const rootPath = this._context.firstRootPath();
88
- const sections = [];
89
- const addSection = (title, codeframe) => {
90
- const section = { title, content: [], isError: title === "Error", codeframe };
91
- sections.push(section);
92
- return section.content;
109
+ async serialize() {
110
+ const redactText = (text2) => {
111
+ for (const [secretName, secretValue] of Object.entries(this._context.config.secrets ?? {}))
112
+ text2 = text2.replaceAll(secretValue, `<secret>${secretName}</secret>`);
113
+ return text2;
93
114
  };
94
- if (this._errors.length) {
95
- const content = addSection("Error");
96
- content.push({ text: this._errors.join("\n"), title: "error" });
97
- }
98
- if (this._results.length) {
99
- const content = addSection("Result");
100
- content.push(...this._results);
115
+ const sections = await this._build();
116
+ const text = [];
117
+ for (const section of sections) {
118
+ if (!section.content.length)
119
+ continue;
120
+ text.push(`### ${section.title}`);
121
+ if (section.codeframe)
122
+ text.push(`\`\`\`${section.codeframe}`);
123
+ text.push(...section.content);
124
+ if (section.codeframe)
125
+ text.push("```");
101
126
  }
102
- if (this._context.config.codegen !== "none" && this._code.length) {
103
- const content = addSection("Ran Playwright code", "js");
104
- for (const code of this._code)
105
- content.push({ text: code, title: "code" });
127
+ const content = [
128
+ {
129
+ type: "text",
130
+ text: redactText(text.join("\n"))
131
+ }
132
+ ];
133
+ if (this._context.config.imageResponses !== "omit") {
134
+ for (const imageResult of this._imageResults) {
135
+ const scaledData = (0, import_screenshot.scaleImageToFitMessage)(imageResult.data, imageResult.imageType);
136
+ content.push({ type: "image", data: scaledData.toString("base64"), mimeType: imageResult.imageType === "png" ? "image/png" : "image/jpeg" });
137
+ }
106
138
  }
107
- const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot() : void 0;
139
+ return {
140
+ content,
141
+ ...sections.some((section) => section.isError) ? { isError: true } : {}
142
+ };
143
+ }
144
+ async _build() {
145
+ const sections = [];
146
+ const addSection = (title, content, codeframe) => {
147
+ const section = { title, content, isError: title === "Error", codeframe };
148
+ sections.push(section);
149
+ return content;
150
+ };
151
+ if (this._errors.length)
152
+ addSection("Error", this._errors);
153
+ if (this._results.length)
154
+ addSection("Result", this._results);
155
+ if (this._context.config.codegen !== "none" && this._code.length)
156
+ addSection("Ran Playwright code", this._code, "js");
157
+ const tabSnapshot = this._context.currentTab() ? await this._context.currentTabOrDie().captureSnapshot(this._clientWorkspace) : void 0;
108
158
  const tabHeaders = await Promise.all(this._context.tabs().map((tab) => tab.headerSnapshot()));
109
159
  if (this._includeSnapshot !== "none" || tabHeaders.some((header) => header.changed)) {
110
- if (tabHeaders.length !== 1) {
111
- const content2 = addSection("Open tabs");
112
- content2.push({ text: renderTabsMarkdown(tabHeaders).join("\n"), title: "Open tabs" });
113
- }
114
- const content = addSection("Page");
115
- content.push({ text: renderTabMarkdown(tabHeaders[0]).join("\n"), title: "Page" });
116
- }
117
- if (tabSnapshot?.modalStates.length) {
118
- const content = addSection("Modal state");
119
- content.push({ text: (0, import_tab.renderModalStates)(this._context.config, tabSnapshot.modalStates).join("\n"), title: "Modal state" });
160
+ if (tabHeaders.length !== 1)
161
+ addSection("Open tabs", renderTabsMarkdown(tabHeaders));
162
+ addSection("Page", renderTabMarkdown(tabHeaders[0]));
120
163
  }
164
+ if (tabSnapshot?.modalStates.length)
165
+ addSection("Modal state", (0, import_tab.renderModalStates)(this._context.config, tabSnapshot.modalStates));
121
166
  if (tabSnapshot && this._includeSnapshot !== "none") {
122
- const content = addSection("Snapshot", "yaml");
123
167
  const snapshot = this._includeSnapshot === "full" ? tabSnapshot.ariaSnapshot : tabSnapshot.ariaSnapshotDiff ?? tabSnapshot.ariaSnapshot;
124
- content.push({ text: snapshot, title: "snapshot", file: { prefix: "page", ext: "yml", suggestedFilename: this._includeSnapshotFileName } });
168
+ if (this._context.config.outputMode === "file" || this._includeSnapshotFileName) {
169
+ const resolvedFile = await this.resolveClientFile({ prefix: "page", ext: "yml", suggestedFilename: this._includeSnapshotFileName }, "Snapshot");
170
+ await import_fs.default.promises.writeFile(resolvedFile.fileName, snapshot, "utf-8");
171
+ addSection("Snapshot", [resolvedFile.printableLink]);
172
+ } else {
173
+ addSection("Snapshot", [snapshot], "yaml");
174
+ }
125
175
  }
176
+ const text = [];
177
+ if (tabSnapshot?.consoleLink)
178
+ text.push(`- New console entries: ${tabSnapshot.consoleLink}`);
126
179
  if (tabSnapshot?.events.filter((event) => event.type !== "request").length) {
127
- const content = addSection("Events");
128
- const text = [];
129
180
  for (const event of tabSnapshot.events) {
130
- if (event.type === "console") {
181
+ if (event.type === "console" && this._context.config.outputMode !== "file") {
131
182
  if ((0, import_tab.shouldIncludeMessage)(this._context.config.console.level, event.message.type))
132
183
  text.push(`- ${trimMiddle(event.message.toString(), 100)}`);
133
184
  } else if (event.type === "download-start") {
134
185
  text.push(`- Downloading file ${event.download.download.suggestedFilename()} ...`);
135
186
  } else if (event.type === "download-finish") {
136
- text.push(`- Downloaded file ${event.download.download.suggestedFilename()} to "${rootPath ? import_path.default.relative(rootPath, event.download.outputFile) : event.download.outputFile}"`);
187
+ text.push(`- Downloaded file ${event.download.download.suggestedFilename()} to "${this._computRelativeTo(event.download.outputFile)}"`);
137
188
  }
138
189
  }
139
- content.push({ text: text.join("\n"), title: "events" });
140
190
  }
191
+ if (text.length)
192
+ addSection("Events", text);
141
193
  return sections;
142
194
  }
143
195
  }
@@ -145,6 +197,8 @@ function renderTabMarkdown(tab) {
145
197
  const lines = [`- Page URL: ${tab.url}`];
146
198
  if (tab.title)
147
199
  lines.push(`- Page Title: ${tab.title}`);
200
+ if (tab.console.errors || tab.console.warnings)
201
+ lines.push(`- Console: ${tab.console.errors} errors, ${tab.console.warnings} warnings`);
148
202
  return lines;
149
203
  }
150
204
  function renderTabsMarkdown(tabs) {
@@ -176,56 +230,6 @@ function parseSections(text) {
176
230
  }
177
231
  return sections;
178
232
  }
179
- async function serializeResponse(context, sections, relativeTo) {
180
- const redactText = (text2) => {
181
- for (const [secretName, secretValue] of Object.entries(context.config.secrets ?? {}))
182
- text2 = text2.replaceAll(secretValue, `<secret>${secretName}</secret>`);
183
- return text2;
184
- };
185
- const text = [];
186
- for (const section of sections) {
187
- text.push(`### ${section.title}`);
188
- const codeframe = [];
189
- for (const result of section.content) {
190
- if (result.file && (result.file.suggestedFilename || context.config.outputMode === "file" || result.data)) {
191
- const generatedFileName = await context.outputFile((0, import_utils.dateAsFileName)(result.file.prefix, result.file.ext), { origin: "code", title: section.title });
192
- const fileName = result.file.suggestedFilename ? await context.outputFile(result.file.suggestedFilename, { origin: "llm", title: section.title }) : generatedFileName;
193
- text.push(`- [${result.title}](${relativeTo ? import_path.default.relative(relativeTo, fileName) : fileName})`);
194
- if (result.data)
195
- await import_fs.default.promises.writeFile(fileName, result.data, "utf-8");
196
- else
197
- await import_fs.default.promises.writeFile(fileName, result.text);
198
- } else {
199
- if (result.text !== void 0)
200
- codeframe.push(result.text);
201
- }
202
- }
203
- if (codeframe?.length) {
204
- if (section.codeframe)
205
- text.push(`\`\`\`${section.codeframe}
206
- ${codeframe.join("\n")}
207
- \`\`\``);
208
- else
209
- text.push(codeframe.join("\n"));
210
- }
211
- }
212
- const content = [
213
- {
214
- type: "text",
215
- text: redactText(text.join("\n"))
216
- }
217
- ];
218
- if (context.config.imageResponses !== "omit") {
219
- for (const result of sections.flatMap((section) => section.content).filter((result2) => result2.file?.contentType)) {
220
- const scaledData = (0, import_screenshot.scaleImageToFitMessage)(result.data, result.file.contentType === "image/png" ? "png" : "jpeg");
221
- content.push({ type: "image", data: scaledData.toString("base64"), mimeType: result.file.contentType });
222
- }
223
- }
224
- return {
225
- content,
226
- ...sections.some((section) => section.isError) ? { isError: true } : {}
227
- };
228
- }
229
233
  function parseResponse(response) {
230
234
  if (response.content?.[0].type !== "text")
231
235
  return void 0;
@@ -262,6 +266,5 @@ function parseResponse(response) {
262
266
  parseResponse,
263
267
  renderTabMarkdown,
264
268
  renderTabsMarkdown,
265
- requestDebug,
266
- serializeResponse
269
+ requestDebug
267
270
  });
@@ -42,7 +42,7 @@ class SessionLog {
42
42
  this._file = import_path.default.join(this._folder, "session.md");
43
43
  }
44
44
  static async create(config, clientInfo) {
45
- const sessionFolder = await (0, import_config.outputFile)(config, clientInfo, `session-${Date.now()}`, { origin: "code", title: "Saving session" });
45
+ const sessionFolder = await (0, import_config.outputFile)(config, clientInfo, `session-${Date.now()}`, { origin: "code" });
46
46
  await import_fs.default.promises.mkdir(sessionFolder, { recursive: true });
47
47
  console.error(`Session: ${sessionFolder}`);
48
48
  return new SessionLog(sessionFolder);
@@ -19,7 +19,6 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
19
19
  var tab_exports = {};
20
20
  __export(tab_exports, {
21
21
  Tab: () => Tab,
22
- TabEvents: () => TabEvents,
23
22
  renderModalStates: () => renderModalStates,
24
23
  shouldIncludeMessage: () => shouldIncludeMessage
25
24
  });
@@ -28,6 +27,7 @@ var import_events = require("events");
28
27
  var import_utils = require("playwright-core/lib/utils");
29
28
  var import_utils2 = require("./tools/utils");
30
29
  var import_log = require("../log");
30
+ var import_logFile = require("./logFile");
31
31
  var import_dialogs = require("./tools/dialogs");
32
32
  var import_files = require("./tools/files");
33
33
  var import_transform = require("../../transform/transform");
@@ -37,13 +37,12 @@ const TabEvents = {
37
37
  class Tab extends import_events.EventEmitter {
38
38
  constructor(context, page, onPageClose) {
39
39
  super();
40
- this._lastHeader = { title: "about:blank", url: "about:blank", current: false };
40
+ this._lastHeader = { title: "about:blank", url: "about:blank", current: false, console: { total: 0, warnings: 0, errors: 0 } };
41
41
  this._consoleMessages = [];
42
42
  this._downloads = [];
43
- this._requests = /* @__PURE__ */ new Set();
43
+ this._requests = [];
44
44
  this._modalStates = [];
45
45
  this._needsFullSnapshot = false;
46
- this._eventEntries = [];
47
46
  this._recentEventEntries = [];
48
47
  this.context = context;
49
48
  this.page = page;
@@ -51,6 +50,8 @@ class Tab extends import_events.EventEmitter {
51
50
  page.on("console", (event) => this._handleConsoleMessage(messageToConsoleMessage(event)));
52
51
  page.on("pageerror", (error) => this._handleConsoleMessage(pageErrorToConsoleMessage(error)));
53
52
  page.on("request", (request) => this._handleRequest(request));
53
+ page.on("response", (response) => this._handleResponse(response));
54
+ page.on("requestfailed", (request) => this._handleRequestFailed(request));
54
55
  page.on("close", () => this._onClose());
55
56
  page.on("filechooser", (chooser) => {
56
57
  this.setModalState({
@@ -67,6 +68,8 @@ class Tab extends import_events.EventEmitter {
67
68
  page.setDefaultNavigationTimeout(this.context.config.timeouts.navigation);
68
69
  page.setDefaultTimeout(this.context.config.timeouts.action);
69
70
  page[tabSymbol] = this;
71
+ const wallTime = Date.now();
72
+ this._consoleLog = new import_logFile.LogFile(this.context, wallTime, "console", "Console");
70
73
  this._initializedPromise = this._initialize();
71
74
  }
72
75
  static forPage(page) {
@@ -86,8 +89,8 @@ class Tab extends import_events.EventEmitter {
86
89
  for (const message of await Tab.collectConsoleMessages(this.page))
87
90
  this._handleConsoleMessage(message);
88
91
  const requests = await this.page.requests().catch(() => []);
89
- for (const request of requests)
90
- this._requests.add(request);
92
+ for (const request of requests.filter((r) => r.existingResponse() || r.failure()))
93
+ this._requests.push(request);
91
94
  for (const initPage of this.context.config.browser.initPage || []) {
92
95
  try {
93
96
  const { default: func } = await (0, import_transform.requireOrImport)(initPage);
@@ -116,10 +119,11 @@ class Tab extends import_events.EventEmitter {
116
119
  });
117
120
  }
118
121
  async _downloadStarted(download) {
122
+ const outputFile = await this.context.outputFile({ suggestedFilename: sanitizeForFilePath(download.suggestedFilename()), prefix: "download", ext: "bin" }, { origin: "code" });
119
123
  const entry = {
120
124
  download,
121
125
  finished: false,
122
- outputFile: await this.context.outputFile(download.suggestedFilename(), { origin: "web", title: "Saving download" })
126
+ outputFile
123
127
  };
124
128
  this._downloads.push(entry);
125
129
  this._addLogEntry({ type: "download-start", wallTime: Date.now(), download: entry });
@@ -130,20 +134,40 @@ class Tab extends import_events.EventEmitter {
130
134
  _clearCollectedArtifacts() {
131
135
  this._consoleMessages.length = 0;
132
136
  this._downloads.length = 0;
133
- this._requests.clear();
134
- this._eventEntries.length = 0;
137
+ this._requests.length = 0;
135
138
  this._recentEventEntries.length = 0;
139
+ this._resetLogs();
140
+ }
141
+ _resetLogs() {
142
+ const wallTime = Date.now();
143
+ this._consoleLog.stop();
144
+ this._consoleLog = new import_logFile.LogFile(this.context, wallTime, "console", "Console");
136
145
  }
137
146
  _handleRequest(request) {
138
- this._requests.add(request);
139
- this._addLogEntry({ type: "request", wallTime: Date.now(), request });
147
+ this._requests.push(request);
148
+ const wallTime = request.timing().startTime || Date.now();
149
+ this._addLogEntry({ type: "request", wallTime, request });
150
+ }
151
+ _handleResponse(response) {
152
+ const timing = response.request().timing();
153
+ const wallTime = timing.responseStart + timing.startTime;
154
+ this._addLogEntry({ type: "request", wallTime, request: response.request() });
155
+ }
156
+ _handleRequestFailed(request) {
157
+ this._requests.push(request);
158
+ const timing = request.timing();
159
+ const wallTime = timing.responseEnd + timing.startTime;
160
+ this._addLogEntry({ type: "request", wallTime, request });
140
161
  }
141
162
  _handleConsoleMessage(message) {
142
163
  this._consoleMessages.push(message);
143
- this._addLogEntry({ type: "console", wallTime: Date.now(), message });
164
+ const wallTime = message.timestamp;
165
+ this._addLogEntry({ type: "console", wallTime, message });
166
+ const level = consoleLevelForMessageType(message.type);
167
+ if (level === "error" || level === "warning")
168
+ this._consoleLog.appendLine(wallTime, () => message.toString());
144
169
  }
145
170
  _addLogEntry(entry) {
146
- this._eventEntries.push(entry);
147
171
  this._recentEventEntries.push(entry);
148
172
  }
149
173
  _onClose() {
@@ -155,8 +179,14 @@ class Tab extends import_events.EventEmitter {
155
179
  await this._raceAgainstModalStates(async () => {
156
180
  title = await (0, import_utils2.callOnPageNoTrace)(this.page, (page) => page.title());
157
181
  });
158
- if (this._lastHeader.title !== title || this._lastHeader.url !== this.page.url() || this._lastHeader.current !== this.isCurrentTab()) {
159
- this._lastHeader = { title: title ?? "", url: this.page.url(), current: this.isCurrentTab() };
182
+ const newHeader = {
183
+ title: title ?? "",
184
+ url: this.page.url(),
185
+ current: this.isCurrentTab(),
186
+ console: await this.consoleMessageCount()
187
+ };
188
+ if (!tabHeaderEquals(this._lastHeader, newHeader)) {
189
+ this._lastHeader = newHeader;
160
190
  return { ...this._lastHeader, changed: true };
161
191
  }
162
192
  return { ...this._lastHeader, changed: false };
@@ -188,6 +218,18 @@ class Tab extends import_events.EventEmitter {
188
218
  }
189
219
  await this.waitForLoadState("load", { timeout: 5e3 });
190
220
  }
221
+ async consoleMessageCount() {
222
+ await this._initializedPromise;
223
+ let errors = 0;
224
+ let warnings = 0;
225
+ for (const message of this._consoleMessages) {
226
+ if (message.type === "error")
227
+ errors++;
228
+ else if (message.type === "warning")
229
+ warnings++;
230
+ }
231
+ return { total: this._consoleMessages.length, errors, warnings };
232
+ }
191
233
  async consoleMessages(level) {
192
234
  await this._initializedPromise;
193
235
  return this._consoleMessages.filter((message) => shouldIncludeMessage(level, message.type));
@@ -202,9 +244,9 @@ class Tab extends import_events.EventEmitter {
202
244
  }
203
245
  async clearRequests() {
204
246
  await this._initializedPromise;
205
- this._requests.clear();
247
+ this._requests.length = 0;
206
248
  }
207
- async captureSnapshot() {
249
+ async captureSnapshot(relativeTo) {
208
250
  await this._initializedPromise;
209
251
  let tabSnapshot;
210
252
  const modalStates = await this._raceAgainstModalStates(async () => {
@@ -217,6 +259,7 @@ class Tab extends import_events.EventEmitter {
217
259
  };
218
260
  });
219
261
  if (tabSnapshot) {
262
+ tabSnapshot.consoleLink = await this._consoleLog.take(relativeTo);
220
263
  tabSnapshot.events = this._recentEventEntries;
221
264
  this._recentEventEntries = [];
222
265
  }
@@ -281,6 +324,7 @@ class Tab extends import_events.EventEmitter {
281
324
  function messageToConsoleMessage(message) {
282
325
  return {
283
326
  type: message.type(),
327
+ timestamp: message.timestamp(),
284
328
  text: message.text(),
285
329
  toString: () => `[${message.type().toUpperCase()}] ${message.text()} @ ${message.location().url}:${message.location().lineNumber}`
286
330
  };
@@ -289,12 +333,14 @@ function pageErrorToConsoleMessage(errorOrValue) {
289
333
  if (errorOrValue instanceof Error) {
290
334
  return {
291
335
  type: "error",
336
+ timestamp: Date.now(),
292
337
  text: errorOrValue.message,
293
338
  toString: () => errorOrValue.stack || errorOrValue.message
294
339
  };
295
340
  }
296
341
  return {
297
342
  type: "error",
343
+ timestamp: Date.now(),
298
344
  text: String(errorOrValue),
299
345
  toString: () => String(errorOrValue)
300
346
  };
@@ -342,10 +388,19 @@ function consoleLevelForMessageType(type) {
342
388
  }
343
389
  }
344
390
  const tabSymbol = Symbol("tabSymbol");
391
+ function sanitizeForFilePath(s) {
392
+ const sanitize = (s2) => s2.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, "-");
393
+ const separator = s.lastIndexOf(".");
394
+ if (separator === -1)
395
+ return sanitize(s);
396
+ return sanitize(s.substring(0, separator)) + "." + sanitize(s.substring(separator + 1));
397
+ }
398
+ function tabHeaderEquals(a, b) {
399
+ return a.title === b.title && a.url === b.url && a.current === b.current && a.console.errors === b.console.errors && a.console.warnings === b.console.warnings && a.console.total === b.console.total;
400
+ }
345
401
  // Annotate the CommonJS export names for ESM import in node:
346
402
  0 && (module.exports = {
347
403
  Tab,
348
- TabEvents,
349
404
  renderModalStates,
350
405
  shouldIncludeMessage
351
406
  });
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var config_exports = {};
20
+ __export(config_exports, {
21
+ default: () => config_default
22
+ });
23
+ module.exports = __toCommonJS(config_exports);
24
+ var import_mcpBundle = require("../../../mcpBundle");
25
+ var import_tool = require("./tool");
26
+ const configShow = (0, import_tool.defineTool)({
27
+ capability: "config",
28
+ schema: {
29
+ name: "browser_get_config",
30
+ title: "Get config",
31
+ description: "Get the final resolved config after merging CLI options, environment variables and config file.",
32
+ inputSchema: import_mcpBundle.z.object({}),
33
+ type: "readOnly"
34
+ },
35
+ handle: async (context, params, response) => {
36
+ response.addTextResult(JSON.stringify(context.config, null, 2));
37
+ }
38
+ });
39
+ var config_default = [
40
+ configShow
41
+ ];
@@ -36,9 +36,13 @@ const console = (0, import_tool.defineTabTool)({
36
36
  type: "readOnly"
37
37
  },
38
38
  handle: async (tab, params, response) => {
39
+ const count = await tab.consoleMessageCount();
40
+ const header = [`Total messages: ${count.total} (Errors: ${count.errors}, Warnings: ${count.warnings})`];
39
41
  const messages = await tab.consoleMessages(params.level);
40
- const text = messages.map((message) => message.toString()).join("\n");
41
- response.addResult("Console", text, { prefix: "console", ext: "log", suggestedFilename: params.filename });
42
+ if (messages.length !== count.total)
43
+ header.push(`Returning ${messages.length} messages for level "${params.level}"`);
44
+ const text = [...header, "", ...messages.map((message) => message.toString())].join("\n");
45
+ await response.addResult("Console", text, { prefix: "console", ext: "log", suggestedFilename: params.filename });
42
46
  }
43
47
  });
44
48
  const consoleClear = (0, import_tool.defineTabTool)({