@different-ai/opencode-browser 4.0.7 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/plugin.js +59 -5
- package/extension/background.js +645 -49
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/plugin.js
CHANGED
|
@@ -12529,10 +12529,11 @@ var plugin = async (ctx) => {
|
|
|
12529
12529
|
description: "Click an element on the page using a CSS selector",
|
|
12530
12530
|
args: {
|
|
12531
12531
|
selector: schema.string(),
|
|
12532
|
+
index: schema.number().optional(),
|
|
12532
12533
|
tabId: schema.number().optional()
|
|
12533
12534
|
},
|
|
12534
|
-
async execute({ selector, tabId }, ctx2) {
|
|
12535
|
-
const data = await brokerRequest("tool", { tool: "click", args: { selector, tabId } });
|
|
12535
|
+
async execute({ selector, index, tabId }, ctx2) {
|
|
12536
|
+
const data = await brokerRequest("tool", { tool: "click", args: { selector, index, tabId } });
|
|
12536
12537
|
return toolResultText(data, `Clicked ${selector}`);
|
|
12537
12538
|
}
|
|
12538
12539
|
}),
|
|
@@ -12542,10 +12543,11 @@ var plugin = async (ctx) => {
|
|
|
12542
12543
|
selector: schema.string(),
|
|
12543
12544
|
text: schema.string(),
|
|
12544
12545
|
clear: schema.boolean().optional(),
|
|
12546
|
+
index: schema.number().optional(),
|
|
12545
12547
|
tabId: schema.number().optional()
|
|
12546
12548
|
},
|
|
12547
|
-
async execute({ selector, text, clear, tabId }, ctx2) {
|
|
12548
|
-
const data = await brokerRequest("tool", { tool: "type", args: { selector, text, clear, tabId } });
|
|
12549
|
+
async execute({ selector, text, clear, index, tabId }, ctx2) {
|
|
12550
|
+
const data = await brokerRequest("tool", { tool: "type", args: { selector, text, clear, index, tabId } });
|
|
12549
12551
|
return toolResultText(data, `Typed "${text}" into ${selector}`);
|
|
12550
12552
|
}
|
|
12551
12553
|
}),
|
|
@@ -12594,7 +12596,7 @@ var plugin = async (ctx) => {
|
|
|
12594
12596
|
}
|
|
12595
12597
|
}),
|
|
12596
12598
|
browser_execute: tool({
|
|
12597
|
-
description: "Execute JavaScript
|
|
12599
|
+
description: "(Deprecated) Execute arbitrary JavaScript in-page. Blocked on many sites by MV3 CSP/unsafe-eval. This tool now accepts JSON commands; prefer browser_query/browser_extract/browser_wait_for.",
|
|
12598
12600
|
args: {
|
|
12599
12601
|
code: schema.string(),
|
|
12600
12602
|
tabId: schema.number().optional()
|
|
@@ -12604,6 +12606,58 @@ var plugin = async (ctx) => {
|
|
|
12604
12606
|
return toolResultText(data, "Execute failed");
|
|
12605
12607
|
}
|
|
12606
12608
|
}),
|
|
12609
|
+
browser_query: tool({
|
|
12610
|
+
description: "Read data from the page using selectors (supports shadow DOM + same-origin iframes).",
|
|
12611
|
+
args: {
|
|
12612
|
+
selector: schema.string(),
|
|
12613
|
+
mode: schema.string().optional(),
|
|
12614
|
+
attribute: schema.string().optional(),
|
|
12615
|
+
property: schema.string().optional(),
|
|
12616
|
+
index: schema.number().optional(),
|
|
12617
|
+
limit: schema.number().optional(),
|
|
12618
|
+
tabId: schema.number().optional()
|
|
12619
|
+
},
|
|
12620
|
+
async execute({ selector, mode, attribute, property, index, limit, tabId }, ctx2) {
|
|
12621
|
+
const data = await brokerRequest("tool", {
|
|
12622
|
+
tool: "query",
|
|
12623
|
+
args: { selector, mode, attribute, property, index, limit, tabId }
|
|
12624
|
+
});
|
|
12625
|
+
return toolResultText(data, "Query failed");
|
|
12626
|
+
}
|
|
12627
|
+
}),
|
|
12628
|
+
browser_wait_for: tool({
|
|
12629
|
+
description: "Wait until a selector appears (supports shadow DOM + same-origin iframes).",
|
|
12630
|
+
args: {
|
|
12631
|
+
selector: schema.string(),
|
|
12632
|
+
timeoutMs: schema.number().optional(),
|
|
12633
|
+
pollMs: schema.number().optional(),
|
|
12634
|
+
tabId: schema.number().optional()
|
|
12635
|
+
},
|
|
12636
|
+
async execute({ selector, timeoutMs, pollMs, tabId }, ctx2) {
|
|
12637
|
+
const data = await brokerRequest("tool", {
|
|
12638
|
+
tool: "wait_for",
|
|
12639
|
+
args: { selector, timeoutMs, pollMs, tabId }
|
|
12640
|
+
});
|
|
12641
|
+
return toolResultText(data, "Wait-for failed");
|
|
12642
|
+
}
|
|
12643
|
+
}),
|
|
12644
|
+
browser_extract: tool({
|
|
12645
|
+
description: "Extract readable text from the page (optionally regex match). Useful when content isn't in the accessibility tree.",
|
|
12646
|
+
args: {
|
|
12647
|
+
mode: schema.string().optional(),
|
|
12648
|
+
pattern: schema.string().optional(),
|
|
12649
|
+
flags: schema.string().optional(),
|
|
12650
|
+
limit: schema.number().optional(),
|
|
12651
|
+
tabId: schema.number().optional()
|
|
12652
|
+
},
|
|
12653
|
+
async execute({ mode, pattern, flags, limit, tabId }, ctx2) {
|
|
12654
|
+
const data = await brokerRequest("tool", {
|
|
12655
|
+
tool: "extract",
|
|
12656
|
+
args: { mode, pattern, flags, limit, tabId }
|
|
12657
|
+
});
|
|
12658
|
+
return toolResultText(data, "Extract failed");
|
|
12659
|
+
}
|
|
12660
|
+
}),
|
|
12607
12661
|
browser_claim_tab: tool({
|
|
12608
12662
|
description: "Claim a tab for this OpenCode session (per-tab ownership).",
|
|
12609
12663
|
args: {
|
package/extension/background.js
CHANGED
|
@@ -15,7 +15,9 @@ chrome.alarms.onAlarm.addListener((alarm) => {
|
|
|
15
15
|
|
|
16
16
|
function connect() {
|
|
17
17
|
if (port) {
|
|
18
|
-
try {
|
|
18
|
+
try {
|
|
19
|
+
port.disconnect()
|
|
20
|
+
} catch {}
|
|
19
21
|
port = null
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -35,7 +37,6 @@ function connect() {
|
|
|
35
37
|
|
|
36
38
|
const err = chrome.runtime.lastError
|
|
37
39
|
if (err?.message) {
|
|
38
|
-
// Usually means native host not installed or crashed
|
|
39
40
|
connectionAttempts++
|
|
40
41
|
if (connectionAttempts === 1) {
|
|
41
42
|
console.log("[OpenCode] Native host not available. Run: npx @different-ai/opencode-browser install")
|
|
@@ -104,6 +105,9 @@ async function executeTool(toolName, args) {
|
|
|
104
105
|
type: toolType,
|
|
105
106
|
screenshot: toolScreenshot,
|
|
106
107
|
snapshot: toolSnapshot,
|
|
108
|
+
extract: toolExtract,
|
|
109
|
+
query: toolQuery,
|
|
110
|
+
wait_for: toolWaitFor,
|
|
107
111
|
execute_script: toolExecuteScript,
|
|
108
112
|
scroll: toolScroll,
|
|
109
113
|
wait: toolWait,
|
|
@@ -151,52 +155,250 @@ async function toolNavigate({ url, tabId }) {
|
|
|
151
155
|
return { tabId: tab.id, content: `Navigated to ${url}` }
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
|
|
158
|
+
function normalizeSelectorList(selector) {
|
|
159
|
+
if (typeof selector !== "string") return []
|
|
160
|
+
const parts = selector
|
|
161
|
+
.split(",")
|
|
162
|
+
.map((s) => s.trim())
|
|
163
|
+
.filter(Boolean)
|
|
164
|
+
return parts.length ? parts : [selector.trim()].filter(Boolean)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function toolClick({ selector, tabId, index = 0 }) {
|
|
155
168
|
if (!selector) throw new Error("Selector is required")
|
|
156
169
|
const tab = await getTabById(tabId)
|
|
157
170
|
|
|
171
|
+
const selectorList = normalizeSelectorList(selector)
|
|
172
|
+
|
|
158
173
|
const result = await chrome.scripting.executeScript({
|
|
159
174
|
target: { tabId: tab.id },
|
|
160
|
-
func: (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
func: (selectors, index) => {
|
|
176
|
+
function safeString(v) {
|
|
177
|
+
return typeof v === "string" ? v : ""
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isVisible(el) {
|
|
181
|
+
if (!el) return false
|
|
182
|
+
const rect = el.getBoundingClientRect()
|
|
183
|
+
if (rect.width <= 0 || rect.height <= 0) return false
|
|
184
|
+
const style = window.getComputedStyle(el)
|
|
185
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function deepQuerySelectorAll(sel, rootDoc) {
|
|
190
|
+
const out = []
|
|
191
|
+
const seen = new Set()
|
|
192
|
+
|
|
193
|
+
function addAll(nodeList) {
|
|
194
|
+
for (const el of nodeList) {
|
|
195
|
+
if (!el || seen.has(el)) continue
|
|
196
|
+
seen.add(el)
|
|
197
|
+
out.push(el)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function walkRoot(root, depth) {
|
|
202
|
+
if (!root || depth > 6) return
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
addAll(root.querySelectorAll(sel))
|
|
206
|
+
} catch {
|
|
207
|
+
// Invalid selector
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const tree = root.querySelectorAll ? root.querySelectorAll("*") : []
|
|
212
|
+
for (const el of tree) {
|
|
213
|
+
if (el.shadowRoot) {
|
|
214
|
+
walkRoot(el.shadowRoot, depth + 1)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Same-origin iframes only
|
|
219
|
+
const frames = root.querySelectorAll ? root.querySelectorAll("iframe") : []
|
|
220
|
+
for (const frame of frames) {
|
|
221
|
+
try {
|
|
222
|
+
const doc = frame.contentDocument
|
|
223
|
+
if (doc) walkRoot(doc, depth + 1)
|
|
224
|
+
} catch {
|
|
225
|
+
// cross-origin
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
walkRoot(rootDoc || document, 0)
|
|
231
|
+
return out
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function tryClick(el) {
|
|
235
|
+
try {
|
|
236
|
+
el.scrollIntoView({ block: "center", inline: "center" })
|
|
237
|
+
} catch {}
|
|
238
|
+
|
|
239
|
+
const rect = el.getBoundingClientRect()
|
|
240
|
+
const x = Math.min(Math.max(rect.left + rect.width / 2, 0), window.innerWidth - 1)
|
|
241
|
+
const y = Math.min(Math.max(rect.top + rect.height / 2, 0), window.innerHeight - 1)
|
|
242
|
+
|
|
243
|
+
const opts = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y }
|
|
244
|
+
try {
|
|
245
|
+
el.dispatchEvent(new MouseEvent("mouseover", opts))
|
|
246
|
+
el.dispatchEvent(new MouseEvent("mousemove", opts))
|
|
247
|
+
el.dispatchEvent(new MouseEvent("mousedown", opts))
|
|
248
|
+
el.dispatchEvent(new MouseEvent("mouseup", opts))
|
|
249
|
+
el.dispatchEvent(new MouseEvent("click", opts))
|
|
250
|
+
} catch {}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
el.click()
|
|
254
|
+
} catch {}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (const sel of selectors) {
|
|
258
|
+
const s = safeString(sel)
|
|
259
|
+
if (!s) continue
|
|
260
|
+
|
|
261
|
+
const matches = deepQuerySelectorAll(s, document)
|
|
262
|
+
const visible = matches.filter(isVisible)
|
|
263
|
+
const chosen = visible[index] || matches[index]
|
|
264
|
+
if (chosen) {
|
|
265
|
+
tryClick(chosen)
|
|
266
|
+
return { success: true, selectorUsed: s }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { success: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
|
|
165
271
|
},
|
|
166
|
-
args: [
|
|
272
|
+
args: [selectorList, index],
|
|
273
|
+
world: "ISOLATED",
|
|
167
274
|
})
|
|
168
275
|
|
|
169
276
|
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Click failed")
|
|
170
|
-
|
|
277
|
+
const used = result[0]?.result?.selectorUsed || selector
|
|
278
|
+
return { tabId: tab.id, content: `Clicked ${used}` }
|
|
171
279
|
}
|
|
172
280
|
|
|
173
|
-
async function toolType({ selector, text, tabId, clear = false }) {
|
|
281
|
+
async function toolType({ selector, text, tabId, clear = false, index = 0 }) {
|
|
174
282
|
if (!selector) throw new Error("Selector is required")
|
|
175
283
|
if (text === undefined) throw new Error("Text is required")
|
|
176
284
|
const tab = await getTabById(tabId)
|
|
177
285
|
|
|
286
|
+
const selectorList = normalizeSelectorList(selector)
|
|
287
|
+
|
|
178
288
|
const result = await chrome.scripting.executeScript({
|
|
179
289
|
target: { tabId: tab.id },
|
|
180
|
-
func: (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
290
|
+
func: (selectors, txt, shouldClear, index) => {
|
|
291
|
+
function isVisible(el) {
|
|
292
|
+
if (!el) return false
|
|
293
|
+
const rect = el.getBoundingClientRect()
|
|
294
|
+
if (rect.width <= 0 || rect.height <= 0) return false
|
|
295
|
+
const style = window.getComputedStyle(el)
|
|
296
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function deepQuerySelectorAll(sel, rootDoc) {
|
|
301
|
+
const out = []
|
|
302
|
+
const seen = new Set()
|
|
303
|
+
|
|
304
|
+
function addAll(nodeList) {
|
|
305
|
+
for (const el of nodeList) {
|
|
306
|
+
if (!el || seen.has(el)) continue
|
|
307
|
+
seen.add(el)
|
|
308
|
+
out.push(el)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function walkRoot(root, depth) {
|
|
313
|
+
if (!root || depth > 6) return
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
addAll(root.querySelectorAll(sel))
|
|
317
|
+
} catch {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const tree = root.querySelectorAll ? root.querySelectorAll("*") : []
|
|
322
|
+
for (const el of tree) {
|
|
323
|
+
if (el.shadowRoot) {
|
|
324
|
+
walkRoot(el.shadowRoot, depth + 1)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const frames = root.querySelectorAll ? root.querySelectorAll("iframe") : []
|
|
329
|
+
for (const frame of frames) {
|
|
330
|
+
try {
|
|
331
|
+
const doc = frame.contentDocument
|
|
332
|
+
if (doc) walkRoot(doc, depth + 1)
|
|
333
|
+
} catch {}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
walkRoot(rootDoc || document, 0)
|
|
338
|
+
return out
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function setNativeValue(el, value) {
|
|
342
|
+
const tag = el.tagName
|
|
343
|
+
if (tag === "INPUT" || tag === "TEXTAREA") {
|
|
344
|
+
const proto = tag === "INPUT" ? window.HTMLInputElement.prototype : window.HTMLTextAreaElement.prototype
|
|
345
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set
|
|
346
|
+
if (setter) setter.call(el, value)
|
|
347
|
+
else el.value = value
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
return false
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const sel of selectors) {
|
|
354
|
+
if (!sel) continue
|
|
355
|
+
const matches = deepQuerySelectorAll(sel, document)
|
|
356
|
+
const visible = matches.filter(isVisible)
|
|
357
|
+
const el = visible[index] || matches[index]
|
|
358
|
+
if (!el) continue
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
el.scrollIntoView({ block: "center", inline: "center" })
|
|
362
|
+
} catch {}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
el.focus()
|
|
366
|
+
} catch {}
|
|
367
|
+
|
|
368
|
+
const tag = el.tagName
|
|
369
|
+
const isTextInput = tag === "INPUT" || tag === "TEXTAREA"
|
|
370
|
+
|
|
371
|
+
if (isTextInput) {
|
|
372
|
+
if (shouldClear) setNativeValue(el, "")
|
|
373
|
+
setNativeValue(el, (el.value || "") + txt)
|
|
374
|
+
el.dispatchEvent(new Event("input", { bubbles: true }))
|
|
375
|
+
el.dispatchEvent(new Event("change", { bubbles: true }))
|
|
376
|
+
return { success: true, selectorUsed: sel }
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (el.isContentEditable) {
|
|
380
|
+
if (shouldClear) el.textContent = ""
|
|
381
|
+
try {
|
|
382
|
+
document.execCommand("insertText", false, txt)
|
|
383
|
+
} catch {
|
|
384
|
+
el.textContent = (el.textContent || "") + txt
|
|
385
|
+
}
|
|
386
|
+
el.dispatchEvent(new Event("input", { bubbles: true }))
|
|
387
|
+
return { success: true, selectorUsed: sel }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return { success: false, error: `Element is not typable: ${sel} (${tag.toLowerCase()})` }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { success: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
|
|
194
394
|
},
|
|
195
|
-
args: [
|
|
395
|
+
args: [selectorList, text, !!clear, index],
|
|
396
|
+
world: "ISOLATED",
|
|
196
397
|
})
|
|
197
398
|
|
|
198
399
|
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Type failed")
|
|
199
|
-
|
|
400
|
+
const used = result[0]?.result?.selectorUsed || selector
|
|
401
|
+
return { tabId: tab.id, content: `Typed "${text}" into ${used}` }
|
|
200
402
|
}
|
|
201
403
|
|
|
202
404
|
async function toolScreenshot({ tabId }) {
|
|
@@ -211,56 +413,109 @@ async function toolSnapshot({ tabId }) {
|
|
|
211
413
|
const result = await chrome.scripting.executeScript({
|
|
212
414
|
target: { tabId: tab.id },
|
|
213
415
|
func: () => {
|
|
416
|
+
function safeText(s) {
|
|
417
|
+
return typeof s === "string" ? s : ""
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isVisible(el) {
|
|
421
|
+
if (!el) return false
|
|
422
|
+
const rect = el.getBoundingClientRect()
|
|
423
|
+
if (rect.width <= 0 || rect.height <= 0) return false
|
|
424
|
+
const style = window.getComputedStyle(el)
|
|
425
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
426
|
+
return true
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function pseudoText(el) {
|
|
430
|
+
try {
|
|
431
|
+
const before = window.getComputedStyle(el, "::before").content
|
|
432
|
+
const after = window.getComputedStyle(el, "::after").content
|
|
433
|
+
const norm = (v) => {
|
|
434
|
+
const s = safeText(v)
|
|
435
|
+
if (!s || s === "none") return ""
|
|
436
|
+
return s.replace(/^"|"$/g, "")
|
|
437
|
+
}
|
|
438
|
+
return { before: norm(before), after: norm(after) }
|
|
439
|
+
} catch {
|
|
440
|
+
return { before: "", after: "" }
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
214
444
|
function getName(el) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
)
|
|
445
|
+
const aria = el.getAttribute("aria-label")
|
|
446
|
+
if (aria) return aria
|
|
447
|
+
const alt = el.getAttribute("alt")
|
|
448
|
+
if (alt) return alt
|
|
449
|
+
const title = el.getAttribute("title")
|
|
450
|
+
if (title) return title
|
|
451
|
+
const placeholder = el.getAttribute("placeholder")
|
|
452
|
+
if (placeholder) return placeholder
|
|
453
|
+
const txt = safeText(el.innerText)
|
|
454
|
+
if (txt.trim()) return txt.slice(0, 200)
|
|
455
|
+
const pt = pseudoText(el)
|
|
456
|
+
const combo = `${pt.before} ${pt.after}`.trim()
|
|
457
|
+
if (combo) return combo.slice(0, 200)
|
|
458
|
+
return ""
|
|
223
459
|
}
|
|
224
460
|
|
|
225
461
|
function build(el, depth = 0, uid = 0) {
|
|
226
|
-
if (depth >
|
|
462
|
+
if (!el || depth > 12) return { nodes: [], nextUid: uid }
|
|
227
463
|
const nodes = []
|
|
228
|
-
|
|
229
|
-
if (
|
|
464
|
+
|
|
465
|
+
if (!isVisible(el)) return { nodes: [], nextUid: uid }
|
|
230
466
|
|
|
231
467
|
const isInteractive =
|
|
232
468
|
["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"].includes(el.tagName) ||
|
|
233
469
|
el.getAttribute("onclick") ||
|
|
234
470
|
el.getAttribute("role") === "button" ||
|
|
235
471
|
el.isContentEditable
|
|
236
|
-
const rect = el.getBoundingClientRect()
|
|
237
472
|
|
|
238
|
-
|
|
473
|
+
const name = getName(el)
|
|
474
|
+
const pt = pseudoText(el)
|
|
475
|
+
|
|
476
|
+
const shouldInclude = isInteractive || name.trim() || pt.before || pt.after
|
|
477
|
+
|
|
478
|
+
if (shouldInclude) {
|
|
239
479
|
const node = {
|
|
240
480
|
uid: `e${uid}`,
|
|
241
481
|
role: el.getAttribute("role") || el.tagName.toLowerCase(),
|
|
242
|
-
name:
|
|
482
|
+
name: name,
|
|
243
483
|
tag: el.tagName.toLowerCase(),
|
|
244
484
|
}
|
|
485
|
+
|
|
486
|
+
if (pt.before) node.before = pt.before
|
|
487
|
+
if (pt.after) node.after = pt.after
|
|
488
|
+
|
|
245
489
|
if (el.href) node.href = el.href
|
|
246
|
-
|
|
490
|
+
|
|
491
|
+
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
247
492
|
node.type = el.type
|
|
248
493
|
node.value = el.value
|
|
494
|
+
if (el.readOnly) node.readOnly = true
|
|
495
|
+
if (el.disabled) node.disabled = true
|
|
249
496
|
}
|
|
497
|
+
|
|
250
498
|
if (el.id) node.selector = `#${el.id}`
|
|
251
499
|
else if (el.className && typeof el.className === "string") {
|
|
252
500
|
const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".")
|
|
253
501
|
if (cls) node.selector = `${el.tagName.toLowerCase()}.${cls}`
|
|
254
502
|
}
|
|
503
|
+
|
|
255
504
|
nodes.push(node)
|
|
256
505
|
uid++
|
|
257
506
|
}
|
|
258
507
|
|
|
508
|
+
if (el.shadowRoot) {
|
|
509
|
+
const r = build(el.shadowRoot.host, depth + 1, uid)
|
|
510
|
+
uid = r.nextUid
|
|
511
|
+
}
|
|
512
|
+
|
|
259
513
|
for (const child of el.children) {
|
|
260
514
|
const r = build(child, depth + 1, uid)
|
|
261
515
|
nodes.push(...r.nodes)
|
|
262
516
|
uid = r.nextUid
|
|
263
517
|
}
|
|
518
|
+
|
|
264
519
|
return { nodes, nextUid: uid }
|
|
265
520
|
}
|
|
266
521
|
|
|
@@ -275,16 +530,114 @@ async function toolSnapshot({ tabId }) {
|
|
|
275
530
|
links.push({ href, text })
|
|
276
531
|
}
|
|
277
532
|
})
|
|
278
|
-
return links.slice(0,
|
|
533
|
+
return links.slice(0, 200)
|
|
279
534
|
}
|
|
280
535
|
|
|
536
|
+
let pageText = ""
|
|
537
|
+
try {
|
|
538
|
+
pageText = safeText(document.body?.innerText || "").slice(0, 20000)
|
|
539
|
+
} catch {}
|
|
540
|
+
|
|
541
|
+
const built = build(document.body).nodes.slice(0, 800)
|
|
542
|
+
|
|
281
543
|
return {
|
|
282
544
|
url: location.href,
|
|
283
545
|
title: document.title,
|
|
284
|
-
|
|
546
|
+
text: pageText,
|
|
547
|
+
nodes: built,
|
|
285
548
|
links: getAllLinks(),
|
|
286
549
|
}
|
|
287
550
|
},
|
|
551
|
+
world: "ISOLATED",
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
return { tabId: tab.id, content: JSON.stringify(result[0]?.result, null, 2) }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async function toolExtract({ tabId, mode = "combined", pattern, flags = "i", limit = 20000 }) {
|
|
558
|
+
const tab = await getTabById(tabId)
|
|
559
|
+
|
|
560
|
+
const result = await chrome.scripting.executeScript({
|
|
561
|
+
target: { tabId: tab.id },
|
|
562
|
+
func: (mode, pattern, flags, limit) => {
|
|
563
|
+
const cap = (s) => String(s ?? "").slice(0, Math.max(0, limit || 0))
|
|
564
|
+
|
|
565
|
+
const getPseudoText = () => {
|
|
566
|
+
const out = []
|
|
567
|
+
const pushContent = (content) => {
|
|
568
|
+
if (!content) return
|
|
569
|
+
const c = String(content)
|
|
570
|
+
if (!c || c === "none" || c === "normal") return
|
|
571
|
+
const unquoted = c.replace(/^"|"$/g, "").replace(/^'|'$/g, "")
|
|
572
|
+
if (unquoted && unquoted !== "none" && unquoted !== "normal") out.push(unquoted)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const elements = Array.from(document.querySelectorAll("*"))
|
|
576
|
+
for (let i = 0; i < elements.length && out.length < 2000; i++) {
|
|
577
|
+
const el = elements[i]
|
|
578
|
+
try {
|
|
579
|
+
const style = window.getComputedStyle(el)
|
|
580
|
+
if (style.display === "none" || style.visibility === "hidden") continue
|
|
581
|
+
const before = window.getComputedStyle(el, "::before").content
|
|
582
|
+
const after = window.getComputedStyle(el, "::after").content
|
|
583
|
+
pushContent(before)
|
|
584
|
+
pushContent(after)
|
|
585
|
+
} catch {
|
|
586
|
+
// ignore
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return out.join("\n")
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const getInputValues = () => {
|
|
593
|
+
const out = []
|
|
594
|
+
const nodes = document.querySelectorAll("input, textarea")
|
|
595
|
+
nodes.forEach((el) => {
|
|
596
|
+
try {
|
|
597
|
+
const name = el.getAttribute("aria-label") || el.getAttribute("name") || el.id || el.className || el.tagName
|
|
598
|
+
const value = el.value
|
|
599
|
+
if (value != null && String(value).trim()) out.push(`${name}: ${value}`)
|
|
600
|
+
} catch {
|
|
601
|
+
// ignore
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
return out.join("\n")
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const getText = () => {
|
|
608
|
+
try {
|
|
609
|
+
return document.body ? document.body.innerText || "" : ""
|
|
610
|
+
} catch {
|
|
611
|
+
return ""
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const parts = []
|
|
616
|
+
if (mode === "text" || mode === "combined") parts.push(getText())
|
|
617
|
+
if (mode === "pseudo" || mode === "combined") parts.push(getPseudoText())
|
|
618
|
+
if (mode === "inputs" || mode === "combined") parts.push(getInputValues())
|
|
619
|
+
|
|
620
|
+
const text = cap(parts.filter(Boolean).join("\n\n"))
|
|
621
|
+
|
|
622
|
+
let matches = []
|
|
623
|
+
if (pattern) {
|
|
624
|
+
try {
|
|
625
|
+
const re = new RegExp(pattern, flags || "")
|
|
626
|
+
const found = []
|
|
627
|
+
let m
|
|
628
|
+
while ((m = re.exec(text)) && found.length < 50) {
|
|
629
|
+
found.push(m[0])
|
|
630
|
+
if (!re.global) break
|
|
631
|
+
}
|
|
632
|
+
matches = found
|
|
633
|
+
} catch (e) {
|
|
634
|
+
matches = []
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return { url: location.href, title: document.title, mode, text, matches }
|
|
639
|
+
},
|
|
640
|
+
args: [mode, pattern, flags, limit],
|
|
288
641
|
})
|
|
289
642
|
|
|
290
643
|
return { tabId: tab.id, content: JSON.stringify(result[0]?.result, null, 2) }
|
|
@@ -296,13 +649,258 @@ async function toolGetTabs() {
|
|
|
296
649
|
return { content: JSON.stringify(out, null, 2) }
|
|
297
650
|
}
|
|
298
651
|
|
|
652
|
+
async function toolQuery({ tabId, selector, mode = "text", attribute, property, limit = 50, index = 0 }) {
|
|
653
|
+
if (!selector) throw new Error("selector is required")
|
|
654
|
+
const tab = await getTabById(tabId)
|
|
655
|
+
|
|
656
|
+
const selectorList = normalizeSelectorList(selector)
|
|
657
|
+
|
|
658
|
+
const result = await chrome.scripting.executeScript({
|
|
659
|
+
target: { tabId: tab.id },
|
|
660
|
+
func: (selectors, mode, attribute, property, limit, index) => {
|
|
661
|
+
function isVisible(el) {
|
|
662
|
+
if (!el) return false
|
|
663
|
+
const rect = el.getBoundingClientRect()
|
|
664
|
+
if (rect.width <= 0 || rect.height <= 0) return false
|
|
665
|
+
const style = window.getComputedStyle(el)
|
|
666
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
667
|
+
return true
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function deepQuerySelectorAll(sel, rootDoc) {
|
|
671
|
+
const out = []
|
|
672
|
+
const seen = new Set()
|
|
673
|
+
|
|
674
|
+
function addAll(nodeList) {
|
|
675
|
+
for (const el of nodeList) {
|
|
676
|
+
if (!el || seen.has(el)) continue
|
|
677
|
+
seen.add(el)
|
|
678
|
+
out.push(el)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function walkRoot(root, depth) {
|
|
683
|
+
if (!root || depth > 6) return
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
addAll(root.querySelectorAll(sel))
|
|
687
|
+
} catch {
|
|
688
|
+
return
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const tree = root.querySelectorAll ? root.querySelectorAll("*") : []
|
|
692
|
+
for (const el of tree) {
|
|
693
|
+
if (el.shadowRoot) {
|
|
694
|
+
walkRoot(el.shadowRoot, depth + 1)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const frames = root.querySelectorAll ? root.querySelectorAll("iframe") : []
|
|
699
|
+
for (const frame of frames) {
|
|
700
|
+
try {
|
|
701
|
+
const doc = frame.contentDocument
|
|
702
|
+
if (doc) walkRoot(doc, depth + 1)
|
|
703
|
+
} catch {}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
walkRoot(rootDoc || document, 0)
|
|
708
|
+
return out
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
for (const sel of selectors) {
|
|
712
|
+
const matches = deepQuerySelectorAll(sel, document)
|
|
713
|
+
if (!matches.length) continue
|
|
714
|
+
|
|
715
|
+
const visible = matches.filter(isVisible)
|
|
716
|
+
const chosen = visible[index] || matches[index]
|
|
717
|
+
|
|
718
|
+
if (mode === "exists") {
|
|
719
|
+
return { ok: true, selectorUsed: sel, exists: true, count: matches.length }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (!chosen) return { ok: false, error: `No element at index ${index} for ${sel}`, selectorUsed: sel }
|
|
723
|
+
|
|
724
|
+
if (mode === "text") {
|
|
725
|
+
const text = (chosen.innerText || chosen.textContent || "").trim()
|
|
726
|
+
return { ok: true, selectorUsed: sel, value: text }
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (mode === "value") {
|
|
730
|
+
const v = chosen.value
|
|
731
|
+
return { ok: true, selectorUsed: sel, value: typeof v === "string" ? v : String(v ?? "") }
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (mode === "attribute") {
|
|
735
|
+
const a = attribute ? chosen.getAttribute(attribute) : null
|
|
736
|
+
return { ok: true, selectorUsed: sel, value: a }
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (mode === "property") {
|
|
740
|
+
if (!property) return { ok: false, error: "property is required", selectorUsed: sel }
|
|
741
|
+
const v = chosen[property]
|
|
742
|
+
return { ok: true, selectorUsed: sel, value: v }
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (mode === "html") {
|
|
746
|
+
return { ok: true, selectorUsed: sel, value: chosen.outerHTML }
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (mode === "list") {
|
|
750
|
+
const items = matches
|
|
751
|
+
.slice(0, Math.max(1, Math.min(200, limit)))
|
|
752
|
+
.map((el) => ({
|
|
753
|
+
text: (el.innerText || el.textContent || "").trim().slice(0, 200),
|
|
754
|
+
tag: (el.tagName || "").toLowerCase(),
|
|
755
|
+
ariaLabel: el.getAttribute ? el.getAttribute("aria-label") : null,
|
|
756
|
+
}))
|
|
757
|
+
return { ok: true, selectorUsed: sel, items, count: matches.length }
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return { ok: false, error: `Unknown mode: ${mode}`, selectorUsed: sel }
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return { ok: false, error: `No matches for selectors: ${selectors.join(", ")}` }
|
|
764
|
+
},
|
|
765
|
+
args: [selectorList, mode, attribute || null, property || null, limit, index],
|
|
766
|
+
world: "ISOLATED",
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
const r = result[0]?.result
|
|
770
|
+
if (!r?.ok) throw new Error(r?.error || "Query failed")
|
|
771
|
+
|
|
772
|
+
// Keep output predictable: JSON for list/property, string otherwise
|
|
773
|
+
if (mode === "list" || mode === "property") {
|
|
774
|
+
return { tabId: tab.id, content: JSON.stringify(r, null, 2) }
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
return { tabId: tab.id, content: typeof r.value === "string" ? r.value : JSON.stringify(r.value) }
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
async function toolWaitFor({ tabId, selector, timeoutMs = 10000, pollMs = 200 }) {
|
|
781
|
+
if (!selector) throw new Error("selector is required")
|
|
782
|
+
const tab = await getTabById(tabId)
|
|
783
|
+
|
|
784
|
+
const selectorList = normalizeSelectorList(selector)
|
|
785
|
+
|
|
786
|
+
const result = await chrome.scripting.executeScript({
|
|
787
|
+
target: { tabId: tab.id },
|
|
788
|
+
func: async (selectors, timeoutMs, pollMs) => {
|
|
789
|
+
function isVisible(el) {
|
|
790
|
+
if (!el) return false
|
|
791
|
+
const rect = el.getBoundingClientRect()
|
|
792
|
+
if (rect.width <= 0 || rect.height <= 0) return false
|
|
793
|
+
const style = window.getComputedStyle(el)
|
|
794
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
795
|
+
return true
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function deepQuerySelector(sel, rootDoc) {
|
|
799
|
+
function findInRoot(root, depth) {
|
|
800
|
+
if (!root || depth > 6) return null
|
|
801
|
+
try {
|
|
802
|
+
const found = root.querySelector(sel)
|
|
803
|
+
if (found) return found
|
|
804
|
+
} catch {
|
|
805
|
+
return null
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const tree = root.querySelectorAll ? root.querySelectorAll("*") : []
|
|
809
|
+
for (const el of tree) {
|
|
810
|
+
if (el.shadowRoot) {
|
|
811
|
+
const f = findInRoot(el.shadowRoot, depth + 1)
|
|
812
|
+
if (f) return f
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const frames = root.querySelectorAll ? root.querySelectorAll("iframe") : []
|
|
817
|
+
for (const frame of frames) {
|
|
818
|
+
try {
|
|
819
|
+
const doc = frame.contentDocument
|
|
820
|
+
if (doc) {
|
|
821
|
+
const f = findInRoot(doc, depth + 1)
|
|
822
|
+
if (f) return f
|
|
823
|
+
}
|
|
824
|
+
} catch {}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
return null
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return findInRoot(rootDoc || document, 0)
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const start = Date.now()
|
|
834
|
+
while (Date.now() - start < timeoutMs) {
|
|
835
|
+
for (const sel of selectors) {
|
|
836
|
+
if (!sel) continue
|
|
837
|
+
const el = deepQuerySelector(sel, document)
|
|
838
|
+
if (el && isVisible(el)) return { ok: true, selectorUsed: sel }
|
|
839
|
+
}
|
|
840
|
+
await new Promise((r) => setTimeout(r, pollMs))
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return { ok: false, error: `Timed out waiting for selectors: ${selectors.join(", ")}` }
|
|
844
|
+
},
|
|
845
|
+
args: [selectorList, timeoutMs, pollMs],
|
|
846
|
+
world: "ISOLATED",
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
const r = result[0]?.result
|
|
850
|
+
if (!r?.ok) throw new Error(r?.error || "wait_for failed")
|
|
851
|
+
return { tabId: tab.id, content: `Found ${r.selectorUsed}` }
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Legacy tool kept for compatibility.
|
|
855
|
+
// We intentionally do NOT evaluate arbitrary JS strings (unpredictable + CSP/unsafe-eval issues).
|
|
856
|
+
// Instead, accept a JSON payload string describing a query.
|
|
299
857
|
async function toolExecuteScript({ code, tabId }) {
|
|
300
858
|
if (!code) throw new Error("Code is required")
|
|
859
|
+
|
|
860
|
+
let command
|
|
861
|
+
try {
|
|
862
|
+
command = JSON.parse(code)
|
|
863
|
+
} catch {
|
|
864
|
+
throw new Error(
|
|
865
|
+
"browser_execute expects JSON (not raw JS) due to MV3 CSP. Try: {\"op\":\"query\",\"selector\":\"...\",\"return\":\"text\" } or use browser_extract."
|
|
866
|
+
)
|
|
867
|
+
}
|
|
868
|
+
|
|
301
869
|
const tab = await getTabById(tabId)
|
|
302
870
|
const result = await chrome.scripting.executeScript({
|
|
303
871
|
target: { tabId: tab.id },
|
|
304
|
-
func:
|
|
872
|
+
func: (cmd) => {
|
|
873
|
+
const getBySelector = (selector) => {
|
|
874
|
+
if (!selector) return null
|
|
875
|
+
try {
|
|
876
|
+
return document.querySelector(selector)
|
|
877
|
+
} catch {
|
|
878
|
+
return null
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const op = cmd?.op
|
|
883
|
+
if (op === "query") {
|
|
884
|
+
const el = getBySelector(cmd.selector)
|
|
885
|
+
if (!el) return { ok: false, error: "not_found" }
|
|
886
|
+
const ret = cmd.return || "text"
|
|
887
|
+
if (ret === "text") return { ok: true, value: el.innerText ?? el.textContent ?? "" }
|
|
888
|
+
if (ret === "value") return { ok: true, value: el.value }
|
|
889
|
+
if (ret === "html") return { ok: true, value: el.innerHTML }
|
|
890
|
+
if (ret === "attr") return { ok: true, value: el.getAttribute(cmd.name) }
|
|
891
|
+
if (ret === "href") return { ok: true, value: el.href }
|
|
892
|
+
return { ok: false, error: `unknown_return:${ret}` }
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (op === "location") {
|
|
896
|
+
return { ok: true, value: { url: location.href, title: document.title } }
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return { ok: false, error: `unknown_op:${String(op)}` }
|
|
900
|
+
},
|
|
901
|
+
args: [command],
|
|
305
902
|
})
|
|
903
|
+
|
|
306
904
|
return { tabId: tab.id, content: JSON.stringify(result[0]?.result) }
|
|
307
905
|
}
|
|
308
906
|
|
|
@@ -323,15 +921,13 @@ async function toolScroll({ x = 0, y = 0, selector, tabId }) {
|
|
|
323
921
|
window.scrollBy(scrollX, scrollY)
|
|
324
922
|
},
|
|
325
923
|
args: [x, y, sel],
|
|
924
|
+
world: "ISOLATED",
|
|
326
925
|
})
|
|
327
926
|
|
|
328
927
|
return { tabId: tab.id, content: `Scrolled ${sel ? `to ${sel}` : `by (${x}, ${y})`}` }
|
|
329
928
|
}
|
|
330
929
|
|
|
331
930
|
async function toolWait({ ms = 1000, tabId }) {
|
|
332
|
-
if (typeof tabId === "number") {
|
|
333
|
-
// keep tabId in response for ownership purposes
|
|
334
|
-
}
|
|
335
931
|
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
336
932
|
return { tabId, content: `Waited ${ms}ms` }
|
|
337
933
|
}
|
|
@@ -348,4 +944,4 @@ chrome.action.onClicked.addListener(() => {
|
|
|
348
944
|
})
|
|
349
945
|
})
|
|
350
946
|
|
|
351
|
-
connect()
|
|
947
|
+
connect()
|
package/extension/manifest.json
CHANGED