@different-ai/opencode-browser 4.1.0 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -18
- package/dist/plugin.js +6 -75
- package/extension/background.js +384 -575
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,11 +23,11 @@ The installer will:
|
|
|
23
23
|
1. Copy the extension to `~/.opencode-browser/extension/`
|
|
24
24
|
2. Walk you through loading + pinning it in `chrome://extensions`
|
|
25
25
|
3. Ask for the extension ID and install a **Native Messaging Host manifest**
|
|
26
|
-
4. Update your
|
|
26
|
+
4. Update your `opencode.json` or `opencode.jsonc` to load the plugin
|
|
27
27
|
|
|
28
28
|
### Configure OpenCode
|
|
29
29
|
|
|
30
|
-
Your
|
|
30
|
+
Your `opencode.json` or `opencode.jsonc` should contain:
|
|
31
31
|
|
|
32
32
|
```json
|
|
33
33
|
{
|
|
@@ -50,29 +50,24 @@ OpenCode Plugin <-> Local Broker (unix socket) <-> Native Host <-> Chrome Extens
|
|
|
50
50
|
|
|
51
51
|
- First time a session touches a tab, the broker **auto-claims** it for that session.
|
|
52
52
|
- Other sessions attempting to use the same tab will get an error.
|
|
53
|
-
|
|
54
|
-
Tools:
|
|
55
|
-
|
|
56
|
-
- `browser_claim_tab({ tabId })`
|
|
57
|
-
- `browser_release_tab({ tabId })`
|
|
58
|
-
- `browser_list_claims()`
|
|
53
|
+
- Use `browser_status` to inspect claims if needed.
|
|
59
54
|
|
|
60
55
|
## Available tools
|
|
61
56
|
|
|
62
|
-
|
|
57
|
+
Core primitives:
|
|
63
58
|
- `browser_status`
|
|
64
59
|
- `browser_get_tabs`
|
|
65
60
|
- `browser_navigate`
|
|
61
|
+
- `browser_query` (modes: `text`, `value`, `list`, `exists`, `page_text`; optional `timeoutMs`/`pollMs`)
|
|
66
62
|
- `browser_click`
|
|
67
63
|
- `browser_type`
|
|
68
|
-
- `browser_screenshot`
|
|
69
|
-
- `browser_snapshot`
|
|
70
64
|
- `browser_scroll`
|
|
71
65
|
- `browser_wait`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
- `
|
|
75
|
-
- `
|
|
66
|
+
|
|
67
|
+
Diagnostics:
|
|
68
|
+
- `browser_snapshot`
|
|
69
|
+
- `browser_screenshot`
|
|
70
|
+
- `browser_version`
|
|
76
71
|
|
|
77
72
|
## Troubleshooting
|
|
78
73
|
|
|
@@ -81,8 +76,8 @@ Tools:
|
|
|
81
76
|
- Confirm the extension ID you pasted matches the loaded extension in `chrome://extensions`
|
|
82
77
|
|
|
83
78
|
**Tab ownership errors**
|
|
84
|
-
- Use `
|
|
85
|
-
-
|
|
79
|
+
- Use `browser_status` to see current claims
|
|
80
|
+
- Close the other OpenCode session to release ownership
|
|
86
81
|
|
|
87
82
|
## Uninstall
|
|
88
83
|
|
|
@@ -90,4 +85,4 @@ Tools:
|
|
|
90
85
|
npx @different-ai/opencode-browser uninstall
|
|
91
86
|
```
|
|
92
87
|
|
|
93
|
-
Then remove the unpacked extension in `chrome://extensions` and remove the plugin from
|
|
88
|
+
Then remove the unpacked extension in `chrome://extensions` and remove the plugin from `opencode.json` or `opencode.jsonc`.
|
package/dist/plugin.js
CHANGED
|
@@ -12595,96 +12595,27 @@ var plugin = async (ctx) => {
|
|
|
12595
12595
|
return toolResultText(data, "Waited");
|
|
12596
12596
|
}
|
|
12597
12597
|
}),
|
|
12598
|
-
browser_execute: tool({
|
|
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.",
|
|
12600
|
-
args: {
|
|
12601
|
-
code: schema.string(),
|
|
12602
|
-
tabId: schema.number().optional()
|
|
12603
|
-
},
|
|
12604
|
-
async execute({ code, tabId }, ctx2) {
|
|
12605
|
-
const data = await brokerRequest("tool", { tool: "execute_script", args: { code, tabId } });
|
|
12606
|
-
return toolResultText(data, "Execute failed");
|
|
12607
|
-
}
|
|
12608
|
-
}),
|
|
12609
12598
|
browser_query: tool({
|
|
12610
|
-
description: "Read data from the page using selectors (
|
|
12599
|
+
description: "Read data from the page using selectors, optional wait, or page_text extraction (shadow DOM + same-origin iframes).",
|
|
12611
12600
|
args: {
|
|
12612
|
-
selector: schema.string(),
|
|
12601
|
+
selector: schema.string().optional(),
|
|
12613
12602
|
mode: schema.string().optional(),
|
|
12614
12603
|
attribute: schema.string().optional(),
|
|
12615
12604
|
property: schema.string().optional(),
|
|
12616
12605
|
index: schema.number().optional(),
|
|
12617
12606
|
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
12607
|
timeoutMs: schema.number().optional(),
|
|
12633
12608
|
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
12609
|
pattern: schema.string().optional(),
|
|
12649
12610
|
flags: schema.string().optional(),
|
|
12650
|
-
limit: schema.number().optional(),
|
|
12651
12611
|
tabId: schema.number().optional()
|
|
12652
12612
|
},
|
|
12653
|
-
async execute({ mode,
|
|
12613
|
+
async execute({ selector, mode, attribute, property, index, limit, timeoutMs, pollMs, pattern, flags, tabId }, ctx2) {
|
|
12654
12614
|
const data = await brokerRequest("tool", {
|
|
12655
|
-
tool: "
|
|
12656
|
-
args: { mode,
|
|
12615
|
+
tool: "query",
|
|
12616
|
+
args: { selector, mode, attribute, property, index, limit, timeoutMs, pollMs, pattern, flags, tabId }
|
|
12657
12617
|
});
|
|
12658
|
-
return toolResultText(data, "
|
|
12659
|
-
}
|
|
12660
|
-
}),
|
|
12661
|
-
browser_claim_tab: tool({
|
|
12662
|
-
description: "Claim a tab for this OpenCode session (per-tab ownership).",
|
|
12663
|
-
args: {
|
|
12664
|
-
tabId: schema.number(),
|
|
12665
|
-
force: schema.boolean().optional()
|
|
12666
|
-
},
|
|
12667
|
-
async execute({ tabId, force }, ctx2) {
|
|
12668
|
-
const data = await brokerRequest("claim_tab", { tabId, force });
|
|
12669
|
-
return JSON.stringify(data);
|
|
12670
|
-
}
|
|
12671
|
-
}),
|
|
12672
|
-
browser_release_tab: tool({
|
|
12673
|
-
description: "Release a previously claimed tab.",
|
|
12674
|
-
args: {
|
|
12675
|
-
tabId: schema.number()
|
|
12676
|
-
},
|
|
12677
|
-
async execute({ tabId }, ctx2) {
|
|
12678
|
-
const data = await brokerRequest("release_tab", { tabId });
|
|
12679
|
-
return JSON.stringify(data);
|
|
12680
|
-
}
|
|
12681
|
-
}),
|
|
12682
|
-
browser_list_claims: tool({
|
|
12683
|
-
description: "List current tab ownership claims.",
|
|
12684
|
-
args: {},
|
|
12685
|
-
async execute(args, ctx2) {
|
|
12686
|
-
const data = await brokerRequest("list_claims", {});
|
|
12687
|
-
return JSON.stringify(data);
|
|
12618
|
+
return toolResultText(data, "Query failed");
|
|
12688
12619
|
}
|
|
12689
12620
|
})
|
|
12690
12621
|
}
|
package/extension/background.js
CHANGED
|
@@ -105,10 +105,7 @@ async function executeTool(toolName, args) {
|
|
|
105
105
|
type: toolType,
|
|
106
106
|
screenshot: toolScreenshot,
|
|
107
107
|
snapshot: toolSnapshot,
|
|
108
|
-
extract: toolExtract,
|
|
109
108
|
query: toolQuery,
|
|
110
|
-
wait_for: toolWaitFor,
|
|
111
|
-
execute_script: toolExecuteScript,
|
|
112
109
|
scroll: toolScroll,
|
|
113
110
|
wait: toolWait,
|
|
114
111
|
}
|
|
@@ -128,276 +125,408 @@ async function getTabById(tabId) {
|
|
|
128
125
|
return tabId ? await chrome.tabs.get(tabId) : await getActiveTab()
|
|
129
126
|
}
|
|
130
127
|
|
|
131
|
-
async function
|
|
132
|
-
const
|
|
133
|
-
|
|
128
|
+
async function runInPage(tabId, command, args) {
|
|
129
|
+
const result = await chrome.scripting.executeScript({
|
|
130
|
+
target: { tabId },
|
|
131
|
+
func: pageOps,
|
|
132
|
+
args: [command, args || {}],
|
|
133
|
+
world: "ISOLATED",
|
|
134
|
+
})
|
|
135
|
+
return result[0]?.result
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
async function
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
await chrome.tabs.update(tab.id, { url })
|
|
138
|
+
async function pageOps(command, args) {
|
|
139
|
+
const options = args || {}
|
|
140
|
+
const MAX_DEPTH = 6
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
function safeString(value) {
|
|
143
|
+
return typeof value === "string" ? value : ""
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeSelectorList(selector) {
|
|
147
|
+
if (Array.isArray(selector)) {
|
|
148
|
+
return selector.map((s) => safeString(s).trim()).filter(Boolean)
|
|
147
149
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
if (typeof selector !== "string") return []
|
|
151
|
+
const parts = selector
|
|
152
|
+
.split(",")
|
|
153
|
+
.map((s) => s.trim())
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
return parts.length ? parts : [selector.trim()].filter(Boolean)
|
|
156
|
+
}
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
function isVisible(el) {
|
|
159
|
+
if (!el) return false
|
|
160
|
+
const rect = el.getBoundingClientRect()
|
|
161
|
+
if (rect.width <= 0 || rect.height <= 0) return false
|
|
162
|
+
const style = window.getComputedStyle(el)
|
|
163
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
157
166
|
|
|
158
|
-
function
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.split(",")
|
|
162
|
-
.map((s) => s.trim())
|
|
163
|
-
.filter(Boolean)
|
|
164
|
-
return parts.length ? parts : [selector.trim()].filter(Boolean)
|
|
165
|
-
}
|
|
167
|
+
function deepQuerySelectorAll(sel, rootDoc) {
|
|
168
|
+
const out = []
|
|
169
|
+
const seen = new Set()
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
function addAll(nodeList) {
|
|
172
|
+
for (const el of nodeList) {
|
|
173
|
+
if (!el || seen.has(el)) continue
|
|
174
|
+
seen.add(el)
|
|
175
|
+
out.push(el)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
170
178
|
|
|
171
|
-
|
|
179
|
+
function walkRoot(root, depth) {
|
|
180
|
+
if (!root || depth > MAX_DEPTH) return
|
|
181
|
+
try {
|
|
182
|
+
addAll(root.querySelectorAll(sel))
|
|
183
|
+
} catch {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
172
186
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
187
|
+
const tree = root.querySelectorAll ? root.querySelectorAll("*") : []
|
|
188
|
+
for (const el of tree) {
|
|
189
|
+
if (el.shadowRoot) {
|
|
190
|
+
walkRoot(el.shadowRoot, depth + 1)
|
|
191
|
+
}
|
|
178
192
|
}
|
|
179
193
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return true
|
|
194
|
+
const frames = root.querySelectorAll ? root.querySelectorAll("iframe") : []
|
|
195
|
+
for (const frame of frames) {
|
|
196
|
+
try {
|
|
197
|
+
const doc = frame.contentDocument
|
|
198
|
+
if (doc) walkRoot(doc, depth + 1)
|
|
199
|
+
} catch {}
|
|
187
200
|
}
|
|
201
|
+
}
|
|
188
202
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
203
|
+
walkRoot(rootDoc || document, 0)
|
|
204
|
+
return out
|
|
205
|
+
}
|
|
192
206
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
function resolveMatches(selectors, index) {
|
|
208
|
+
for (const sel of selectors) {
|
|
209
|
+
const s = safeString(sel)
|
|
210
|
+
if (!s) continue
|
|
211
|
+
const matches = deepQuerySelectorAll(s, document)
|
|
212
|
+
if (!matches.length) continue
|
|
213
|
+
const visible = matches.filter(isVisible)
|
|
214
|
+
const chosen = visible[index] || matches[index]
|
|
215
|
+
return { selectorUsed: s, matches, chosen }
|
|
216
|
+
}
|
|
217
|
+
return { selectorUsed: selectors[0] || "", matches: [], chosen: null }
|
|
218
|
+
}
|
|
200
219
|
|
|
201
|
-
|
|
202
|
-
|
|
220
|
+
function clickElement(el) {
|
|
221
|
+
try {
|
|
222
|
+
el.scrollIntoView({ block: "center", inline: "center" })
|
|
223
|
+
} catch {}
|
|
203
224
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return
|
|
209
|
-
}
|
|
225
|
+
const rect = el.getBoundingClientRect()
|
|
226
|
+
const x = Math.min(Math.max(rect.left + rect.width / 2, 0), window.innerWidth - 1)
|
|
227
|
+
const y = Math.min(Math.max(rect.top + rect.height / 2, 0), window.innerHeight - 1)
|
|
228
|
+
const opts = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y }
|
|
210
229
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
230
|
+
try {
|
|
231
|
+
el.dispatchEvent(new MouseEvent("mouseover", opts))
|
|
232
|
+
el.dispatchEvent(new MouseEvent("mousemove", opts))
|
|
233
|
+
el.dispatchEvent(new MouseEvent("mousedown", opts))
|
|
234
|
+
el.dispatchEvent(new MouseEvent("mouseup", opts))
|
|
235
|
+
el.dispatchEvent(new MouseEvent("click", opts))
|
|
236
|
+
} catch {}
|
|
217
237
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
238
|
+
try {
|
|
239
|
+
el.click()
|
|
240
|
+
} catch {}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function setNativeValue(el, value) {
|
|
244
|
+
const tag = el.tagName
|
|
245
|
+
if (tag === "INPUT" || tag === "TEXTAREA") {
|
|
246
|
+
const proto = tag === "INPUT" ? window.HTMLInputElement.prototype : window.HTMLTextAreaElement.prototype
|
|
247
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set
|
|
248
|
+
if (setter) setter.call(el, value)
|
|
249
|
+
else el.value = value
|
|
250
|
+
return true
|
|
251
|
+
}
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getInputValues() {
|
|
256
|
+
const out = []
|
|
257
|
+
const nodes = document.querySelectorAll("input, textarea")
|
|
258
|
+
nodes.forEach((el) => {
|
|
259
|
+
try {
|
|
260
|
+
const name = el.getAttribute("aria-label") || el.getAttribute("name") || el.id || el.className || el.tagName
|
|
261
|
+
const value = el.value
|
|
262
|
+
if (value != null && String(value).trim()) out.push(`${name}: ${value}`)
|
|
263
|
+
} catch {}
|
|
264
|
+
})
|
|
265
|
+
return out.join("\n")
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getPseudoText() {
|
|
269
|
+
const out = []
|
|
270
|
+
const elements = Array.from(document.querySelectorAll("*"))
|
|
271
|
+
for (let i = 0; i < elements.length && out.length < 2000; i++) {
|
|
272
|
+
const el = elements[i]
|
|
273
|
+
try {
|
|
274
|
+
const style = window.getComputedStyle(el)
|
|
275
|
+
if (style.display === "none" || style.visibility === "hidden") continue
|
|
276
|
+
const before = window.getComputedStyle(el, "::before").content
|
|
277
|
+
const after = window.getComputedStyle(el, "::after").content
|
|
278
|
+
const pushContent = (content) => {
|
|
279
|
+
if (!content) return
|
|
280
|
+
const c = String(content)
|
|
281
|
+
if (!c || c === "none" || c === "normal") return
|
|
282
|
+
const unquoted = c.replace(/^"|"$/g, "").replace(/^'|'$/g, "")
|
|
283
|
+
if (unquoted && unquoted !== "none" && unquoted !== "normal") out.push(unquoted)
|
|
228
284
|
}
|
|
285
|
+
pushContent(before)
|
|
286
|
+
pushContent(after)
|
|
287
|
+
} catch {}
|
|
288
|
+
}
|
|
289
|
+
return out.join("\n")
|
|
290
|
+
}
|
|
229
291
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
292
|
+
function buildMatches(text, pattern, flags) {
|
|
293
|
+
if (!pattern) return []
|
|
294
|
+
try {
|
|
295
|
+
const re = new RegExp(pattern, flags || "")
|
|
296
|
+
const found = []
|
|
297
|
+
let m
|
|
298
|
+
while ((m = re.exec(text)) && found.length < 50) {
|
|
299
|
+
found.push(m[0])
|
|
300
|
+
if (!re.global) break
|
|
301
|
+
}
|
|
302
|
+
return found
|
|
303
|
+
} catch {
|
|
304
|
+
return []
|
|
305
|
+
}
|
|
306
|
+
}
|
|
233
307
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
308
|
+
function getPageText(limit, pattern, flags) {
|
|
309
|
+
const parts = []
|
|
310
|
+
const bodyText = safeString(document.body?.innerText || "")
|
|
311
|
+
if (bodyText.trim()) parts.push(bodyText)
|
|
312
|
+
const inputValues = getInputValues()
|
|
313
|
+
if (inputValues) parts.push(inputValues)
|
|
314
|
+
const pseudo = getPseudoText()
|
|
315
|
+
if (pseudo) parts.push(pseudo)
|
|
316
|
+
const text = parts.filter(Boolean).join("\n\n").slice(0, Math.max(0, limit))
|
|
317
|
+
return {
|
|
318
|
+
url: location.href,
|
|
319
|
+
title: document.title,
|
|
320
|
+
text,
|
|
321
|
+
matches: buildMatches(text, pattern, flags),
|
|
322
|
+
}
|
|
323
|
+
}
|
|
238
324
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
325
|
+
const mode = typeof options.mode === "string" && options.mode ? options.mode : "text"
|
|
326
|
+
const selectors = normalizeSelectorList(options.selector)
|
|
327
|
+
const index = Number.isFinite(options.index) ? options.index : 0
|
|
328
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 0
|
|
329
|
+
const pollMs = Number.isFinite(options.pollMs) ? options.pollMs : 200
|
|
330
|
+
const limit = Number.isFinite(options.limit) ? options.limit : mode === "page_text" ? 20000 : 50
|
|
331
|
+
const pattern = typeof options.pattern === "string" ? options.pattern : null
|
|
332
|
+
const flags = typeof options.flags === "string" ? options.flags : "i"
|
|
333
|
+
|
|
334
|
+
if (command === "click") {
|
|
335
|
+
const match = resolveMatches(selectors, index)
|
|
336
|
+
if (!match.chosen) {
|
|
337
|
+
return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
|
|
338
|
+
}
|
|
339
|
+
clickElement(match.chosen)
|
|
340
|
+
return { ok: true, selectorUsed: match.selectorUsed }
|
|
341
|
+
}
|
|
242
342
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
} catch {}
|
|
343
|
+
if (command === "type") {
|
|
344
|
+
const text = options.text
|
|
345
|
+
const shouldClear = !!options.clear
|
|
346
|
+
const match = resolveMatches(selectors, index)
|
|
347
|
+
if (!match.chosen) {
|
|
348
|
+
return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
|
|
349
|
+
}
|
|
251
350
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
351
|
+
try {
|
|
352
|
+
match.chosen.scrollIntoView({ block: "center", inline: "center" })
|
|
353
|
+
} catch {}
|
|
256
354
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
355
|
+
try {
|
|
356
|
+
match.chosen.focus()
|
|
357
|
+
} catch {}
|
|
260
358
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
359
|
+
const tag = match.chosen.tagName
|
|
360
|
+
const isTextInput = tag === "INPUT" || tag === "TEXTAREA"
|
|
361
|
+
|
|
362
|
+
if (isTextInput) {
|
|
363
|
+
if (shouldClear) setNativeValue(match.chosen, "")
|
|
364
|
+
setNativeValue(match.chosen, (match.chosen.value || "") + text)
|
|
365
|
+
match.chosen.dispatchEvent(new Event("input", { bubbles: true }))
|
|
366
|
+
match.chosen.dispatchEvent(new Event("change", { bubbles: true }))
|
|
367
|
+
return { ok: true, selectorUsed: match.selectorUsed }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (match.chosen.isContentEditable) {
|
|
371
|
+
if (shouldClear) match.chosen.textContent = ""
|
|
372
|
+
try {
|
|
373
|
+
document.execCommand("insertText", false, text)
|
|
374
|
+
} catch {
|
|
375
|
+
match.chosen.textContent = (match.chosen.textContent || "") + text
|
|
268
376
|
}
|
|
377
|
+
match.chosen.dispatchEvent(new Event("input", { bubbles: true }))
|
|
378
|
+
return { ok: true, selectorUsed: match.selectorUsed }
|
|
379
|
+
}
|
|
269
380
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
args: [selectorList, index],
|
|
273
|
-
world: "ISOLATED",
|
|
274
|
-
})
|
|
381
|
+
return { ok: false, error: `Element is not typable: ${match.selectorUsed} (${tag.toLowerCase()})` }
|
|
382
|
+
}
|
|
275
383
|
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
384
|
+
if (command === "scroll") {
|
|
385
|
+
const scrollX = Number.isFinite(options.x) ? options.x : 0
|
|
386
|
+
const scrollY = Number.isFinite(options.y) ? options.y : 0
|
|
387
|
+
if (selectors.length) {
|
|
388
|
+
const match = resolveMatches(selectors, index)
|
|
389
|
+
if (!match.chosen) {
|
|
390
|
+
return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
match.chosen.scrollIntoView({ behavior: "smooth", block: "center" })
|
|
394
|
+
} catch {}
|
|
395
|
+
return { ok: true, selectorUsed: match.selectorUsed }
|
|
396
|
+
}
|
|
397
|
+
window.scrollBy(scrollX, scrollY)
|
|
398
|
+
return { ok: true }
|
|
399
|
+
}
|
|
280
400
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
401
|
+
if (command === "query") {
|
|
402
|
+
if (mode === "page_text") {
|
|
403
|
+
if (selectors.length && timeoutMs > 0) {
|
|
404
|
+
const start = Date.now()
|
|
405
|
+
while (Date.now() - start < timeoutMs) {
|
|
406
|
+
const match = resolveMatches(selectors, index)
|
|
407
|
+
if (match.matches.length) break
|
|
408
|
+
await new Promise((r) => setTimeout(r, pollMs))
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return { ok: true, value: getPageText(limit, pattern, flags) }
|
|
412
|
+
}
|
|
285
413
|
|
|
286
|
-
|
|
414
|
+
if (!selectors.length) {
|
|
415
|
+
return { ok: false, error: "Selector is required" }
|
|
416
|
+
}
|
|
287
417
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (
|
|
295
|
-
|
|
296
|
-
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false
|
|
297
|
-
return true
|
|
418
|
+
let match = resolveMatches(selectors, index)
|
|
419
|
+
if (timeoutMs > 0) {
|
|
420
|
+
const start = Date.now()
|
|
421
|
+
while (Date.now() - start < timeoutMs) {
|
|
422
|
+
match = resolveMatches(selectors, index)
|
|
423
|
+
if (mode === "exists" && match.matches.length) break
|
|
424
|
+
if (mode !== "exists" && match.chosen) break
|
|
425
|
+
await new Promise((r) => setTimeout(r, pollMs))
|
|
298
426
|
}
|
|
427
|
+
}
|
|
299
428
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
429
|
+
if (mode === "exists") {
|
|
430
|
+
return {
|
|
431
|
+
ok: true,
|
|
432
|
+
selectorUsed: match.selectorUsed,
|
|
433
|
+
value: { exists: match.matches.length > 0, count: match.matches.length },
|
|
434
|
+
}
|
|
435
|
+
}
|
|
303
436
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
seen.add(el)
|
|
308
|
-
out.push(el)
|
|
309
|
-
}
|
|
310
|
-
}
|
|
437
|
+
if (!match.chosen) {
|
|
438
|
+
return { ok: false, error: `No matches for selectors: ${selectors.join(", ")}` }
|
|
439
|
+
}
|
|
311
440
|
|
|
312
|
-
|
|
313
|
-
|
|
441
|
+
if (mode === "text") {
|
|
442
|
+
const text = (match.chosen.innerText || match.chosen.textContent || "").trim()
|
|
443
|
+
return { ok: true, selectorUsed: match.selectorUsed, value: text }
|
|
444
|
+
}
|
|
314
445
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
446
|
+
if (mode === "value") {
|
|
447
|
+
const value = match.chosen.value
|
|
448
|
+
return { ok: true, selectorUsed: match.selectorUsed, value: typeof value === "string" ? value : String(value ?? "") }
|
|
449
|
+
}
|
|
320
450
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
}
|
|
451
|
+
if (mode === "attribute") {
|
|
452
|
+
const value = options.attribute ? match.chosen.getAttribute(options.attribute) : null
|
|
453
|
+
return { ok: true, selectorUsed: match.selectorUsed, value }
|
|
454
|
+
}
|
|
327
455
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (doc) walkRoot(doc, depth + 1)
|
|
333
|
-
} catch {}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
456
|
+
if (mode === "property") {
|
|
457
|
+
if (!options.property) return { ok: false, error: "property is required" }
|
|
458
|
+
return { ok: true, selectorUsed: match.selectorUsed, value: match.chosen[options.property] }
|
|
459
|
+
}
|
|
336
460
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
461
|
+
if (mode === "html") {
|
|
462
|
+
return { ok: true, selectorUsed: match.selectorUsed, value: match.chosen.outerHTML }
|
|
463
|
+
}
|
|
340
464
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
465
|
+
if (mode === "list") {
|
|
466
|
+
const maxItems = Math.min(Math.max(1, limit), 200)
|
|
467
|
+
const items = match.matches.slice(0, maxItems).map((el) => ({
|
|
468
|
+
text: (el.innerText || el.textContent || "").trim().slice(0, 200),
|
|
469
|
+
tag: (el.tagName || "").toLowerCase(),
|
|
470
|
+
ariaLabel: el.getAttribute ? el.getAttribute("aria-label") : null,
|
|
471
|
+
}))
|
|
472
|
+
return {
|
|
473
|
+
ok: true,
|
|
474
|
+
selectorUsed: match.selectorUsed,
|
|
475
|
+
value: { items, count: match.matches.length },
|
|
351
476
|
}
|
|
477
|
+
}
|
|
352
478
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const matches = deepQuerySelectorAll(sel, document)
|
|
356
|
-
const visible = matches.filter(isVisible)
|
|
357
|
-
const el = visible[index] || matches[index]
|
|
358
|
-
if (!el) continue
|
|
479
|
+
return { ok: false, error: `Unknown mode: ${mode}` }
|
|
480
|
+
}
|
|
359
481
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
} catch {}
|
|
482
|
+
return { ok: false, error: `Unknown command: ${String(command)}` }
|
|
483
|
+
}
|
|
363
484
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
485
|
+
async function toolGetActiveTab() {
|
|
486
|
+
const tab = await getActiveTab()
|
|
487
|
+
return { tabId: tab.id, content: { tabId: tab.id, url: tab.url, title: tab.title } }
|
|
488
|
+
}
|
|
367
489
|
|
|
368
|
-
|
|
369
|
-
|
|
490
|
+
async function toolNavigate({ url, tabId }) {
|
|
491
|
+
if (!url) throw new Error("URL is required")
|
|
492
|
+
const tab = await getTabById(tabId)
|
|
493
|
+
await chrome.tabs.update(tab.id, { url })
|
|
370
494
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
495
|
+
await new Promise((resolve) => {
|
|
496
|
+
const listener = (updatedTabId, info) => {
|
|
497
|
+
if (updatedTabId === tab.id && info.status === "complete") {
|
|
498
|
+
chrome.tabs.onUpdated.removeListener(listener)
|
|
499
|
+
resolve()
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
chrome.tabs.onUpdated.addListener(listener)
|
|
503
|
+
setTimeout(() => {
|
|
504
|
+
chrome.tabs.onUpdated.removeListener(listener)
|
|
505
|
+
resolve()
|
|
506
|
+
}, 30000)
|
|
507
|
+
})
|
|
378
508
|
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
}
|
|
509
|
+
return { tabId: tab.id, content: `Navigated to ${url}` }
|
|
510
|
+
}
|
|
389
511
|
|
|
390
|
-
|
|
391
|
-
|
|
512
|
+
async function toolClick({ selector, tabId, index = 0 }) {
|
|
513
|
+
if (!selector) throw new Error("Selector is required")
|
|
514
|
+
const tab = await getTabById(tabId)
|
|
392
515
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
516
|
+
const result = await runInPage(tab.id, "click", { selector, index })
|
|
517
|
+
if (!result?.ok) throw new Error(result?.error || "Click failed")
|
|
518
|
+
const used = result.selectorUsed || selector
|
|
519
|
+
return { tabId: tab.id, content: `Clicked ${used}` }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function toolType({ selector, text, tabId, clear = false, index = 0 }) {
|
|
523
|
+
if (!selector) throw new Error("Selector is required")
|
|
524
|
+
if (text === undefined) throw new Error("Text is required")
|
|
525
|
+
const tab = await getTabById(tabId)
|
|
398
526
|
|
|
399
|
-
|
|
400
|
-
|
|
527
|
+
const result = await runInPage(tab.id, "type", { selector, text, clear, index })
|
|
528
|
+
if (!result?.ok) throw new Error(result?.error || "Type failed")
|
|
529
|
+
const used = result.selectorUsed || selector
|
|
401
530
|
return { tabId: tab.id, content: `Typed "${text}" into ${used}` }
|
|
402
531
|
}
|
|
403
532
|
|
|
@@ -554,377 +683,57 @@ async function toolSnapshot({ tabId }) {
|
|
|
554
683
|
return { tabId: tab.id, content: JSON.stringify(result[0]?.result, null, 2) }
|
|
555
684
|
}
|
|
556
685
|
|
|
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],
|
|
641
|
-
})
|
|
642
|
-
|
|
643
|
-
return { tabId: tab.id, content: JSON.stringify(result[0]?.result, null, 2) }
|
|
644
|
-
}
|
|
645
|
-
|
|
646
686
|
async function toolGetTabs() {
|
|
647
687
|
const tabs = await chrome.tabs.query({})
|
|
648
688
|
const out = tabs.map((t) => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId }))
|
|
649
689
|
return { content: JSON.stringify(out, null, 2) }
|
|
650
690
|
}
|
|
651
691
|
|
|
652
|
-
async function toolQuery({
|
|
653
|
-
|
|
692
|
+
async function toolQuery({
|
|
693
|
+
tabId,
|
|
694
|
+
selector,
|
|
695
|
+
mode = "text",
|
|
696
|
+
attribute,
|
|
697
|
+
property,
|
|
698
|
+
limit,
|
|
699
|
+
index = 0,
|
|
700
|
+
timeoutMs,
|
|
701
|
+
pollMs,
|
|
702
|
+
pattern,
|
|
703
|
+
flags,
|
|
704
|
+
}) {
|
|
705
|
+
if (!selector && mode !== "page_text") throw new Error("selector is required")
|
|
654
706
|
const tab = await getTabById(tabId)
|
|
655
707
|
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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",
|
|
708
|
+
const result = await runInPage(tab.id, "query", {
|
|
709
|
+
selector,
|
|
710
|
+
mode,
|
|
711
|
+
attribute,
|
|
712
|
+
property,
|
|
713
|
+
limit,
|
|
714
|
+
index,
|
|
715
|
+
timeoutMs,
|
|
716
|
+
pollMs,
|
|
717
|
+
pattern,
|
|
718
|
+
flags,
|
|
767
719
|
})
|
|
768
720
|
|
|
769
|
-
|
|
770
|
-
if (!r?.ok) throw new Error(r?.error || "Query failed")
|
|
721
|
+
if (!result?.ok) throw new Error(result?.error || "Query failed")
|
|
771
722
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
return { tabId: tab.id, content: JSON.stringify(r, null, 2) }
|
|
723
|
+
if (mode === "list" || mode === "property" || mode === "exists" || mode === "page_text") {
|
|
724
|
+
return { tabId: tab.id, content: JSON.stringify(result, null, 2) }
|
|
775
725
|
}
|
|
776
726
|
|
|
777
|
-
return { tabId: tab.id, content: typeof
|
|
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.
|
|
857
|
-
async function toolExecuteScript({ code, tabId }) {
|
|
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
|
-
|
|
869
|
-
const tab = await getTabById(tabId)
|
|
870
|
-
const result = await chrome.scripting.executeScript({
|
|
871
|
-
target: { tabId: tab.id },
|
|
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],
|
|
902
|
-
})
|
|
903
|
-
|
|
904
|
-
return { tabId: tab.id, content: JSON.stringify(result[0]?.result) }
|
|
727
|
+
return { tabId: tab.id, content: typeof result.value === "string" ? result.value : JSON.stringify(result.value) }
|
|
905
728
|
}
|
|
906
729
|
|
|
907
730
|
async function toolScroll({ x = 0, y = 0, selector, tabId }) {
|
|
908
731
|
const tab = await getTabById(tabId)
|
|
909
|
-
const sel = selector || null
|
|
910
|
-
|
|
911
|
-
await chrome.scripting.executeScript({
|
|
912
|
-
target: { tabId: tab.id },
|
|
913
|
-
func: (scrollX, scrollY, sel) => {
|
|
914
|
-
if (sel) {
|
|
915
|
-
const el = document.querySelector(sel)
|
|
916
|
-
if (el) {
|
|
917
|
-
el.scrollIntoView({ behavior: "smooth", block: "center" })
|
|
918
|
-
return
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
window.scrollBy(scrollX, scrollY)
|
|
922
|
-
},
|
|
923
|
-
args: [x, y, sel],
|
|
924
|
-
world: "ISOLATED",
|
|
925
|
-
})
|
|
926
732
|
|
|
927
|
-
|
|
733
|
+
const result = await runInPage(tab.id, "scroll", { x, y, selector })
|
|
734
|
+
if (!result?.ok) throw new Error(result?.error || "Scroll failed")
|
|
735
|
+
const target = result.selectorUsed ? `to ${result.selectorUsed}` : `by (${x}, ${y})`
|
|
736
|
+
return { tabId: tab.id, content: `Scrolled ${target}` }
|
|
928
737
|
}
|
|
929
738
|
|
|
930
739
|
async function toolWait({ ms = 1000, tabId }) {
|
package/extension/manifest.json
CHANGED