@agimon-ai/browse-tool 0.2.0 → 0.2.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.
@@ -1,3 +1,382 @@
1
+ async function screenshotHandler(args, _context, tabId) {
2
+ if (!tabId) {
3
+ return {
4
+ content: [{ type: "text", text: "No active tab" }],
5
+ isError: true
6
+ };
7
+ }
8
+ try {
9
+ const format = args.format || "png";
10
+ const quality = args.quality;
11
+ const tab = await globalThis.chrome.tabs.get(tabId);
12
+ if (!tab.windowId) {
13
+ return {
14
+ content: [{ type: "text", text: "Tab has no window" }],
15
+ isError: true
16
+ };
17
+ }
18
+ await globalThis.chrome.tabs.update(tabId, { active: true });
19
+ const dataUrl = await globalThis.chrome.tabs.captureVisibleTab(tab.windowId, {
20
+ format,
21
+ quality: format === "jpeg" ? quality : void 0
22
+ });
23
+ const base64 = dataUrl.split(",")[1];
24
+ const mimeType = format === "png" ? "image/png" : "image/jpeg";
25
+ return {
26
+ content: [
27
+ {
28
+ type: "image",
29
+ data: base64,
30
+ mimeType
31
+ }
32
+ ]
33
+ };
34
+ } catch (error) {
35
+ const message = error instanceof Error ? error.message : "Screenshot failed";
36
+ return {
37
+ content: [{ type: "text", text: message }],
38
+ isError: true
39
+ };
40
+ }
41
+ }
42
+ async function snapshotHandler(args, _context, tabId) {
43
+ if (!tabId) {
44
+ return {
45
+ content: [{ type: "text", text: "No active tab" }],
46
+ isError: true
47
+ };
48
+ }
49
+ try {
50
+ const root = typeof args.root === "string" && args.root.trim() ? args.root.trim() : "body";
51
+ const [result] = await globalThis.chrome.scripting.executeScript({
52
+ target: { tabId },
53
+ world: "ISOLATED",
54
+ args: [root],
55
+ func: (rootSelector) => {
56
+ let uidCounter = 0;
57
+ const uidMap = /* @__PURE__ */ new WeakMap();
58
+ const assignUid = (element) => {
59
+ const existing = uidMap.get(element);
60
+ if (existing) {
61
+ return existing;
62
+ }
63
+ const uid = `ax-${++uidCounter}`;
64
+ uidMap.set(element, uid);
65
+ return uid;
66
+ };
67
+ const isVisible = (element) => {
68
+ const style = window.getComputedStyle(element);
69
+ return style.display !== "none" && style.visibility !== "hidden";
70
+ };
71
+ const getInputRole = (input) => {
72
+ const type = (input.type || "text").toLowerCase();
73
+ const inputRoles = {
74
+ button: "button",
75
+ checkbox: "checkbox",
76
+ email: "textbox",
77
+ number: "spinbutton",
78
+ password: "textbox",
79
+ radio: "radio",
80
+ range: "slider",
81
+ search: "searchbox",
82
+ submit: "button",
83
+ tel: "textbox",
84
+ text: "textbox",
85
+ url: "textbox"
86
+ };
87
+ return inputRoles[type] || "textbox";
88
+ };
89
+ const getRole = (element) => {
90
+ const ariaRole = element.getAttribute("role");
91
+ if (ariaRole) {
92
+ return ariaRole;
93
+ }
94
+ const tagName = element.tagName.toLowerCase();
95
+ const roleMap = {
96
+ a: element.hasAttribute("href") ? "link" : "none",
97
+ article: "article",
98
+ aside: "complementary",
99
+ button: "button",
100
+ footer: "contentinfo",
101
+ form: "form",
102
+ h1: "heading",
103
+ h2: "heading",
104
+ h3: "heading",
105
+ h4: "heading",
106
+ h5: "heading",
107
+ h6: "heading",
108
+ header: "banner",
109
+ img: "img",
110
+ input: getInputRole(element),
111
+ li: "listitem",
112
+ main: "main",
113
+ nav: "navigation",
114
+ ol: "list",
115
+ option: "option",
116
+ progress: "progressbar",
117
+ section: element.hasAttribute("aria-label") ? "region" : "none",
118
+ select: "combobox",
119
+ table: "table",
120
+ tbody: "rowgroup",
121
+ td: "cell",
122
+ textarea: "textbox",
123
+ th: "columnheader",
124
+ tr: "row",
125
+ ul: "list"
126
+ };
127
+ return roleMap[tagName] || "none";
128
+ };
129
+ const getAccessibleName = (element) => {
130
+ const ariaLabel = element.getAttribute("aria-label");
131
+ if (ariaLabel) {
132
+ return ariaLabel;
133
+ }
134
+ const labelledBy = element.getAttribute("aria-labelledby");
135
+ if (labelledBy) {
136
+ const labels = labelledBy.split(" ").map((id) => document.getElementById(id)?.textContent).filter(Boolean).join(" ");
137
+ if (labels) {
138
+ return labels;
139
+ }
140
+ }
141
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
142
+ if (element.id) {
143
+ const label = document.querySelector(`label[for="${element.id}"]`);
144
+ if (label?.textContent) {
145
+ return label.textContent.trim();
146
+ }
147
+ }
148
+ if (element.placeholder) {
149
+ return element.placeholder;
150
+ }
151
+ }
152
+ const tagName = element.tagName.toLowerCase();
153
+ if (["button", "a", "h1", "h2", "h3", "h4", "h5", "h6", "label", "option"].includes(tagName)) {
154
+ const text = element.textContent?.trim();
155
+ if (text && text.length < 100) {
156
+ return text;
157
+ }
158
+ }
159
+ if (element instanceof HTMLImageElement && element.alt) {
160
+ return element.alt;
161
+ }
162
+ return void 0;
163
+ };
164
+ const addState = (element, node) => {
165
+ if (document.activeElement === element) {
166
+ node.focused = true;
167
+ }
168
+ if (element.disabled) {
169
+ node.disabled = true;
170
+ }
171
+ if (element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio")) {
172
+ node.checked = element.indeterminate ? "mixed" : element.checked;
173
+ }
174
+ if (element instanceof HTMLOptionElement) {
175
+ node.selected = element.selected;
176
+ }
177
+ const expanded = element.getAttribute("aria-expanded");
178
+ if (expanded) {
179
+ node.expanded = expanded === "true";
180
+ }
181
+ const headingMatch = element.tagName.match(/^H([1-6])$/);
182
+ if (headingMatch) {
183
+ node.level = Number.parseInt(headingMatch[1], 10);
184
+ }
185
+ const pressed = element.getAttribute("aria-pressed");
186
+ if (pressed) {
187
+ node.pressed = pressed === "mixed" ? "mixed" : pressed === "true";
188
+ }
189
+ };
190
+ const buildNode = (element) => {
191
+ if (!isVisible(element)) {
192
+ return null;
193
+ }
194
+ const children = [];
195
+ for (const child of element.children) {
196
+ const childNode = buildNode(child);
197
+ if (childNode) {
198
+ children.push(childNode);
199
+ }
200
+ }
201
+ const role = getRole(element);
202
+ const name = getAccessibleName(element);
203
+ if (role === "none" && !name && children.length === 1) {
204
+ return children[0];
205
+ }
206
+ const node = {
207
+ uid: assignUid(element),
208
+ role
209
+ };
210
+ if (name) {
211
+ node.name = name;
212
+ }
213
+ if (children.length > 0) {
214
+ node.children = children;
215
+ }
216
+ addState(element, node);
217
+ return node;
218
+ };
219
+ const rootElement = document.querySelector(rootSelector) ?? document.body;
220
+ if (!(rootElement instanceof Element)) {
221
+ throw new Error(`Root element not found: ${rootSelector}`);
222
+ }
223
+ return buildNode(rootElement) ?? { uid: "root", role: "none", name: "Empty page" };
224
+ }
225
+ });
226
+ if (!result) {
227
+ return {
228
+ content: [{ type: "text", text: `Snapshot failed: executeScript returned no result (tabId=${tabId})` }],
229
+ isError: true
230
+ };
231
+ }
232
+ const snapshot = JSON.stringify(result.result ?? { uid: "root", role: "none", name: "Empty page" }, null, 2);
233
+ return {
234
+ content: [
235
+ {
236
+ type: "text",
237
+ text: snapshot || "No accessibility tree available"
238
+ }
239
+ ]
240
+ };
241
+ } catch (error) {
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ return {
244
+ content: [{ type: "text", text: `Snapshot error (tabId=${tabId}): ${message}` }],
245
+ isError: true
246
+ };
247
+ }
248
+ }
249
+
250
+ async function resizePageHandler(args, _context, tabId) {
251
+ if (!tabId) {
252
+ return {
253
+ content: [{ type: "text", text: "No active tab" }],
254
+ isError: true
255
+ };
256
+ }
257
+ const width = args.width;
258
+ const height = args.height;
259
+ if (!width || !height) {
260
+ return {
261
+ content: [{ type: "text", text: "width and height are required" }],
262
+ isError: true
263
+ };
264
+ }
265
+ try {
266
+ const tab = await globalThis.chrome.tabs.get(tabId);
267
+ if (!tab.windowId) {
268
+ return {
269
+ content: [{ type: "text", text: "Cannot find window for tab" }],
270
+ isError: true
271
+ };
272
+ }
273
+ await globalThis.chrome.windows.update(tab.windowId, {
274
+ width,
275
+ height
276
+ });
277
+ return {
278
+ content: [{ type: "text", text: `Page resized to ${width}x${height}` }]
279
+ };
280
+ } catch (error) {
281
+ const message = error instanceof Error ? error.message : "Resize failed";
282
+ return {
283
+ content: [{ type: "text", text: message }],
284
+ isError: true
285
+ };
286
+ }
287
+ }
288
+ async function handleDialogHandler(args, _context, tabId) {
289
+ if (!tabId) {
290
+ return {
291
+ content: [{ type: "text", text: "No active tab" }],
292
+ isError: true
293
+ };
294
+ }
295
+ const accept = args.accept !== false;
296
+ const promptText = args.promptText;
297
+ try {
298
+ await globalThis.chrome.debugger.attach({ tabId }, "1.3");
299
+ await globalThis.chrome.debugger.sendCommand({ tabId }, "Page.handleJavaScriptDialog", {
300
+ accept,
301
+ promptText
302
+ });
303
+ await globalThis.chrome.debugger.detach({ tabId });
304
+ return {
305
+ content: [{ type: "text", text: `Dialog ${accept ? "accepted" : "dismissed"}` }]
306
+ };
307
+ } catch (error) {
308
+ try {
309
+ await globalThis.chrome.debugger.detach({ tabId });
310
+ } catch {
311
+ }
312
+ const message = error instanceof Error ? error.message : "Handle dialog failed";
313
+ return {
314
+ content: [{ type: "text", text: message }],
315
+ isError: true
316
+ };
317
+ }
318
+ }
319
+ async function emulateHandler(args, _context, tabId) {
320
+ if (!tabId) {
321
+ return {
322
+ content: [{ type: "text", text: "No active tab" }],
323
+ isError: true
324
+ };
325
+ }
326
+ try {
327
+ await globalThis.chrome.debugger.attach({ tabId }, "1.3");
328
+ const commands = [];
329
+ if (args.geolocation) {
330
+ const geo = args.geolocation;
331
+ commands.push({
332
+ method: "Emulation.setGeolocationOverride",
333
+ params: {
334
+ latitude: geo.latitude,
335
+ longitude: geo.longitude,
336
+ accuracy: geo.accuracy || 100
337
+ }
338
+ });
339
+ }
340
+ if (args.timezone) {
341
+ commands.push({
342
+ method: "Emulation.setTimezoneOverride",
343
+ params: { timezoneId: args.timezone }
344
+ });
345
+ }
346
+ if (args.locale) {
347
+ commands.push({
348
+ method: "Emulation.setLocaleOverride",
349
+ params: { locale: args.locale }
350
+ });
351
+ }
352
+ if (args.colorScheme) {
353
+ commands.push({
354
+ method: "Emulation.setEmulatedMedia",
355
+ params: {
356
+ features: [{ name: "prefers-color-scheme", value: args.colorScheme }]
357
+ }
358
+ });
359
+ }
360
+ for (const cmd of commands) {
361
+ await globalThis.chrome.debugger.sendCommand({ tabId }, cmd.method, cmd.params);
362
+ }
363
+ await globalThis.chrome.debugger.detach({ tabId });
364
+ return {
365
+ content: [{ type: "text", text: "Emulation settings applied" }]
366
+ };
367
+ } catch (error) {
368
+ try {
369
+ await globalThis.chrome.debugger.detach({ tabId });
370
+ } catch {
371
+ }
372
+ const message = error instanceof Error ? error.message : "Emulation failed";
373
+ return {
374
+ content: [{ type: "text", text: message }],
375
+ isError: true
376
+ };
377
+ }
378
+ }
379
+
1
380
  /** A special constant with type `never` */
2
381
  function $constructor(name, initializer, params) {
3
382
  function init(inst, def) {
@@ -4786,17 +5165,60 @@ object({
4786
5165
  function createContentMessage(action, params = {}) {
4787
5166
  return { action, params };
4788
5167
  }
5168
+ function attachTelemetryToParams(source, params) {
5169
+ const telemetry = source.__browseToolTelemetry;
5170
+ if (!telemetry || typeof telemetry !== "object") {
5171
+ return params;
5172
+ }
5173
+ return {
5174
+ ...params,
5175
+ __browseToolTelemetry: telemetry
5176
+ };
5177
+ }
4789
5178
 
5179
+ const POST_INJECTION_RETRY_COUNT = 5;
5180
+ const POST_INJECTION_RETRY_DELAY_MS = 100;
5181
+ function isContentResponse(value) {
5182
+ if (!value || typeof value !== "object") {
5183
+ return false;
5184
+ }
5185
+ const candidate = value;
5186
+ return typeof candidate.success === "boolean";
5187
+ }
5188
+ async function delay(ms) {
5189
+ await new Promise((resolve) => setTimeout(resolve, ms));
5190
+ }
5191
+ async function sendValidatedMessage(tabId, message) {
5192
+ const response = await globalThis.chrome.tabs.sendMessage(tabId, message);
5193
+ if (!isContentResponse(response)) {
5194
+ throw new Error(`Content script returned an invalid response (tabId=${tabId})`);
5195
+ }
5196
+ return response;
5197
+ }
4790
5198
  async function sendMessageToContentScript(tabId, message) {
4791
5199
  try {
4792
- const response = await globalThis.chrome.tabs.sendMessage(tabId, message);
4793
- return response;
5200
+ return await sendValidatedMessage(tabId, message);
4794
5201
  } catch (error) {
4795
5202
  const errorMessage = error instanceof Error ? error.message : String(error);
4796
- if (errorMessage.includes("Could not establish connection") || errorMessage.includes("Receiving end does not exist")) {
5203
+ if (errorMessage.includes("Could not establish connection") || errorMessage.includes("Receiving end does not exist") || errorMessage.includes("invalid response")) {
4797
5204
  await injectContentScript(tabId);
4798
- const response = await globalThis.chrome.tabs.sendMessage(tabId, message);
4799
- return response;
5205
+ let lastError;
5206
+ for (let attempt = 1; attempt <= POST_INJECTION_RETRY_COUNT; attempt += 1) {
5207
+ try {
5208
+ return await sendValidatedMessage(tabId, message);
5209
+ } catch (retryError) {
5210
+ lastError = retryError instanceof Error ? retryError : new Error(String(retryError));
5211
+ if (attempt < POST_INJECTION_RETRY_COUNT) {
5212
+ await delay(POST_INJECTION_RETRY_DELAY_MS);
5213
+ }
5214
+ }
5215
+ }
5216
+ throw new Error(
5217
+ `Content script returned an invalid response after injection (tabId=${tabId}): ${lastError?.message ?? "unknown error"}`,
5218
+ {
5219
+ cause: error
5220
+ }
5221
+ );
4800
5222
  }
4801
5223
  throw error;
4802
5224
  }
@@ -4810,218 +5232,15 @@ async function injectContentScript(tabId) {
4810
5232
  } catch (error) {
4811
5233
  const errorMessage = error instanceof Error ? error.message : String(error);
4812
5234
  if (errorMessage.includes("Cannot access")) {
4813
- throw new Error(`Cannot inject content script into restricted page (tabId=${tabId}): ${errorMessage}`);
5235
+ throw new Error(`Cannot inject content script into restricted page (tabId=${tabId}): ${errorMessage}`, {
5236
+ cause: error
5237
+ });
4814
5238
  }
4815
- throw new Error(`Content script injection failed (tabId=${tabId}): ${errorMessage}`);
5239
+ throw new Error(`Content script injection failed (tabId=${tabId}): ${errorMessage}`, { cause: error });
4816
5240
  }
4817
5241
  }
4818
5242
 
4819
- async function screenshotHandler(args, _context, tabId) {
4820
- if (!tabId) {
4821
- return {
4822
- content: [{ type: "text", text: "No active tab" }],
4823
- isError: true
4824
- };
4825
- }
4826
- try {
4827
- const format = args.format || "png";
4828
- const quality = args.quality;
4829
- const tab = await globalThis.chrome.tabs.get(tabId);
4830
- if (!tab.windowId) {
4831
- return {
4832
- content: [{ type: "text", text: "Tab has no window" }],
4833
- isError: true
4834
- };
4835
- }
4836
- await globalThis.chrome.tabs.update(tabId, { active: true });
4837
- const dataUrl = await globalThis.chrome.tabs.captureVisibleTab(tab.windowId, {
4838
- format,
4839
- quality: format === "jpeg" ? quality : void 0
4840
- });
4841
- const base64 = dataUrl.split(",")[1];
4842
- const mimeType = format === "png" ? "image/png" : "image/jpeg";
4843
- return {
4844
- content: [
4845
- {
4846
- type: "image",
4847
- data: base64,
4848
- mimeType
4849
- }
4850
- ]
4851
- };
4852
- } catch (error) {
4853
- const message = error instanceof Error ? error.message : "Screenshot failed";
4854
- return {
4855
- content: [{ type: "text", text: message }],
4856
- isError: true
4857
- };
4858
- }
4859
- }
4860
- async function snapshotHandler(_args, _context, tabId) {
4861
- if (!tabId) {
4862
- return {
4863
- content: [{ type: "text", text: "No active tab" }],
4864
- isError: true
4865
- };
4866
- }
4867
- try {
4868
- const message = createContentMessage(ContentAction.GET_SNAPSHOT, {});
4869
- const response = await sendMessageToContentScript(tabId, message);
4870
- if (!response.success) {
4871
- return {
4872
- content: [{ type: "text", text: `Snapshot failed: ${response.error || "unknown error from content script"}` }],
4873
- isError: true
4874
- };
4875
- }
4876
- const snapshot = typeof response.result === "string" ? response.result : JSON.stringify(response.result, null, 2);
4877
- return {
4878
- content: [
4879
- {
4880
- type: "text",
4881
- text: snapshot || "No accessibility tree available"
4882
- }
4883
- ]
4884
- };
4885
- } catch (error) {
4886
- const message = error instanceof Error ? error.message : String(error);
4887
- return {
4888
- content: [{ type: "text", text: `Snapshot error (tabId=${tabId}): ${message}` }],
4889
- isError: true
4890
- };
4891
- }
4892
- }
4893
-
4894
- async function resizePageHandler(args, _context, tabId) {
4895
- if (!tabId) {
4896
- return {
4897
- content: [{ type: "text", text: "No active tab" }],
4898
- isError: true
4899
- };
4900
- }
4901
- const width = args.width;
4902
- const height = args.height;
4903
- if (!width || !height) {
4904
- return {
4905
- content: [{ type: "text", text: "width and height are required" }],
4906
- isError: true
4907
- };
4908
- }
4909
- try {
4910
- const tab = await globalThis.chrome.tabs.get(tabId);
4911
- if (!tab.windowId) {
4912
- return {
4913
- content: [{ type: "text", text: "Cannot find window for tab" }],
4914
- isError: true
4915
- };
4916
- }
4917
- await globalThis.chrome.windows.update(tab.windowId, {
4918
- width,
4919
- height
4920
- });
4921
- return {
4922
- content: [{ type: "text", text: `Page resized to ${width}x${height}` }]
4923
- };
4924
- } catch (error) {
4925
- const message = error instanceof Error ? error.message : "Resize failed";
4926
- return {
4927
- content: [{ type: "text", text: message }],
4928
- isError: true
4929
- };
4930
- }
4931
- }
4932
- async function handleDialogHandler(args, _context, tabId) {
4933
- if (!tabId) {
4934
- return {
4935
- content: [{ type: "text", text: "No active tab" }],
4936
- isError: true
4937
- };
4938
- }
4939
- const accept = args.accept !== false;
4940
- const promptText = args.promptText;
4941
- try {
4942
- await globalThis.chrome.debugger.attach({ tabId }, "1.3");
4943
- await globalThis.chrome.debugger.sendCommand({ tabId }, "Page.handleJavaScriptDialog", {
4944
- accept,
4945
- promptText
4946
- });
4947
- await globalThis.chrome.debugger.detach({ tabId });
4948
- return {
4949
- content: [{ type: "text", text: `Dialog ${accept ? "accepted" : "dismissed"}` }]
4950
- };
4951
- } catch (error) {
4952
- try {
4953
- await globalThis.chrome.debugger.detach({ tabId });
4954
- } catch {
4955
- }
4956
- const message = error instanceof Error ? error.message : "Handle dialog failed";
4957
- return {
4958
- content: [{ type: "text", text: message }],
4959
- isError: true
4960
- };
4961
- }
4962
- }
4963
- async function emulateHandler(args, _context, tabId) {
4964
- if (!tabId) {
4965
- return {
4966
- content: [{ type: "text", text: "No active tab" }],
4967
- isError: true
4968
- };
4969
- }
4970
- try {
4971
- await globalThis.chrome.debugger.attach({ tabId }, "1.3");
4972
- const commands = [];
4973
- if (args.geolocation) {
4974
- const geo = args.geolocation;
4975
- commands.push({
4976
- method: "Emulation.setGeolocationOverride",
4977
- params: {
4978
- latitude: geo.latitude,
4979
- longitude: geo.longitude,
4980
- accuracy: geo.accuracy || 100
4981
- }
4982
- });
4983
- }
4984
- if (args.timezone) {
4985
- commands.push({
4986
- method: "Emulation.setTimezoneOverride",
4987
- params: { timezoneId: args.timezone }
4988
- });
4989
- }
4990
- if (args.locale) {
4991
- commands.push({
4992
- method: "Emulation.setLocaleOverride",
4993
- params: { locale: args.locale }
4994
- });
4995
- }
4996
- if (args.colorScheme) {
4997
- commands.push({
4998
- method: "Emulation.setEmulatedMedia",
4999
- params: {
5000
- features: [{ name: "prefers-color-scheme", value: args.colorScheme }]
5001
- }
5002
- });
5003
- }
5004
- for (const cmd of commands) {
5005
- await globalThis.chrome.debugger.sendCommand({ tabId }, cmd.method, cmd.params);
5006
- }
5007
- await globalThis.chrome.debugger.detach({ tabId });
5008
- return {
5009
- content: [{ type: "text", text: "Emulation settings applied" }]
5010
- };
5011
- } catch (error) {
5012
- try {
5013
- await globalThis.chrome.debugger.detach({ tabId });
5014
- } catch {
5015
- }
5016
- const message = error instanceof Error ? error.message : "Emulation failed";
5017
- return {
5018
- content: [{ type: "text", text: message }],
5019
- isError: true
5020
- };
5021
- }
5022
- }
5023
-
5024
- async function selectHandler(args, _context, tabId) {
5243
+ async function selectHandler(args, _context, tabId) {
5025
5244
  if (!tabId) {
5026
5245
  return {
5027
5246
  content: [{ type: "text", text: "No active tab" }],
@@ -5030,10 +5249,12 @@ async function selectHandler(args, _context, tabId) {
5030
5249
  }
5031
5250
  try {
5032
5251
  const message = createContentMessage(ContentAction.SELECT, {
5033
- selector: args.selector,
5034
- xpath: args.xpath,
5035
- uid: args.uid,
5036
- value: args.values || args.value
5252
+ ...attachTelemetryToParams(args, {
5253
+ selector: args.selector,
5254
+ xpath: args.xpath,
5255
+ uid: args.uid,
5256
+ value: args.values || args.value
5257
+ })
5037
5258
  });
5038
5259
  const response = await sendMessageToContentScript(tabId, message);
5039
5260
  if (!response.success) {
@@ -5062,10 +5283,12 @@ async function hoverHandler(args, _context, tabId) {
5062
5283
  }
5063
5284
  try {
5064
5285
  const message = createContentMessage(ContentAction.HOVER, {
5065
- selector: args.selector,
5066
- xpath: args.xpath,
5067
- text: args.text,
5068
- uid: args.uid
5286
+ ...attachTelemetryToParams(args, {
5287
+ selector: args.selector,
5288
+ xpath: args.xpath,
5289
+ text: args.text,
5290
+ uid: args.uid
5291
+ })
5069
5292
  });
5070
5293
  const response = await sendMessageToContentScript(tabId, message);
5071
5294
  if (!response.success) {
@@ -5094,16 +5317,18 @@ async function dragHandler(args, _context, tabId) {
5094
5317
  }
5095
5318
  try {
5096
5319
  const message = createContentMessage(ContentAction.DRAG, {
5097
- source: {
5098
- selector: args.selector,
5099
- xpath: args.xpath,
5100
- uid: args.uid
5101
- },
5102
- target: {
5103
- selector: args.targetSelector,
5104
- xpath: args.targetXpath,
5105
- uid: args.targetUid
5106
- }
5320
+ ...attachTelemetryToParams(args, {
5321
+ source: {
5322
+ selector: args.selector,
5323
+ xpath: args.xpath,
5324
+ uid: args.uid
5325
+ },
5326
+ target: {
5327
+ selector: args.targetSelector,
5328
+ xpath: args.targetXpath,
5329
+ uid: args.targetUid
5330
+ }
5331
+ })
5107
5332
  });
5108
5333
  const response = await sendMessageToContentScript(tabId, message);
5109
5334
  if (!response.success) {
@@ -5139,8 +5364,10 @@ async function pressKeyHandler(args, _context, tabId) {
5139
5364
  }
5140
5365
  try {
5141
5366
  const message = createContentMessage(ContentAction.PRESS_KEY, {
5142
- key,
5143
- modifiers: args.modifiers
5367
+ ...attachTelemetryToParams(args, {
5368
+ key,
5369
+ modifiers: args.modifiers
5370
+ })
5144
5371
  });
5145
5372
  const response = await sendMessageToContentScript(tabId, message);
5146
5373
  if (!response.success) {
@@ -5170,13 +5397,15 @@ async function clickHandler(args, _context, tabId) {
5170
5397
  }
5171
5398
  try {
5172
5399
  const message = createContentMessage(ContentAction.CLICK, {
5173
- selector: args.selector,
5174
- xpath: args.xpath,
5175
- text: args.text,
5176
- uid: args.uid,
5177
- button: args.button,
5178
- clickCount: args.clickCount,
5179
- modifiers: args.modifiers
5400
+ ...attachTelemetryToParams(args, {
5401
+ selector: args.selector,
5402
+ xpath: args.xpath,
5403
+ text: args.text,
5404
+ uid: args.uid,
5405
+ button: args.button,
5406
+ clickCount: args.clickCount,
5407
+ modifiers: args.modifiers
5408
+ })
5180
5409
  });
5181
5410
  const response = await sendMessageToContentScript(tabId, message);
5182
5411
  if (!response.success) {
@@ -5212,12 +5441,14 @@ async function fillHandler(args, _context, tabId) {
5212
5441
  }
5213
5442
  try {
5214
5443
  const message = createContentMessage(ContentAction.FILL, {
5215
- selector: args.selector,
5216
- xpath: args.xpath,
5217
- text: args.text,
5218
- uid: args.uid,
5219
- value,
5220
- force: args.force
5444
+ ...attachTelemetryToParams(args, {
5445
+ selector: args.selector,
5446
+ xpath: args.xpath,
5447
+ text: args.text,
5448
+ uid: args.uid,
5449
+ value,
5450
+ force: args.force
5451
+ })
5221
5452
  });
5222
5453
  const response = await sendMessageToContentScript(tabId, message);
5223
5454
  if (!response.success) {
@@ -5253,11 +5484,13 @@ async function typeHandler(args, _context, tabId) {
5253
5484
  }
5254
5485
  try {
5255
5486
  const message = createContentMessage(ContentAction.TYPE, {
5256
- selector: args.selector,
5257
- xpath: args.xpath,
5258
- uid: args.uid,
5259
- text,
5260
- delay: args.delay
5487
+ ...attachTelemetryToParams(args, {
5488
+ selector: args.selector,
5489
+ xpath: args.xpath,
5490
+ uid: args.uid,
5491
+ text,
5492
+ delay: args.delay
5493
+ })
5261
5494
  });
5262
5495
  const response = await sendMessageToContentScript(tabId, message);
5263
5496
  if (!response.success) {
@@ -5277,6 +5510,159 @@ async function typeHandler(args, _context, tabId) {
5277
5510
  };
5278
5511
  }
5279
5512
  }
5513
+ async function uploadFileHandler(args, _context, tabId) {
5514
+ if (!tabId) {
5515
+ return {
5516
+ content: [{ type: "text", text: "No active tab" }],
5517
+ isError: true
5518
+ };
5519
+ }
5520
+ const filesInput = args.files;
5521
+ const files = Array.isArray(filesInput) ? filesInput : typeof filesInput === "string" ? [filesInput] : [];
5522
+ if (files.length === 0 || files.some((file) => typeof file !== "string" || !file)) {
5523
+ return {
5524
+ content: [{ type: "text", text: "files is required and must be a string or string[]" }],
5525
+ isError: true
5526
+ };
5527
+ }
5528
+ const markerAttr = "data-browse-tool-upload-id";
5529
+ const markerValue = `upload-${crypto.randomUUID()}`;
5530
+ try {
5531
+ const [markerResult] = await globalThis.chrome.scripting.executeScript({
5532
+ target: { tabId },
5533
+ world: "ISOLATED",
5534
+ args: [
5535
+ {
5536
+ selector: args.selector,
5537
+ xpath: args.xpath,
5538
+ text: args.text,
5539
+ uid: args.uid
5540
+ },
5541
+ markerAttr,
5542
+ markerValue
5543
+ ],
5544
+ func: (locator, attrName, attrValue) => {
5545
+ const findByText = (text) => {
5546
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, {
5547
+ acceptNode(node) {
5548
+ const element = node;
5549
+ return element.textContent?.includes(text) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
5550
+ }
5551
+ });
5552
+ const match = walker.nextNode();
5553
+ return match instanceof Element ? match : null;
5554
+ };
5555
+ const locateElement = () => {
5556
+ if (typeof locator.selector === "string" && locator.selector) {
5557
+ const element = document.querySelector(locator.selector);
5558
+ if (element) {
5559
+ return element;
5560
+ }
5561
+ }
5562
+ if (typeof locator.xpath === "string" && locator.xpath) {
5563
+ const result = document.evaluate(locator.xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
5564
+ if (result.singleNodeValue instanceof Element) {
5565
+ return result.singleNodeValue;
5566
+ }
5567
+ }
5568
+ if (typeof locator.text === "string" && locator.text) {
5569
+ const element = findByText(locator.text);
5570
+ if (element) {
5571
+ return element;
5572
+ }
5573
+ }
5574
+ if (typeof locator.uid === "string" && locator.uid) {
5575
+ const element = document.querySelector(`[data-browse-tool-uid="${locator.uid}"]`);
5576
+ if (element) {
5577
+ return element;
5578
+ }
5579
+ }
5580
+ return document.querySelector('input[type="file"]');
5581
+ };
5582
+ const resolveFileInput = (element) => {
5583
+ if (!element) {
5584
+ return null;
5585
+ }
5586
+ if (element instanceof HTMLInputElement && element.type === "file") {
5587
+ return element;
5588
+ }
5589
+ if (element instanceof HTMLLabelElement && element.htmlFor) {
5590
+ const labelled = document.getElementById(element.htmlFor);
5591
+ if (labelled instanceof HTMLInputElement && labelled.type === "file") {
5592
+ return labelled;
5593
+ }
5594
+ }
5595
+ const nested = element.querySelector('input[type="file"]');
5596
+ return nested instanceof HTMLInputElement ? nested : null;
5597
+ };
5598
+ const input = resolveFileInput(locateElement());
5599
+ if (!input) {
5600
+ throw new Error("File input element not found");
5601
+ }
5602
+ input.setAttribute(attrName, attrValue);
5603
+ return {
5604
+ ok: true,
5605
+ multiple: input.multiple
5606
+ };
5607
+ }
5608
+ });
5609
+ if (!markerResult?.result || typeof markerResult.result !== "object") {
5610
+ return {
5611
+ content: [{ type: "text", text: "Failed to resolve file input element" }],
5612
+ isError: true
5613
+ };
5614
+ }
5615
+ await globalThis.chrome.debugger.attach({ tabId }, "1.3");
5616
+ try {
5617
+ const documentRoot = await globalThis.chrome.debugger.sendCommand({ tabId }, "DOM.getDocument", {
5618
+ depth: -1
5619
+ });
5620
+ const rootNodeId = documentRoot.root?.nodeId;
5621
+ if (!rootNodeId) {
5622
+ throw new Error("Failed to resolve document root");
5623
+ }
5624
+ const queryResult = await globalThis.chrome.debugger.sendCommand({ tabId }, "DOM.querySelector", {
5625
+ nodeId: rootNodeId,
5626
+ selector: `input[${markerAttr}="${markerValue}"]`
5627
+ });
5628
+ if (!queryResult.nodeId) {
5629
+ throw new Error("Failed to resolve file input node");
5630
+ }
5631
+ await globalThis.chrome.debugger.sendCommand({ tabId }, "DOM.setFileInputFiles", {
5632
+ nodeId: queryResult.nodeId,
5633
+ files
5634
+ });
5635
+ } finally {
5636
+ try {
5637
+ await globalThis.chrome.debugger.detach({ tabId });
5638
+ } catch {
5639
+ }
5640
+ try {
5641
+ await globalThis.chrome.scripting.executeScript({
5642
+ target: { tabId },
5643
+ world: "ISOLATED",
5644
+ args: [markerAttr, markerValue],
5645
+ func: (attrName, attrValue) => {
5646
+ const element = document.querySelector(`input[${attrName}="${attrValue}"]`);
5647
+ if (element instanceof HTMLInputElement) {
5648
+ element.removeAttribute(attrName);
5649
+ }
5650
+ }
5651
+ });
5652
+ } catch {
5653
+ }
5654
+ }
5655
+ return {
5656
+ content: [{ type: "text", text: `Uploaded ${files.length} file(s) successfully` }]
5657
+ };
5658
+ } catch (error) {
5659
+ const errorMessage = error instanceof Error ? error.message : "Upload failed";
5660
+ return {
5661
+ content: [{ type: "text", text: errorMessage }],
5662
+ isError: true
5663
+ };
5664
+ }
5665
+ }
5280
5666
 
5281
5667
  async function navigateHandler(args, _context, tabId) {
5282
5668
  const url = args.url;
@@ -5325,7 +5711,9 @@ async function navigateHandler(args, _context, tabId) {
5325
5711
  }
5326
5712
  }
5327
5713
 
5328
- async function waitForNavigation(tabId, timeout = 3e4) {
5714
+ const DEFAULT_TOOL_TIMEOUT_MS = 18e4;
5715
+
5716
+ async function waitForNavigation(tabId, timeout = DEFAULT_TOOL_TIMEOUT_MS) {
5329
5717
  return new Promise((resolve) => {
5330
5718
  const listener = (updatedTabId, changeInfo) => {
5331
5719
  if (updatedTabId === tabId && changeInfo.status === "complete") {
@@ -5450,7 +5838,7 @@ async function pdfHandler(args, _context, tabId) {
5450
5838
  }
5451
5839
 
5452
5840
  let activeRecordingTabId;
5453
- async function startRecordingHandler(_args, _context, tabId) {
5841
+ async function startRecordingHandler(args, context, tabId) {
5454
5842
  if (!tabId) {
5455
5843
  return {
5456
5844
  content: [{ type: "text", text: "No active tab" }],
@@ -5493,7 +5881,7 @@ async function startRecordingHandler(_args, _context, tabId) {
5493
5881
  };
5494
5882
  }
5495
5883
  }
5496
- async function stopRecordingHandler(_args, _context, _tabId) {
5884
+ async function stopRecordingHandler(args, context, tabId) {
5497
5885
  if (activeRecordingTabId === void 0) {
5498
5886
  return {
5499
5887
  content: [{ type: "text", text: "No recording in progress" }],
@@ -5544,47 +5932,38 @@ async function evaluateScriptHandler(args, _context, tabId) {
5544
5932
  };
5545
5933
  }
5546
5934
  try {
5547
- const results = await globalThis.chrome.scripting.executeScript({
5548
- target: { tabId },
5549
- world: "MAIN",
5550
- func: (code) => {
5551
- const resultId = `__playwright_mcp_result_${Date.now()}`;
5552
- const script2 = document.createElement("script");
5553
- script2.textContent = `
5554
- try {
5555
- const __result = (function() { return ${code}; })();
5556
- document.body.setAttribute('${resultId}', JSON.stringify({ success: true, result: __result }));
5557
- } catch (e) {
5558
- document.body.setAttribute('${resultId}', JSON.stringify({ success: false, error: e.message }));
5559
- }
5560
- `;
5561
- document.body.appendChild(script2);
5562
- script2.remove();
5563
- const resultStr = document.body.getAttribute(resultId);
5564
- document.body.removeAttribute(resultId);
5565
- if (resultStr) {
5566
- return JSON.parse(resultStr);
5567
- }
5568
- return { success: false, error: "No result returned" };
5569
- },
5570
- args: [script]
5935
+ const arg = args.arg;
5936
+ const functionLikePattern = /^\s*(?:async\s+)?(?:function\b|\(?\s*[\w$,\s]*\)?\s*=>)/;
5937
+ const expression = arg !== void 0 && functionLikePattern.test(script) ? `(${script})(${JSON.stringify(arg)})` : script;
5938
+ await globalThis.chrome.debugger.attach({ tabId }, "1.3");
5939
+ const response = await globalThis.chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
5940
+ expression,
5941
+ returnByValue: true,
5942
+ awaitPromise: true,
5943
+ allowUnsafeEvalBlockedByCSP: true
5571
5944
  });
5572
- const result = results[0]?.result;
5573
- if (!result?.success) {
5945
+ await globalThis.chrome.debugger.detach({ tabId });
5946
+ if (response.exceptionDetails) {
5947
+ const errorMessage = response.exceptionDetails.exception?.description || response.exceptionDetails.exception?.value || response.exceptionDetails.text || "Script execution failed";
5574
5948
  return {
5575
- content: [{ type: "text", text: result?.error || "Script execution failed" }],
5949
+ content: [{ type: "text", text: errorMessage }],
5576
5950
  isError: true
5577
5951
  };
5578
5952
  }
5953
+ const value = response.result?.value ?? response.result?.unserializableValue ?? null;
5579
5954
  return {
5580
5955
  content: [
5581
5956
  {
5582
5957
  type: "text",
5583
- text: JSON.stringify(result.result, null, 2)
5958
+ text: JSON.stringify(value, null, 2)
5584
5959
  }
5585
5960
  ]
5586
5961
  };
5587
5962
  } catch (error) {
5963
+ try {
5964
+ await globalThis.chrome.debugger.detach({ tabId });
5965
+ } catch {
5966
+ }
5588
5967
  const message = error instanceof Error ? error.message : "Script execution failed";
5589
5968
  return {
5590
5969
  content: [{ type: "text", text: message }],
@@ -5599,13 +5978,23 @@ async function waitForHandler(args, _context, tabId) {
5599
5978
  isError: true
5600
5979
  };
5601
5980
  }
5602
- const timeout = args.timeout || 3e4;
5981
+ const timeout = args.timeout || DEFAULT_TOOL_TIMEOUT_MS;
5982
+ const selector = args.selector;
5983
+ const text = args.text;
5984
+ if (!selector && !text) {
5985
+ await new Promise((resolve) => setTimeout(resolve, timeout));
5986
+ return {
5987
+ content: [{ type: "text", text: `Waited for ${timeout}ms` }]
5988
+ };
5989
+ }
5603
5990
  try {
5604
5991
  const message = createContentMessage(ContentAction.WAIT_FOR, {
5605
- selector: args.selector,
5606
- text: args.text,
5607
- state: args.state || "visible",
5608
- timeout
5992
+ ...attachTelemetryToParams(args, {
5993
+ selector,
5994
+ text,
5995
+ state: args.state || "visible",
5996
+ timeout
5997
+ })
5609
5998
  });
5610
5999
  const response = await sendMessageToContentScript(tabId, message);
5611
6000
  if (!response.success) {
@@ -5641,7 +6030,7 @@ async function waitForResponseHandler(args, _context, tabId) {
5641
6030
  isError: true
5642
6031
  };
5643
6032
  }
5644
- const timeout = typeof args.timeout === "number" ? args.timeout : 3e4;
6033
+ const timeout = typeof args.timeout === "number" ? args.timeout : DEFAULT_TOOL_TIMEOUT_MS;
5645
6034
  const url = typeof args.url === "string" ? args.url : void 0;
5646
6035
  const method = typeof args.method === "string" ? args.method : void 0;
5647
6036
  const status = typeof args.status === "number" ? args.status : void 0;
@@ -5793,12 +6182,12 @@ async function clearStorageStateHandler(_args, _context, tabId) {
5793
6182
  }
5794
6183
  }
5795
6184
 
5796
- async function listPagesHandler(_args, context, _tabId) {
6185
+ async function listPagesHandler(args, context, tabId) {
5797
6186
  try {
5798
6187
  const chromeTabs = await globalThis.chrome.tabs.query({});
5799
6188
  const tabList = chromeTabs.filter((tab) => tab.id !== void 0).map((tab) => {
5800
- const tabId = tab.id;
5801
- const pageId = context.tabRegistry.getByTabId(tabId)?.id || `tab-${tabId}`;
6189
+ const tabId2 = tab.id;
6190
+ const pageId = context.tabRegistry.getByTabId(tabId2)?.id || `tab-${tabId2}`;
5802
6191
  return {
5803
6192
  id: pageId,
5804
6193
  url: tab.url || "",
@@ -5822,7 +6211,7 @@ async function listPagesHandler(_args, context, _tabId) {
5822
6211
  };
5823
6212
  }
5824
6213
  }
5825
- async function newPageHandler(args, context, _tabId) {
6214
+ async function newPageHandler(args, context, tabId) {
5826
6215
  try {
5827
6216
  const url = args.url || "about:blank";
5828
6217
  const tab = await globalThis.chrome.tabs.create({ url });
@@ -5850,7 +6239,7 @@ async function newPageHandler(args, context, _tabId) {
5850
6239
  };
5851
6240
  }
5852
6241
  }
5853
- async function selectPageHandler(args, context, _tabId) {
6242
+ async function selectPageHandler(args, context, tabId) {
5854
6243
  const pageId = args.pageId;
5855
6244
  if (!pageId) {
5856
6245
  return {
@@ -5884,7 +6273,7 @@ async function selectPageHandler(args, context, _tabId) {
5884
6273
  };
5885
6274
  }
5886
6275
  }
5887
- async function closePageHandler(args, context, _tabId) {
6276
+ async function closePageHandler(args, context, tabId) {
5888
6277
  const pageId = args.pageId;
5889
6278
  if (!pageId) {
5890
6279
  return {
@@ -5934,6 +6323,7 @@ const toolHandlers = {
5934
6323
  browser_click: clickHandler,
5935
6324
  browser_fill: fillHandler,
5936
6325
  browser_type: typeHandler,
6326
+ browser_upload_file: uploadFileHandler,
5937
6327
  // Extended input
5938
6328
  browser_select: selectHandler,
5939
6329
  browser_hover: hoverHandler,
@@ -5987,6 +6377,226 @@ async function executeToolRequest(request, context) {
5987
6377
  }
5988
6378
  }
5989
6379
 
6380
+ const SCOPE_NAME = "browse-tool-extension";
6381
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
6382
+ function randomHex(byteLength) {
6383
+ const bytes = new Uint8Array(byteLength);
6384
+ crypto.getRandomValues(bytes);
6385
+ return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
6386
+ }
6387
+ function nowUnixNano() {
6388
+ return `${BigInt(Date.now()) * 1000000n}`;
6389
+ }
6390
+ function sanitizeAttributes(attributes) {
6391
+ if (!attributes) {
6392
+ return [];
6393
+ }
6394
+ return Object.entries(attributes).filter(([, value]) => value !== void 0 && value !== null).map(([key, value]) => ({
6395
+ key,
6396
+ value: toOtlpAnyValue(value)
6397
+ }));
6398
+ }
6399
+ function toOtlpAnyValue(value) {
6400
+ if (typeof value === "boolean") {
6401
+ return { boolValue: value };
6402
+ }
6403
+ if (typeof value === "number") {
6404
+ return Number.isInteger(value) ? { intValue: value } : { doubleValue: value };
6405
+ }
6406
+ if (Array.isArray(value)) {
6407
+ return {
6408
+ arrayValue: {
6409
+ values: value.map((item) => toOtlpAnyValue(item))
6410
+ }
6411
+ };
6412
+ }
6413
+ if (typeof value === "object" && value !== null) {
6414
+ return { stringValue: JSON.stringify(value) };
6415
+ }
6416
+ return { stringValue: String(value) };
6417
+ }
6418
+ function severityNumber(level) {
6419
+ switch (level) {
6420
+ case "trace":
6421
+ return 1;
6422
+ case "debug":
6423
+ return 5;
6424
+ case "info":
6425
+ return 9;
6426
+ case "warn":
6427
+ return 13;
6428
+ case "error":
6429
+ return 17;
6430
+ case "fatal":
6431
+ return 21;
6432
+ }
6433
+ }
6434
+ function safeRequestOrigin(serverUrl) {
6435
+ try {
6436
+ return new URL(serverUrl).origin;
6437
+ } catch {
6438
+ return void 0;
6439
+ }
6440
+ }
6441
+ function isLoopbackHost(hostname) {
6442
+ return LOOPBACK_HOSTS.has(hostname);
6443
+ }
6444
+ class ExtensionTelemetryClient {
6445
+ config = {
6446
+ enabled: false,
6447
+ serviceName: "browse-tool-extension"
6448
+ };
6449
+ configuredServerOrigin;
6450
+ async configure(serverUrl) {
6451
+ const requestOrigin = safeRequestOrigin(serverUrl);
6452
+ if (!requestOrigin || this.configuredServerOrigin === requestOrigin) {
6453
+ return;
6454
+ }
6455
+ try {
6456
+ const response = await fetch(`${requestOrigin}/extension/telemetry-config`, {
6457
+ method: "GET",
6458
+ headers: {
6459
+ Accept: "application/json"
6460
+ }
6461
+ });
6462
+ if (!response.ok) {
6463
+ throw new Error(`HTTP ${response.status}`);
6464
+ }
6465
+ const config = await response.json();
6466
+ this.config = config;
6467
+ this.configuredServerOrigin = requestOrigin;
6468
+ } catch {
6469
+ this.config = {
6470
+ enabled: false,
6471
+ serviceName: this.config.serviceName
6472
+ };
6473
+ this.configuredServerOrigin = requestOrigin;
6474
+ }
6475
+ }
6476
+ getConfig() {
6477
+ return this.config;
6478
+ }
6479
+ startSpan(name, parentContext, options = {}) {
6480
+ const context = {
6481
+ traceId: parentContext?.traceId ?? randomHex(16),
6482
+ spanId: randomHex(8),
6483
+ ...parentContext?.spanId ? { parentSpanId: parentContext.spanId } : {}
6484
+ };
6485
+ const startTimeUnixNano = nowUnixNano();
6486
+ return {
6487
+ context,
6488
+ end: async (status) => {
6489
+ if (!this.config.enabled || !this.config.tracesEndpoint) {
6490
+ return;
6491
+ }
6492
+ await this.postJson(this.config.tracesEndpoint, {
6493
+ resourceSpans: [
6494
+ {
6495
+ resource: {
6496
+ attributes: [
6497
+ {
6498
+ key: "service.name",
6499
+ value: { stringValue: this.config.serviceName }
6500
+ },
6501
+ {
6502
+ key: "telemetry.sdk.language",
6503
+ value: { stringValue: "webjs" }
6504
+ }
6505
+ ]
6506
+ },
6507
+ scopeSpans: [
6508
+ {
6509
+ scope: { name: SCOPE_NAME },
6510
+ spans: [
6511
+ {
6512
+ traceId: context.traceId,
6513
+ spanId: context.spanId,
6514
+ parentSpanId: context.parentSpanId,
6515
+ name,
6516
+ kind: 1,
6517
+ startTimeUnixNano,
6518
+ endTimeUnixNano: nowUnixNano(),
6519
+ attributes: sanitizeAttributes(options.attributes),
6520
+ ...status ? {
6521
+ status: {
6522
+ code: status.code ?? 1,
6523
+ ...status.message ? { message: status.message } : {}
6524
+ }
6525
+ } : {}
6526
+ }
6527
+ ]
6528
+ }
6529
+ ]
6530
+ }
6531
+ ]
6532
+ });
6533
+ }
6534
+ };
6535
+ }
6536
+ async log(level, message, options = {}) {
6537
+ if (!this.config.enabled || !this.config.logsEndpoint) {
6538
+ return;
6539
+ }
6540
+ await this.postJson(this.config.logsEndpoint, {
6541
+ resourceLogs: [
6542
+ {
6543
+ resource: {
6544
+ attributes: [
6545
+ {
6546
+ key: "service.name",
6547
+ value: { stringValue: this.config.serviceName }
6548
+ },
6549
+ {
6550
+ key: "telemetry.sdk.language",
6551
+ value: { stringValue: "webjs" }
6552
+ }
6553
+ ]
6554
+ },
6555
+ scopeLogs: [
6556
+ {
6557
+ scope: { name: SCOPE_NAME },
6558
+ logRecords: [
6559
+ {
6560
+ timeUnixNano: nowUnixNano(),
6561
+ observedTimeUnixNano: nowUnixNano(),
6562
+ severityText: level.toUpperCase(),
6563
+ severityNumber: severityNumber(level),
6564
+ body: { stringValue: message },
6565
+ attributes: sanitizeAttributes(options.attributes),
6566
+ ...options.context ? {
6567
+ traceId: options.context.traceId,
6568
+ spanId: options.context.spanId
6569
+ } : {}
6570
+ }
6571
+ ]
6572
+ }
6573
+ ]
6574
+ }
6575
+ ]
6576
+ });
6577
+ }
6578
+ async postJson(url, body) {
6579
+ try {
6580
+ const parsed = new URL(url);
6581
+ if (this.configuredServerOrigin) {
6582
+ const serverOrigin = new URL(this.configuredServerOrigin);
6583
+ if (isLoopbackHost(parsed.hostname) && !isLoopbackHost(serverOrigin.hostname)) {
6584
+ parsed.hostname = serverOrigin.hostname;
6585
+ }
6586
+ }
6587
+ await fetch(parsed.toString(), {
6588
+ method: "POST",
6589
+ headers: {
6590
+ "Content-Type": "application/json"
6591
+ },
6592
+ body: JSON.stringify(body)
6593
+ });
6594
+ } catch {
6595
+ }
6596
+ }
6597
+ }
6598
+ const extensionTelemetryClient = new ExtensionTelemetryClient();
6599
+
5990
6600
  class ExtensionHttpClient {
5991
6601
  serverUrl;
5992
6602
  connected = false;
@@ -6365,6 +6975,10 @@ const ContentItemSchema = object({
6365
6975
  data: string().optional(),
6366
6976
  mimeType: string().optional()
6367
6977
  });
6978
+ const TaskTelemetryContextSchema = object({
6979
+ traceId: string().regex(/^[0-9a-f]{32}$/i).optional(),
6980
+ parentSpanId: string().regex(/^[0-9a-f]{16}$/i).optional()
6981
+ });
6368
6982
  const TaskResultContentSchema = object({
6369
6983
  content: array(ContentItemSchema),
6370
6984
  isError: boolean().optional()
@@ -6376,7 +6990,8 @@ const TaskPushSchema = object({
6376
6990
  taskId: string(),
6377
6991
  tool: string(),
6378
6992
  arguments: record(string(), unknown()),
6379
- pageId: string().optional()
6993
+ pageId: string().optional(),
6994
+ telemetry: TaskTelemetryContextSchema.optional()
6380
6995
  })
6381
6996
  });
6382
6997
  const PageCreatedSchema = object({
@@ -6711,6 +7326,7 @@ class TaskPollerService {
6711
7326
  if (serverUrl) {
6712
7327
  this.serverUrl = serverUrl;
6713
7328
  }
7329
+ await extensionTelemetryClient.configure(this.serverUrl);
6714
7330
  this.browserId = browserId ?? `browser-${Date.now()}`;
6715
7331
  this.useWebSocket = true;
6716
7332
  this.status = "registering";
@@ -6751,10 +7367,23 @@ class TaskPollerService {
6751
7367
  });
6752
7368
  try {
6753
7369
  await this.wsClient.connect();
7370
+ void extensionTelemetryClient.log("info", "extension websocket connected", {
7371
+ attributes: {
7372
+ "browse_tool.browser.id": this.browserId,
7373
+ "browse_tool.server.url": this.serverUrl
7374
+ }
7375
+ });
6754
7376
  } catch (error) {
6755
7377
  this.lastError = error instanceof Error ? error.message : String(error);
6756
7378
  this.status = "error";
6757
7379
  this.useWebSocket = false;
7380
+ void extensionTelemetryClient.log("error", "extension websocket connect failed", {
7381
+ attributes: {
7382
+ "browse_tool.browser.id": this.browserId,
7383
+ "browse_tool.server.url": this.serverUrl,
7384
+ "error.message": this.lastError
7385
+ }
7386
+ });
6758
7387
  }
6759
7388
  }
6760
7389
  handleWebSocketTask(task) {
@@ -6767,7 +7396,15 @@ class TaskPollerService {
6767
7396
  const serverTask = {
6768
7397
  id: task.payload.taskId,
6769
7398
  tool: task.payload.tool,
6770
- arguments: task.payload.arguments
7399
+ arguments: {
7400
+ ...task.payload.arguments,
7401
+ ...task.payload.telemetry ? {
7402
+ __browseToolTelemetry: {
7403
+ traceId: task.payload.telemetry.traceId,
7404
+ parentSpanId: task.payload.telemetry.parentSpanId
7405
+ }
7406
+ } : {}
7407
+ }
6771
7408
  };
6772
7409
  this.executeTaskWithWebSocket(serverTask).then(() => {
6773
7410
  this.taskCount++;
@@ -6780,30 +7417,7 @@ class TaskPollerService {
6780
7417
  });
6781
7418
  }
6782
7419
  async executeTaskWithWebSocket(task) {
6783
- let result;
6784
- try {
6785
- const { pageId, ...restArgs } = task.arguments;
6786
- result = await executeToolRequest(
6787
- {
6788
- id: task.id,
6789
- type: "request",
6790
- tool: task.tool,
6791
- arguments: restArgs,
6792
- pageId
6793
- },
6794
- this.toolContext
6795
- );
6796
- } catch (error) {
6797
- result = {
6798
- content: [
6799
- {
6800
- type: "text",
6801
- text: `Tool execution error: ${error instanceof Error ? error.message : String(error)}`
6802
- }
6803
- ],
6804
- isError: true
6805
- };
6806
- }
7420
+ const result = await this.executeTaskCore(task);
6807
7421
  const resultContent = {
6808
7422
  content: result.content.map((c) => ({
6809
7423
  type: c.type,
@@ -6823,6 +7437,7 @@ class TaskPollerService {
6823
7437
  this.serverUrl = serverUrl;
6824
7438
  this.client.setServerUrl(serverUrl);
6825
7439
  }
7440
+ await extensionTelemetryClient.configure(this.serverUrl);
6826
7441
  this.browserId = browserId ?? `browser-${Date.now()}`;
6827
7442
  this.useWebSocket = false;
6828
7443
  this.status = "registering";
@@ -6912,6 +7527,15 @@ class TaskPollerService {
6912
7527
  if (!task) {
6913
7528
  return;
6914
7529
  }
7530
+ if (task.telemetry) {
7531
+ task.arguments = {
7532
+ ...task.arguments,
7533
+ __browseToolTelemetry: {
7534
+ traceId: task.telemetry.traceId,
7535
+ parentSpanId: task.telemetry.parentSpanId
7536
+ }
7537
+ };
7538
+ }
6915
7539
  this.isExecuting = true;
6916
7540
  this.status = "executing";
6917
7541
  await this.executeTask(task);
@@ -6926,47 +7550,92 @@ class TaskPollerService {
6926
7550
  }
6927
7551
  }
6928
7552
  async executeTask(task) {
7553
+ const result = await this.executeTaskCore(task);
7554
+ const taskResult = {
7555
+ taskId: task.id,
7556
+ success: !result.isError,
7557
+ result: {
7558
+ content: result.content.map((c) => ({
7559
+ type: c.type,
7560
+ text: "text" in c ? c.text : void 0,
7561
+ data: "data" in c ? c.data : void 0,
7562
+ mimeType: "mimeType" in c ? c.mimeType : void 0
7563
+ })),
7564
+ isError: result.isError
7565
+ }
7566
+ };
7567
+ try {
7568
+ await this.client.submitResult(taskResult);
7569
+ } catch {
7570
+ }
7571
+ }
7572
+ async executeTaskCore(task) {
7573
+ const parentContext = extractTelemetryContext(task.arguments);
7574
+ const taskSpan = extensionTelemetryClient.startSpan("browse_tool.extension.task.execute", parentContext, {
7575
+ attributes: {
7576
+ "browse_tool.task.id": task.id,
7577
+ "browse_tool.tool.name": task.tool
7578
+ }
7579
+ });
7580
+ void extensionTelemetryClient.log("info", "extension task execution started", {
7581
+ context: taskSpan.context,
7582
+ attributes: {
7583
+ "browse_tool.task.id": task.id,
7584
+ "browse_tool.tool.name": task.tool
7585
+ }
7586
+ });
6929
7587
  let result;
6930
7588
  try {
6931
- const { pageId, ...restArgs } = task.arguments;
7589
+ const {
7590
+ pageId,
7591
+ __browseToolTelemetry: _unusedTelemetry,
7592
+ ...restArgs
7593
+ } = task.arguments;
7594
+ void _unusedTelemetry;
6932
7595
  result = await executeToolRequest(
6933
7596
  {
6934
7597
  id: task.id,
6935
7598
  type: "request",
6936
7599
  tool: task.tool,
6937
- arguments: restArgs,
7600
+ arguments: {
7601
+ ...restArgs,
7602
+ __browseToolTelemetry: {
7603
+ traceId: taskSpan.context.traceId,
7604
+ spanId: taskSpan.context.spanId
7605
+ }
7606
+ },
6938
7607
  pageId
6939
7608
  },
6940
7609
  this.toolContext
6941
7610
  );
6942
7611
  } catch (error) {
7612
+ const message = error instanceof Error ? error.message : String(error);
6943
7613
  result = {
6944
7614
  content: [
6945
7615
  {
6946
7616
  type: "text",
6947
- text: `Tool execution error: ${error instanceof Error ? error.message : String(error)}`
7617
+ text: `Tool execution error: ${message}`
6948
7618
  }
6949
7619
  ],
6950
7620
  isError: true
6951
7621
  };
6952
7622
  }
6953
- const taskResult = {
6954
- taskId: task.id,
6955
- success: !result.isError,
6956
- result: {
6957
- content: result.content.map((c) => ({
6958
- type: c.type,
6959
- text: "text" in c ? c.text : void 0,
6960
- data: "data" in c ? c.data : void 0,
6961
- mimeType: "mimeType" in c ? c.mimeType : void 0
6962
- })),
6963
- isError: result.isError
7623
+ void extensionTelemetryClient.log(result.isError ? "error" : "info", "extension task execution completed", {
7624
+ context: taskSpan.context,
7625
+ attributes: {
7626
+ "browse_tool.task.id": task.id,
7627
+ "browse_tool.tool.name": task.tool,
7628
+ "browse_tool.result.error": Boolean(result.isError),
7629
+ ...result.isError ? { "error.message": extractResultError(result) ?? "Extension tool execution failed" } : {}
6964
7630
  }
6965
- };
6966
- try {
6967
- await this.client.submitResult(taskResult);
6968
- } catch {
6969
- }
7631
+ });
7632
+ await taskSpan.end(
7633
+ result.isError ? {
7634
+ code: 2,
7635
+ message: extractResultError(result) ?? "Extension tool execution failed"
7636
+ } : { code: 1 }
7637
+ );
7638
+ return result;
6970
7639
  }
6971
7640
  getState() {
6972
7641
  const connectionStatus = this.client.getConnectionStatus();
@@ -6995,6 +7664,29 @@ class TaskPollerService {
6995
7664
  }
6996
7665
  }
6997
7666
  const taskPollerService = new TaskPollerService();
7667
+ function extractTelemetryContext(args) {
7668
+ const telemetry = args.__browseToolTelemetry;
7669
+ if (!telemetry || typeof telemetry !== "object") {
7670
+ return void 0;
7671
+ }
7672
+ const traceId = typeof telemetry.traceId === "string" ? telemetry.traceId : void 0;
7673
+ const spanId = typeof telemetry.spanId === "string" ? telemetry.spanId : void 0;
7674
+ const parentSpanId = typeof telemetry.parentSpanId === "string" ? telemetry.parentSpanId : void 0;
7675
+ if (!traceId) {
7676
+ return void 0;
7677
+ }
7678
+ return {
7679
+ traceId,
7680
+ spanId: spanId ?? parentSpanId ?? "0000000000000000"
7681
+ };
7682
+ }
7683
+ function extractResultError(result) {
7684
+ if (!result.isError) {
7685
+ return void 0;
7686
+ }
7687
+ const firstContent = result.content[0];
7688
+ return firstContent?.type === "text" ? firstContent.text : void 0;
7689
+ }
6998
7690
 
6999
7691
  const toolContext = {
7000
7692
  tabRegistry
@@ -7211,6 +7903,14 @@ chrome.alarms.onAlarm.addListener((alarm) => {
7211
7903
  }
7212
7904
  });
7213
7905
  chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
7906
+ if (message.type === "BROWSER_TELEMETRY_EVENT") {
7907
+ void extensionTelemetryClient.log(message.level || "info", message.message || "browser telemetry event", {
7908
+ attributes: typeof message.attributes === "object" && message.attributes !== null ? message.attributes : void 0,
7909
+ context: typeof message.context === "object" && message.context !== null ? message.context : void 0
7910
+ });
7911
+ sendResponse({ success: true });
7912
+ return true;
7913
+ }
7214
7914
  if (message.type === "CONNECT_TO_SERVER") {
7215
7915
  const serverUrl = message.serverUrl || "http://localhost:3200";
7216
7916
  const browserId = message.browserId;