@different-ai/opencode-browser 4.5.1 → 4.6.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 +32 -2
- package/bin/broker.cjs +1 -1
- package/dist/plugin.js +335 -90
- package/extension/background.js +594 -19
- package/extension/manifest.json +4 -2
- package/package.json +13 -9
- package/.opencode/skill/github-release/SKILL.md +0 -12
package/extension/background.js
CHANGED
|
@@ -1,19 +1,200 @@
|
|
|
1
1
|
const NATIVE_HOST_NAME = "com.opencode.browser_automation"
|
|
2
2
|
const KEEPALIVE_ALARM = "keepalive"
|
|
3
|
+
const PERMISSION_HINT = "Click the OpenCode Browser extension icon and approve requested permissions."
|
|
4
|
+
const OPTIONAL_RUNTIME_PERMISSIONS = ["nativeMessaging", "downloads", "debugger"]
|
|
5
|
+
const OPTIONAL_RUNTIME_ORIGINS = ["<all_urls>"]
|
|
6
|
+
|
|
7
|
+
const runtimeManifest = chrome.runtime.getManifest()
|
|
8
|
+
const declaredOptionalPermissions = new Set(runtimeManifest.optional_permissions || [])
|
|
9
|
+
const declaredOptionalOrigins = new Set(runtimeManifest.optional_host_permissions || [])
|
|
3
10
|
|
|
4
11
|
let port = null
|
|
5
12
|
let isConnected = false
|
|
6
13
|
let connectionAttempts = 0
|
|
14
|
+
let nativePermissionHintLogged = false
|
|
15
|
+
|
|
16
|
+
// Debugger state management for console/error capture
|
|
17
|
+
const debuggerState = new Map()
|
|
18
|
+
const MAX_LOG_ENTRIES = 1000
|
|
19
|
+
|
|
20
|
+
async function hasPermissions(query) {
|
|
21
|
+
if (!chrome.permissions?.contains) return true
|
|
22
|
+
try {
|
|
23
|
+
return await chrome.permissions.contains(query)
|
|
24
|
+
} catch {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function hasNativeMessagingPermission() {
|
|
30
|
+
return await hasPermissions({ permissions: ["nativeMessaging"] })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function hasDebuggerPermission() {
|
|
34
|
+
return await hasPermissions({ permissions: ["debugger"] })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function hasDownloadsPermission() {
|
|
38
|
+
return await hasPermissions({ permissions: ["downloads"] })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function hasHostAccessPermission() {
|
|
42
|
+
return await hasPermissions({ origins: ["<all_urls>"] })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function requestOptionalPermissionsFromClick() {
|
|
46
|
+
if (!chrome.permissions?.contains || !chrome.permissions?.request) {
|
|
47
|
+
return { granted: true, requested: false, permissions: [], origins: [] }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const permissions = []
|
|
51
|
+
for (const permission of OPTIONAL_RUNTIME_PERMISSIONS) {
|
|
52
|
+
if (!declaredOptionalPermissions.has(permission)) continue
|
|
53
|
+
const granted = await hasPermissions({ permissions: [permission] })
|
|
54
|
+
if (!granted) permissions.push(permission)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const origins = []
|
|
58
|
+
for (const origin of OPTIONAL_RUNTIME_ORIGINS) {
|
|
59
|
+
if (!declaredOptionalOrigins.has(origin)) continue
|
|
60
|
+
const granted = await hasPermissions({ origins: [origin] })
|
|
61
|
+
if (!granted) origins.push(origin)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!permissions.length && !origins.length) {
|
|
65
|
+
return { granted: true, requested: false, permissions, origins }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const granted = await chrome.permissions.request({ permissions, origins })
|
|
70
|
+
return { granted, requested: true, permissions, origins }
|
|
71
|
+
} catch (error) {
|
|
72
|
+
return {
|
|
73
|
+
granted: false,
|
|
74
|
+
requested: true,
|
|
75
|
+
permissions,
|
|
76
|
+
origins,
|
|
77
|
+
error: error?.message || String(error),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function ensureDebuggerAvailable() {
|
|
83
|
+
if (!chrome.debugger?.attach) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
reason: "Debugger API unavailable in this build.",
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const granted = await hasDebuggerPermission()
|
|
91
|
+
if (!granted) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
reason: `Debugger permission not granted. ${PERMISSION_HINT}`,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { ok: true }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function ensureDownloadsAvailable() {
|
|
102
|
+
if (!chrome.downloads) {
|
|
103
|
+
throw new Error(`Downloads API unavailable in this build. ${PERMISSION_HINT}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const granted = await hasDownloadsPermission()
|
|
107
|
+
if (!granted) {
|
|
108
|
+
throw new Error(`Downloads permission not granted. ${PERMISSION_HINT}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function ensureDebuggerAttached(tabId) {
|
|
113
|
+
const availability = await ensureDebuggerAvailable()
|
|
114
|
+
if (!availability.ok) {
|
|
115
|
+
return {
|
|
116
|
+
attached: false,
|
|
117
|
+
unavailableReason: availability.reason,
|
|
118
|
+
consoleMessages: [],
|
|
119
|
+
pageErrors: [],
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (debuggerState.has(tabId)) return debuggerState.get(tabId)
|
|
124
|
+
|
|
125
|
+
const state = { attached: false, consoleMessages: [], pageErrors: [] }
|
|
126
|
+
debuggerState.set(tabId, state)
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await chrome.debugger.attach({ tabId }, "1.3")
|
|
130
|
+
await chrome.debugger.sendCommand({ tabId }, "Runtime.enable")
|
|
131
|
+
state.attached = true
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.warn("[OpenCode] Failed to attach debugger:", e.message || e)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return state
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (chrome.debugger?.onEvent) {
|
|
140
|
+
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
141
|
+
const state = debuggerState.get(source.tabId)
|
|
142
|
+
if (!state) return
|
|
143
|
+
|
|
144
|
+
if (method === "Runtime.consoleAPICalled") {
|
|
145
|
+
if (state.consoleMessages.length >= MAX_LOG_ENTRIES) {
|
|
146
|
+
state.consoleMessages.shift()
|
|
147
|
+
}
|
|
148
|
+
state.consoleMessages.push({
|
|
149
|
+
type: params.type,
|
|
150
|
+
text: params.args.map((a) => a.value ?? a.description ?? "").join(" "),
|
|
151
|
+
timestamp: Date.now(),
|
|
152
|
+
source: params.stackTrace?.callFrames?.[0]?.url,
|
|
153
|
+
line: params.stackTrace?.callFrames?.[0]?.lineNumber,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (method === "Runtime.exceptionThrown") {
|
|
158
|
+
if (state.pageErrors.length >= MAX_LOG_ENTRIES) {
|
|
159
|
+
state.pageErrors.shift()
|
|
160
|
+
}
|
|
161
|
+
state.pageErrors.push({
|
|
162
|
+
message: params.exceptionDetails.text,
|
|
163
|
+
source: params.exceptionDetails.url,
|
|
164
|
+
line: params.exceptionDetails.lineNumber,
|
|
165
|
+
column: params.exceptionDetails.columnNumber,
|
|
166
|
+
stack: params.exceptionDetails.exception?.description,
|
|
167
|
+
timestamp: Date.now(),
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (chrome.debugger?.onDetach) {
|
|
174
|
+
chrome.debugger.onDetach.addListener((source) => {
|
|
175
|
+
if (debuggerState.has(source.tabId)) {
|
|
176
|
+
const state = debuggerState.get(source.tabId)
|
|
177
|
+
state.attached = false
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
183
|
+
if (debuggerState.has(tabId)) {
|
|
184
|
+
if (chrome.debugger?.detach) chrome.debugger.detach({ tabId }).catch(() => {})
|
|
185
|
+
debuggerState.delete(tabId)
|
|
186
|
+
}
|
|
187
|
+
})
|
|
7
188
|
|
|
8
189
|
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.25 })
|
|
9
190
|
|
|
10
191
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
11
192
|
if (alarm.name === KEEPALIVE_ALARM) {
|
|
12
|
-
if (!isConnected) connect()
|
|
193
|
+
if (!isConnected) connect().catch(() => {})
|
|
13
194
|
}
|
|
14
195
|
})
|
|
15
196
|
|
|
16
|
-
function connect() {
|
|
197
|
+
async function connect() {
|
|
17
198
|
if (port) {
|
|
18
199
|
try {
|
|
19
200
|
port.disconnect()
|
|
@@ -21,6 +202,19 @@ function connect() {
|
|
|
21
202
|
port = null
|
|
22
203
|
}
|
|
23
204
|
|
|
205
|
+
const nativeMessagingAllowed = await hasNativeMessagingPermission()
|
|
206
|
+
if (!nativeMessagingAllowed) {
|
|
207
|
+
isConnected = false
|
|
208
|
+
updateBadge(false)
|
|
209
|
+
if (!nativePermissionHintLogged) {
|
|
210
|
+
nativePermissionHintLogged = true
|
|
211
|
+
console.log(`[OpenCode] Native messaging permission not granted. ${PERMISSION_HINT}`)
|
|
212
|
+
}
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
nativePermissionHintLogged = false
|
|
217
|
+
|
|
24
218
|
try {
|
|
25
219
|
port = chrome.runtime.connectNative(NATIVE_HOST_NAME)
|
|
26
220
|
|
|
@@ -111,6 +305,12 @@ async function executeTool(toolName, args) {
|
|
|
111
305
|
query: toolQuery,
|
|
112
306
|
scroll: toolScroll,
|
|
113
307
|
wait: toolWait,
|
|
308
|
+
download: toolDownload,
|
|
309
|
+
list_downloads: toolListDownloads,
|
|
310
|
+
set_file_input: toolSetFileInput,
|
|
311
|
+
highlight: toolHighlight,
|
|
312
|
+
console: toolConsole,
|
|
313
|
+
errors: toolErrors,
|
|
114
314
|
}
|
|
115
315
|
|
|
116
316
|
const fn = tools[toolName]
|
|
@@ -129,13 +329,26 @@ async function getTabById(tabId) {
|
|
|
129
329
|
}
|
|
130
330
|
|
|
131
331
|
async function runInPage(tabId, command, args) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
332
|
+
const hasHostAccess = await hasHostAccessPermission()
|
|
333
|
+
if (!hasHostAccess) {
|
|
334
|
+
throw new Error(`Site access permission not granted. ${PERMISSION_HINT}`)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const result = await chrome.scripting.executeScript({
|
|
339
|
+
target: { tabId },
|
|
340
|
+
func: pageOps,
|
|
341
|
+
args: [command, args || {}],
|
|
342
|
+
world: "ISOLATED",
|
|
343
|
+
})
|
|
344
|
+
return result[0]?.result
|
|
345
|
+
} catch (error) {
|
|
346
|
+
const message = error?.message || String(error)
|
|
347
|
+
if (message.includes("Cannot access contents of the page")) {
|
|
348
|
+
throw new Error(`Site access permission not granted for this page. ${PERMISSION_HINT}`)
|
|
349
|
+
}
|
|
350
|
+
throw error
|
|
351
|
+
}
|
|
139
352
|
}
|
|
140
353
|
|
|
141
354
|
async function pageOps(command, args) {
|
|
@@ -609,6 +822,67 @@ async function pageOps(command, args) {
|
|
|
609
822
|
}
|
|
610
823
|
}
|
|
611
824
|
|
|
825
|
+
if (command === "set_file_input") {
|
|
826
|
+
const rawFiles = Array.isArray(options.files) ? options.files : options.files ? [options.files] : []
|
|
827
|
+
if (!rawFiles.length) return { ok: false, error: "files is required" }
|
|
828
|
+
|
|
829
|
+
const match = await resolveMatches(selectors, index, timeoutMs, pollMs)
|
|
830
|
+
if (!match.chosen) {
|
|
831
|
+
return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const tag = match.chosen.tagName
|
|
835
|
+
if (tag !== "INPUT" || match.chosen.type !== "file") {
|
|
836
|
+
return { ok: false, error: `Element is not a file input: ${match.selectorUsed} (${tag.toLowerCase()})` }
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function decodeBase64(value) {
|
|
840
|
+
const raw = safeString(value)
|
|
841
|
+
const b64 = raw.includes(",") ? raw.split(",").pop() : raw
|
|
842
|
+
const binary = atob(b64)
|
|
843
|
+
const bytes = new Uint8Array(binary.length)
|
|
844
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
|
845
|
+
return bytes
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const dt = new DataTransfer()
|
|
849
|
+
const names = []
|
|
850
|
+
|
|
851
|
+
for (const fileInfo of rawFiles) {
|
|
852
|
+
const name = safeString(fileInfo?.name) || "upload.bin"
|
|
853
|
+
const mimeType = safeString(fileInfo?.mimeType) || "application/octet-stream"
|
|
854
|
+
const base64 = safeString(fileInfo?.base64)
|
|
855
|
+
if (!base64) return { ok: false, error: "file.base64 is required" }
|
|
856
|
+
const bytes = decodeBase64(base64)
|
|
857
|
+
const file = new File([bytes], name, { type: mimeType, lastModified: Date.now() })
|
|
858
|
+
dt.items.add(file)
|
|
859
|
+
names.push(name)
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
try {
|
|
863
|
+
match.chosen.scrollIntoView({ block: "center", inline: "center" })
|
|
864
|
+
} catch {}
|
|
865
|
+
|
|
866
|
+
try {
|
|
867
|
+
match.chosen.focus()
|
|
868
|
+
} catch {}
|
|
869
|
+
|
|
870
|
+
try {
|
|
871
|
+
match.chosen.files = dt.files
|
|
872
|
+
} catch {
|
|
873
|
+
try {
|
|
874
|
+
Object.defineProperty(match.chosen, "files", { value: dt.files, writable: false })
|
|
875
|
+
} catch {
|
|
876
|
+
return { ok: false, error: "Failed to set file input" }
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
match.chosen.dispatchEvent(new Event("input", { bubbles: true }))
|
|
881
|
+
match.chosen.dispatchEvent(new Event("change", { bubbles: true }))
|
|
882
|
+
|
|
883
|
+
return { ok: true, selectorUsed: match.selectorUsed, count: dt.files.length, names }
|
|
884
|
+
}
|
|
885
|
+
|
|
612
886
|
if (command === "scroll") {
|
|
613
887
|
const scrollX = Number.isFinite(options.x) ? options.x : 0
|
|
614
888
|
const scrollY = Number.isFinite(options.y) ? options.y : 0
|
|
@@ -626,6 +900,73 @@ async function pageOps(command, args) {
|
|
|
626
900
|
return { ok: true }
|
|
627
901
|
}
|
|
628
902
|
|
|
903
|
+
if (command === "highlight") {
|
|
904
|
+
const duration = Number.isFinite(options.duration) ? options.duration : 3000
|
|
905
|
+
const color = typeof options.color === "string" ? options.color : "#ff0000"
|
|
906
|
+
const showInfo = !!options.showInfo
|
|
907
|
+
|
|
908
|
+
const match = await resolveMatches(selectors, index, timeoutMs, pollMs)
|
|
909
|
+
if (!match.chosen) {
|
|
910
|
+
return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const el = match.chosen
|
|
914
|
+
const rect = el.getBoundingClientRect()
|
|
915
|
+
|
|
916
|
+
// Remove any existing highlight overlay
|
|
917
|
+
const existing = document.getElementById("__opc_highlight_overlay")
|
|
918
|
+
if (existing) existing.remove()
|
|
919
|
+
|
|
920
|
+
// Create overlay
|
|
921
|
+
const overlay = document.createElement("div")
|
|
922
|
+
overlay.id = "__opc_highlight_overlay"
|
|
923
|
+
overlay.style.cssText = `
|
|
924
|
+
position: fixed;
|
|
925
|
+
top: ${rect.top}px;
|
|
926
|
+
left: ${rect.left}px;
|
|
927
|
+
width: ${rect.width}px;
|
|
928
|
+
height: ${rect.height}px;
|
|
929
|
+
border: 3px solid ${color};
|
|
930
|
+
box-shadow: 0 0 10px ${color};
|
|
931
|
+
pointer-events: none;
|
|
932
|
+
z-index: 2147483647;
|
|
933
|
+
transition: opacity 0.3s;
|
|
934
|
+
`
|
|
935
|
+
|
|
936
|
+
if (showInfo) {
|
|
937
|
+
const info = document.createElement("div")
|
|
938
|
+
info.style.cssText = `
|
|
939
|
+
position: absolute;
|
|
940
|
+
top: -25px;
|
|
941
|
+
left: 0;
|
|
942
|
+
background: ${color};
|
|
943
|
+
color: white;
|
|
944
|
+
padding: 2px 8px;
|
|
945
|
+
font-size: 12px;
|
|
946
|
+
font-family: monospace;
|
|
947
|
+
border-radius: 3px;
|
|
948
|
+
white-space: nowrap;
|
|
949
|
+
`
|
|
950
|
+
info.textContent = `${el.tagName.toLowerCase()}${el.id ? "#" + el.id : ""}`
|
|
951
|
+
overlay.appendChild(info)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
document.body.appendChild(overlay)
|
|
955
|
+
|
|
956
|
+
setTimeout(() => {
|
|
957
|
+
overlay.style.opacity = "0"
|
|
958
|
+
setTimeout(() => overlay.remove(), 300)
|
|
959
|
+
}, duration)
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
ok: true,
|
|
963
|
+
selectorUsed: match.selectorUsed,
|
|
964
|
+
highlighted: true,
|
|
965
|
+
tag: el.tagName,
|
|
966
|
+
id: el.id || null,
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
629
970
|
if (command === "query") {
|
|
630
971
|
if (mode === "page_text") {
|
|
631
972
|
if (selectors.length && timeoutMs > 0) {
|
|
@@ -986,16 +1327,250 @@ async function toolWait({ ms = 1000, tabId }) {
|
|
|
986
1327
|
return { tabId, content: `Waited ${ms}ms` }
|
|
987
1328
|
}
|
|
988
1329
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1330
|
+
function clampNumber(value, min, max, fallback) {
|
|
1331
|
+
const n = Number(value)
|
|
1332
|
+
if (!Number.isFinite(n)) return fallback
|
|
1333
|
+
return Math.min(Math.max(n, min), max)
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function normalizeDownloadTimeoutMs(value) {
|
|
1337
|
+
return clampNumber(value, 0, 60000, 60000)
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function waitForNextDownloadCreated(timeoutMs) {
|
|
1341
|
+
const timeout = normalizeDownloadTimeoutMs(timeoutMs)
|
|
1342
|
+
return new Promise((resolve, reject) => {
|
|
1343
|
+
const listener = (item) => {
|
|
1344
|
+
cleanup()
|
|
1345
|
+
resolve(item)
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const timer = timeout
|
|
1349
|
+
? setTimeout(() => {
|
|
1350
|
+
cleanup()
|
|
1351
|
+
reject(new Error("Timed out waiting for download to start"))
|
|
1352
|
+
}, timeout)
|
|
1353
|
+
: null
|
|
1354
|
+
|
|
1355
|
+
function cleanup() {
|
|
1356
|
+
chrome.downloads.onCreated.removeListener(listener)
|
|
1357
|
+
if (timer) clearTimeout(timer)
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
chrome.downloads.onCreated.addListener(listener)
|
|
998
1361
|
})
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
async function getDownloadById(downloadId) {
|
|
1365
|
+
const items = await chrome.downloads.search({ id: downloadId })
|
|
1366
|
+
return items && items.length ? items[0] : null
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function waitForDownloadCompletion(downloadId, timeoutMs) {
|
|
1370
|
+
const timeout = normalizeDownloadTimeoutMs(timeoutMs)
|
|
1371
|
+
const pollMs = 200
|
|
1372
|
+
const endAt = Date.now() + timeout
|
|
1373
|
+
|
|
1374
|
+
while (true) {
|
|
1375
|
+
const item = await getDownloadById(downloadId)
|
|
1376
|
+
if (item && (item.state === "complete" || item.state === "interrupted")) return item
|
|
1377
|
+
if (!timeout || Date.now() >= endAt) return item
|
|
1378
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs))
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
async function toolDownload({
|
|
1383
|
+
url,
|
|
1384
|
+
selector,
|
|
1385
|
+
filename,
|
|
1386
|
+
conflictAction,
|
|
1387
|
+
saveAs = false,
|
|
1388
|
+
wait = false,
|
|
1389
|
+
downloadTimeoutMs,
|
|
1390
|
+
tabId,
|
|
1391
|
+
index = 0,
|
|
1392
|
+
timeoutMs,
|
|
1393
|
+
pollMs,
|
|
1394
|
+
}) {
|
|
1395
|
+
const hasUrl = typeof url === "string" && url.trim()
|
|
1396
|
+
const hasSelector = typeof selector === "string" && selector.trim()
|
|
1397
|
+
|
|
1398
|
+
await ensureDownloadsAvailable()
|
|
1399
|
+
|
|
1400
|
+
if (!hasUrl && !hasSelector) throw new Error("url or selector is required")
|
|
1401
|
+
if (hasUrl && hasSelector) throw new Error("Provide either url or selector, not both")
|
|
1402
|
+
|
|
1403
|
+
let downloadId = null
|
|
1404
|
+
|
|
1405
|
+
if (hasUrl) {
|
|
1406
|
+
const options = { url: url.trim() }
|
|
1407
|
+
if (typeof filename === "string" && filename.trim()) options.filename = filename.trim()
|
|
1408
|
+
if (typeof conflictAction === "string" && conflictAction.trim()) options.conflictAction = conflictAction.trim()
|
|
1409
|
+
if (typeof saveAs === "boolean") options.saveAs = saveAs
|
|
1410
|
+
|
|
1411
|
+
downloadId = await chrome.downloads.download(options)
|
|
1412
|
+
} else {
|
|
1413
|
+
const tab = await getTabById(tabId)
|
|
1414
|
+
const created = waitForNextDownloadCreated(downloadTimeoutMs)
|
|
1415
|
+
const clicked = await runInPage(tab.id, "click", { selector, index, timeoutMs, pollMs })
|
|
1416
|
+
if (!clicked?.ok) throw new Error(clicked?.error || "Click failed")
|
|
1417
|
+
const createdItem = await created
|
|
1418
|
+
downloadId = createdItem?.id
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (!Number.isFinite(downloadId)) throw new Error("Download did not start")
|
|
1422
|
+
|
|
1423
|
+
if (!wait) {
|
|
1424
|
+
const item = await getDownloadById(downloadId)
|
|
1425
|
+
return { content: { downloadId, item } }
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const item = await waitForDownloadCompletion(downloadId, downloadTimeoutMs)
|
|
1429
|
+
return { content: { downloadId, item } }
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
async function toolListDownloads({ limit = 20, state } = {}) {
|
|
1433
|
+
await ensureDownloadsAvailable()
|
|
1434
|
+
|
|
1435
|
+
const limitValue = clampNumber(limit, 1, 200, 20)
|
|
1436
|
+
const query = { orderBy: ["-startTime"], limit: limitValue }
|
|
1437
|
+
if (typeof state === "string" && state.trim()) query.state = state.trim()
|
|
1438
|
+
|
|
1439
|
+
const downloads = await chrome.downloads.search(query)
|
|
1440
|
+
const out = downloads.map((d) => ({
|
|
1441
|
+
id: d.id,
|
|
1442
|
+
url: d.url,
|
|
1443
|
+
filename: d.filename,
|
|
1444
|
+
state: d.state,
|
|
1445
|
+
bytesReceived: d.bytesReceived,
|
|
1446
|
+
totalBytes: d.totalBytes,
|
|
1447
|
+
startTime: d.startTime,
|
|
1448
|
+
endTime: d.endTime,
|
|
1449
|
+
error: d.error,
|
|
1450
|
+
mime: d.mime,
|
|
1451
|
+
}))
|
|
1452
|
+
|
|
1453
|
+
return { content: JSON.stringify({ downloads: out }, null, 2) }
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
async function toolSetFileInput({ selector, tabId, index = 0, timeoutMs, pollMs, files }) {
|
|
1457
|
+
if (!selector) throw new Error("Selector is required")
|
|
1458
|
+
const tab = await getTabById(tabId)
|
|
1459
|
+
|
|
1460
|
+
const result = await runInPage(tab.id, "set_file_input", { selector, index, timeoutMs, pollMs, files })
|
|
1461
|
+
if (!result?.ok) throw new Error(result?.error || "Failed to set file input")
|
|
1462
|
+
const used = result.selectorUsed || selector
|
|
1463
|
+
return { tabId: tab.id, content: JSON.stringify({ selector: used, ...result }, null, 2) }
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
async function toolHighlight({ selector, tabId, index = 0, duration, color, showInfo, timeoutMs, pollMs }) {
|
|
1467
|
+
if (!selector) throw new Error("Selector is required")
|
|
1468
|
+
const tab = await getTabById(tabId)
|
|
1469
|
+
|
|
1470
|
+
const result = await runInPage(tab.id, "highlight", {
|
|
1471
|
+
selector,
|
|
1472
|
+
index,
|
|
1473
|
+
duration,
|
|
1474
|
+
color,
|
|
1475
|
+
showInfo,
|
|
1476
|
+
timeoutMs,
|
|
1477
|
+
pollMs,
|
|
1478
|
+
})
|
|
1479
|
+
if (!result?.ok) throw new Error(result?.error || "Highlight failed")
|
|
1480
|
+
return {
|
|
1481
|
+
tabId: tab.id,
|
|
1482
|
+
content: JSON.stringify({
|
|
1483
|
+
highlighted: true,
|
|
1484
|
+
tag: result.tag,
|
|
1485
|
+
id: result.id,
|
|
1486
|
+
selectorUsed: result.selectorUsed,
|
|
1487
|
+
}),
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
async function toolConsole({ tabId, clear = false, filter } = {}) {
|
|
1492
|
+
const tab = await getTabById(tabId)
|
|
1493
|
+
const state = await ensureDebuggerAttached(tab.id)
|
|
1494
|
+
|
|
1495
|
+
if (!state.attached) {
|
|
1496
|
+
return {
|
|
1497
|
+
tabId: tab.id,
|
|
1498
|
+
content: JSON.stringify({
|
|
1499
|
+
error: state.unavailableReason || "Debugger not attached. DevTools may be open or another debugger is active.",
|
|
1500
|
+
messages: [],
|
|
1501
|
+
}),
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
let messages = [...state.consoleMessages]
|
|
1506
|
+
|
|
1507
|
+
if (filter && typeof filter === "string") {
|
|
1508
|
+
const filterType = filter.toLowerCase()
|
|
1509
|
+
messages = messages.filter((m) => m.type === filterType)
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
if (clear) {
|
|
1513
|
+
state.consoleMessages = []
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
return {
|
|
1517
|
+
tabId: tab.id,
|
|
1518
|
+
content: JSON.stringify(messages, null, 2),
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
async function toolErrors({ tabId, clear = false } = {}) {
|
|
1523
|
+
const tab = await getTabById(tabId)
|
|
1524
|
+
const state = await ensureDebuggerAttached(tab.id)
|
|
1525
|
+
|
|
1526
|
+
if (!state.attached) {
|
|
1527
|
+
return {
|
|
1528
|
+
tabId: tab.id,
|
|
1529
|
+
content: JSON.stringify({
|
|
1530
|
+
error: state.unavailableReason || "Debugger not attached. DevTools may be open or another debugger is active.",
|
|
1531
|
+
errors: [],
|
|
1532
|
+
}),
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const errors = [...state.pageErrors]
|
|
1537
|
+
|
|
1538
|
+
if (clear) {
|
|
1539
|
+
state.pageErrors = []
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
return {
|
|
1543
|
+
tabId: tab.id,
|
|
1544
|
+
content: JSON.stringify(errors, null, 2),
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
chrome.runtime.onInstalled.addListener(() => connect().catch(() => {}))
|
|
1549
|
+
chrome.runtime.onStartup.addListener(() => connect().catch(() => {}))
|
|
1550
|
+
|
|
1551
|
+
if (chrome.permissions?.onAdded) {
|
|
1552
|
+
chrome.permissions.onAdded.addListener(() => connect().catch(() => {}))
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
chrome.action.onClicked.addListener(async () => {
|
|
1556
|
+
const permissionResult = await requestOptionalPermissionsFromClick()
|
|
1557
|
+
if (!permissionResult.granted) {
|
|
1558
|
+
updateBadge(false)
|
|
1559
|
+
if (permissionResult.error) {
|
|
1560
|
+
console.warn("[OpenCode] Permission request failed:", permissionResult.error)
|
|
1561
|
+
} else {
|
|
1562
|
+
console.warn("[OpenCode] Permission request denied.")
|
|
1563
|
+
}
|
|
1564
|
+
return
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (permissionResult.requested) {
|
|
1568
|
+
const requestedPermissions = permissionResult.permissions.join(", ") || "none"
|
|
1569
|
+
const requestedOrigins = permissionResult.origins.join(", ") || "none"
|
|
1570
|
+
console.log(`[OpenCode] Requested permissions -> permissions: ${requestedPermissions}; origins: ${requestedOrigins}`)
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
await connect()
|
|
999
1574
|
})
|
|
1000
1575
|
|
|
1001
|
-
connect()
|
|
1576
|
+
connect().catch(() => {})
|
package/extension/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "OpenCode Browser Automation",
|
|
4
4
|
"short_name": "OpenCode Browser",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.6.0",
|
|
6
6
|
"description": "Browser automation for OpenCode",
|
|
7
7
|
"homepage_url": "https://github.com/different-ai/opencode-browser",
|
|
8
8
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu9KICDxZh63m3QeQX13b37J/p7eG+cIEk4UDv+Phr7W/rqruy8DtdLFZ3WC37JYXo2Qk2iTWzIuBYjnc367RMY4Khu3f5hRyp67nvujuNY+0ACkrNGJk/viTodcW3CDcib9gijQQmPI1ZFkbec3qG0CN1hFxyn6pkdQZ8rnU9uo8ctntHIXrcfaHvQNT3AIVQfe8UjWPgalB0YCq9RamPVfMu6bG+6QLZGSKbvYDn0o5f8A/JZkm6K0MIii2EH61Oszhb/9GzZOciKqRZN+0mo23xbWHDp4ofDFyfQyMi83D3nDNldVGPSkIYlQWZaKHalaCoBRK+gzz7uuFoGrcsQIDAQAB",
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
"storage",
|
|
14
14
|
"notifications",
|
|
15
15
|
"alarms",
|
|
16
|
-
"nativeMessaging"
|
|
16
|
+
"nativeMessaging",
|
|
17
|
+
"downloads",
|
|
18
|
+
"debugger"
|
|
17
19
|
],
|
|
18
20
|
"host_permissions": [
|
|
19
21
|
"<all_urls>"
|