@different-ai/opencode-browser 4.5.0 → 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 +41 -9
- package/bin/broker.cjs +21 -20
- package/dist/plugin.js +353 -90
- package/extension/background.js +601 -19
- package/extension/manifest.json +4 -2
- package/package.json +4 -2
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
|
|
|
@@ -101,6 +295,7 @@ async function executeTool(toolName, args) {
|
|
|
101
295
|
get_active_tab: toolGetActiveTab,
|
|
102
296
|
get_tabs: toolGetTabs,
|
|
103
297
|
open_tab: toolOpenTab,
|
|
298
|
+
close_tab: toolCloseTab,
|
|
104
299
|
navigate: toolNavigate,
|
|
105
300
|
click: toolClick,
|
|
106
301
|
type: toolType,
|
|
@@ -110,6 +305,12 @@ async function executeTool(toolName, args) {
|
|
|
110
305
|
query: toolQuery,
|
|
111
306
|
scroll: toolScroll,
|
|
112
307
|
wait: toolWait,
|
|
308
|
+
download: toolDownload,
|
|
309
|
+
list_downloads: toolListDownloads,
|
|
310
|
+
set_file_input: toolSetFileInput,
|
|
311
|
+
highlight: toolHighlight,
|
|
312
|
+
console: toolConsole,
|
|
313
|
+
errors: toolErrors,
|
|
113
314
|
}
|
|
114
315
|
|
|
115
316
|
const fn = tools[toolName]
|
|
@@ -128,13 +329,26 @@ async function getTabById(tabId) {
|
|
|
128
329
|
}
|
|
129
330
|
|
|
130
331
|
async function runInPage(tabId, command, args) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
+
}
|
|
138
352
|
}
|
|
139
353
|
|
|
140
354
|
async function pageOps(command, args) {
|
|
@@ -608,6 +822,67 @@ async function pageOps(command, args) {
|
|
|
608
822
|
}
|
|
609
823
|
}
|
|
610
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
|
+
|
|
611
886
|
if (command === "scroll") {
|
|
612
887
|
const scrollX = Number.isFinite(options.x) ? options.x : 0
|
|
613
888
|
const scrollY = Number.isFinite(options.y) ? options.y : 0
|
|
@@ -625,6 +900,73 @@ async function pageOps(command, args) {
|
|
|
625
900
|
return { ok: true }
|
|
626
901
|
}
|
|
627
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
|
+
|
|
628
970
|
if (command === "query") {
|
|
629
971
|
if (mode === "page_text") {
|
|
630
972
|
if (selectors.length && timeoutMs > 0) {
|
|
@@ -709,6 +1051,12 @@ async function toolOpenTab({ url, active = true }) {
|
|
|
709
1051
|
return { tabId: tab.id, content: { tabId: tab.id, url: tab.url, active: tab.active } }
|
|
710
1052
|
}
|
|
711
1053
|
|
|
1054
|
+
async function toolCloseTab({ tabId }) {
|
|
1055
|
+
if (!Number.isFinite(tabId)) throw new Error("tabId is required")
|
|
1056
|
+
await chrome.tabs.remove(tabId)
|
|
1057
|
+
return { tabId, content: { tabId, closed: true } }
|
|
1058
|
+
}
|
|
1059
|
+
|
|
712
1060
|
async function toolNavigate({ url, tabId }) {
|
|
713
1061
|
if (!url) throw new Error("URL is required")
|
|
714
1062
|
const tab = await getTabById(tabId)
|
|
@@ -979,16 +1327,250 @@ async function toolWait({ ms = 1000, tabId }) {
|
|
|
979
1327
|
return { tabId, content: `Waited ${ms}ms` }
|
|
980
1328
|
}
|
|
981
1329
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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)
|
|
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,
|
|
991
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()
|
|
992
1574
|
})
|
|
993
1575
|
|
|
994
|
-
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>"
|