@different-ai/opencode-browser 3.0.0 → 4.0.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 +46 -102
- package/bin/broker.cjs +290 -0
- package/bin/cli.js +266 -240
- package/bin/native-host.cjs +136 -0
- package/extension/background.js +235 -190
- package/extension/manifest.json +3 -2
- package/package.json +15 -13
- package/src/plugin.ts +299 -0
- package/src/mcp-server.ts +0 -440
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// Chrome Native Messaging host for OpenCode Browser.
|
|
5
|
+
// Speaks length-prefixed JSON over stdin/stdout and forwards messages to the local broker over a unix socket.
|
|
6
|
+
|
|
7
|
+
const net = require("net");
|
|
8
|
+
const fs = require("fs");
|
|
9
|
+
const os = require("os");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const { spawn } = require("child_process");
|
|
12
|
+
|
|
13
|
+
const BASE_DIR = path.join(os.homedir(), ".opencode-browser");
|
|
14
|
+
const SOCKET_PATH = path.join(BASE_DIR, "broker.sock");
|
|
15
|
+
const BROKER_PATH = path.join(BASE_DIR, "broker.cjs");
|
|
16
|
+
|
|
17
|
+
fs.mkdirSync(BASE_DIR, { recursive: true });
|
|
18
|
+
|
|
19
|
+
function createJsonLineParser(onMessage) {
|
|
20
|
+
let buffer = "";
|
|
21
|
+
return (chunk) => {
|
|
22
|
+
buffer += chunk.toString("utf8");
|
|
23
|
+
while (true) {
|
|
24
|
+
const idx = buffer.indexOf("\n");
|
|
25
|
+
if (idx === -1) return;
|
|
26
|
+
const line = buffer.slice(0, idx);
|
|
27
|
+
buffer = buffer.slice(idx + 1);
|
|
28
|
+
if (!line.trim()) continue;
|
|
29
|
+
try {
|
|
30
|
+
onMessage(JSON.parse(line));
|
|
31
|
+
} catch {
|
|
32
|
+
// ignore
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeJsonLine(socket, msg) {
|
|
39
|
+
socket.write(JSON.stringify(msg) + "\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function maybeStartBroker() {
|
|
43
|
+
try {
|
|
44
|
+
if (!fs.existsSync(BROKER_PATH)) return;
|
|
45
|
+
const child = spawn(process.execPath, [BROKER_PATH], { detached: true, stdio: "ignore" });
|
|
46
|
+
child.unref();
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function connectToBroker() {
|
|
53
|
+
return await new Promise((resolve, reject) => {
|
|
54
|
+
const socket = net.createConnection(SOCKET_PATH);
|
|
55
|
+
socket.once("connect", () => resolve(socket));
|
|
56
|
+
socket.once("error", (err) => reject(err));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function ensureBroker() {
|
|
61
|
+
try {
|
|
62
|
+
return await connectToBroker();
|
|
63
|
+
} catch {
|
|
64
|
+
maybeStartBroker();
|
|
65
|
+
for (let i = 0; i < 50; i++) {
|
|
66
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
67
|
+
try {
|
|
68
|
+
return await connectToBroker();
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
throw new Error("Could not connect to broker");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Native messaging framing ---
|
|
76
|
+
let stdinBuffer = Buffer.alloc(0);
|
|
77
|
+
|
|
78
|
+
function writeNativeMessage(obj) {
|
|
79
|
+
try {
|
|
80
|
+
const payload = Buffer.from(JSON.stringify(obj), "utf8");
|
|
81
|
+
const header = Buffer.alloc(4);
|
|
82
|
+
header.writeUInt32LE(payload.length, 0);
|
|
83
|
+
process.stdout.write(Buffer.concat([header, payload]));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error("[native-host] write error", e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function onStdinData(chunk, onMessage) {
|
|
90
|
+
stdinBuffer = Buffer.concat([stdinBuffer, chunk]);
|
|
91
|
+
while (stdinBuffer.length >= 4) {
|
|
92
|
+
const len = stdinBuffer.readUInt32LE(0);
|
|
93
|
+
if (stdinBuffer.length < 4 + len) return;
|
|
94
|
+
const body = stdinBuffer.slice(4, 4 + len);
|
|
95
|
+
stdinBuffer = stdinBuffer.slice(4 + len);
|
|
96
|
+
try {
|
|
97
|
+
onMessage(JSON.parse(body.toString("utf8")));
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
(async () => {
|
|
105
|
+
const broker = await ensureBroker();
|
|
106
|
+
broker.setNoDelay(true);
|
|
107
|
+
broker.on("data", createJsonLineParser((msg) => {
|
|
108
|
+
if (msg && msg.type === "to_extension" && msg.message) {
|
|
109
|
+
writeNativeMessage(msg.message);
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
broker.on("close", () => {
|
|
114
|
+
process.exit(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
broker.on("error", () => {
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
writeJsonLine(broker, { type: "hello", role: "native-host" });
|
|
122
|
+
|
|
123
|
+
process.stdin.on("data", (chunk) =>
|
|
124
|
+
onStdinData(chunk, (message) => {
|
|
125
|
+
// Forward extension-origin messages to broker.
|
|
126
|
+
writeJsonLine(broker, { type: "from_extension", message });
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
process.stdin.on("end", () => {
|
|
131
|
+
try {
|
|
132
|
+
broker.end();
|
|
133
|
+
} catch {}
|
|
134
|
+
process.exit(0);
|
|
135
|
+
});
|
|
136
|
+
})();
|
package/extension/background.js
CHANGED
|
@@ -1,306 +1,351 @@
|
|
|
1
|
-
const
|
|
2
|
-
const KEEPALIVE_ALARM = "keepalive"
|
|
1
|
+
const NATIVE_HOST_NAME = "com.opencode.browser_automation"
|
|
2
|
+
const KEEPALIVE_ALARM = "keepalive"
|
|
3
3
|
|
|
4
|
-
let
|
|
5
|
-
let isConnected = false
|
|
4
|
+
let port = null
|
|
5
|
+
let isConnected = false
|
|
6
|
+
let connectionAttempts = 0
|
|
6
7
|
|
|
7
|
-
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.25 })
|
|
8
|
+
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.25 })
|
|
8
9
|
|
|
9
10
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
10
11
|
if (alarm.name === KEEPALIVE_ALARM) {
|
|
11
|
-
if (!isConnected)
|
|
12
|
-
console.log("[OpenCode] Alarm triggered reconnect");
|
|
13
|
-
connect();
|
|
14
|
-
}
|
|
12
|
+
if (!isConnected) connect()
|
|
15
13
|
}
|
|
16
|
-
})
|
|
14
|
+
})
|
|
17
15
|
|
|
18
16
|
function connect() {
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ws = null;
|
|
17
|
+
if (port) {
|
|
18
|
+
try { port.disconnect() } catch {}
|
|
19
|
+
port = null
|
|
23
20
|
}
|
|
24
|
-
|
|
21
|
+
|
|
25
22
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
port = chrome.runtime.connectNative(NATIVE_HOST_NAME)
|
|
24
|
+
|
|
25
|
+
port.onMessage.addListener((message) => {
|
|
26
|
+
handleMessage(message).catch((e) => {
|
|
27
|
+
console.error("[OpenCode] Message handler error:", e)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
port.onDisconnect.addListener(() => {
|
|
32
|
+
isConnected = false
|
|
33
|
+
port = null
|
|
34
|
+
updateBadge(false)
|
|
35
|
+
|
|
36
|
+
const err = chrome.runtime.lastError
|
|
37
|
+
if (err?.message) {
|
|
38
|
+
// Usually means native host not installed or crashed
|
|
39
|
+
connectionAttempts++
|
|
40
|
+
if (connectionAttempts === 1) {
|
|
41
|
+
console.log("[OpenCode] Native host not available. Run: npx @different-ai/opencode-browser install")
|
|
42
|
+
} else if (connectionAttempts % 20 === 0) {
|
|
43
|
+
console.log("[OpenCode] Still waiting for native host...")
|
|
44
|
+
}
|
|
40
45
|
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
ws = null;
|
|
47
|
-
updateBadge(false);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
ws.onerror = (err) => {
|
|
51
|
-
console.error("[OpenCode] WebSocket error");
|
|
52
|
-
isConnected = false;
|
|
53
|
-
updateBadge(false);
|
|
54
|
-
};
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
isConnected = true
|
|
49
|
+
connectionAttempts = 0
|
|
50
|
+
updateBadge(true)
|
|
55
51
|
} catch (e) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
isConnected = false
|
|
53
|
+
updateBadge(false)
|
|
54
|
+
console.error("[OpenCode] connectNative failed:", e)
|
|
59
55
|
}
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
function updateBadge(connected) {
|
|
63
|
-
chrome.action.setBadgeText({ text: connected ? "ON" : "" })
|
|
64
|
-
chrome.action.setBadgeBackgroundColor({ color: connected ? "#22c55e" : "#ef4444" })
|
|
59
|
+
chrome.action.setBadgeText({ text: connected ? "ON" : "" })
|
|
60
|
+
chrome.action.setBadgeBackgroundColor({ color: connected ? "#22c55e" : "#ef4444" })
|
|
65
61
|
}
|
|
66
62
|
|
|
67
63
|
function send(message) {
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
if (!port) return false
|
|
65
|
+
try {
|
|
66
|
+
port.postMessage(message)
|
|
67
|
+
return true
|
|
68
|
+
} catch {
|
|
69
|
+
return false
|
|
71
70
|
}
|
|
72
|
-
return false;
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
async function handleMessage(message) {
|
|
74
|
+
if (!message || typeof message !== "object") return
|
|
75
|
+
|
|
76
76
|
if (message.type === "tool_request") {
|
|
77
|
-
await handleToolRequest(message)
|
|
77
|
+
await handleToolRequest(message)
|
|
78
78
|
} else if (message.type === "ping") {
|
|
79
|
-
send({ type: "pong" })
|
|
79
|
+
send({ type: "pong" })
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
async function handleToolRequest(request) {
|
|
84
|
-
const { id, tool, args } = request
|
|
85
|
-
|
|
84
|
+
const { id, tool, args } = request
|
|
85
|
+
|
|
86
86
|
try {
|
|
87
|
-
const result = await executeTool(tool, args || {})
|
|
88
|
-
send({ type: "tool_response", id, result
|
|
87
|
+
const result = await executeTool(tool, args || {})
|
|
88
|
+
send({ type: "tool_response", id, result })
|
|
89
89
|
} catch (error) {
|
|
90
|
-
send({
|
|
90
|
+
send({
|
|
91
|
+
type: "tool_response",
|
|
92
|
+
id,
|
|
93
|
+
error: { content: error?.message || String(error) },
|
|
94
|
+
})
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
async function executeTool(toolName, args) {
|
|
95
99
|
const tools = {
|
|
100
|
+
get_active_tab: toolGetActiveTab,
|
|
101
|
+
get_tabs: toolGetTabs,
|
|
96
102
|
navigate: toolNavigate,
|
|
97
103
|
click: toolClick,
|
|
98
104
|
type: toolType,
|
|
99
105
|
screenshot: toolScreenshot,
|
|
100
106
|
snapshot: toolSnapshot,
|
|
101
|
-
get_tabs: toolGetTabs,
|
|
102
107
|
execute_script: toolExecuteScript,
|
|
103
108
|
scroll: toolScroll,
|
|
104
|
-
wait: toolWait
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const fn = tools[toolName]
|
|
108
|
-
if (!fn) throw new Error(`Unknown tool: ${toolName}`)
|
|
109
|
-
return await fn(args)
|
|
109
|
+
wait: toolWait,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const fn = tools[toolName]
|
|
113
|
+
if (!fn) throw new Error(`Unknown tool: ${toolName}`)
|
|
114
|
+
return await fn(args)
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
async function getActiveTab() {
|
|
113
|
-
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
|
|
114
|
-
if (!tab?.id) throw new Error("No active tab found")
|
|
115
|
-
return tab
|
|
118
|
+
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
|
|
119
|
+
if (!tab?.id) throw new Error("No active tab found")
|
|
120
|
+
return tab
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
async function getTabById(tabId) {
|
|
119
|
-
return tabId ? await chrome.tabs.get(tabId) : await getActiveTab()
|
|
124
|
+
return tabId ? await chrome.tabs.get(tabId) : await getActiveTab()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function toolGetActiveTab() {
|
|
128
|
+
const tab = await getActiveTab()
|
|
129
|
+
return { tabId: tab.id, content: { tabId: tab.id, url: tab.url, title: tab.title } }
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
async function toolNavigate({ url, tabId }) {
|
|
123
|
-
if (!url) throw new Error("URL is required")
|
|
124
|
-
const tab = await getTabById(tabId)
|
|
125
|
-
await chrome.tabs.update(tab.id, { url })
|
|
126
|
-
|
|
133
|
+
if (!url) throw new Error("URL is required")
|
|
134
|
+
const tab = await getTabById(tabId)
|
|
135
|
+
await chrome.tabs.update(tab.id, { url })
|
|
136
|
+
|
|
127
137
|
await new Promise((resolve) => {
|
|
128
138
|
const listener = (updatedTabId, info) => {
|
|
129
139
|
if (updatedTabId === tab.id && info.status === "complete") {
|
|
130
|
-
chrome.tabs.onUpdated.removeListener(listener)
|
|
131
|
-
resolve()
|
|
140
|
+
chrome.tabs.onUpdated.removeListener(listener)
|
|
141
|
+
resolve()
|
|
132
142
|
}
|
|
133
|
-
}
|
|
134
|
-
chrome.tabs.onUpdated.addListener(listener)
|
|
135
|
-
setTimeout(() => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
143
|
+
}
|
|
144
|
+
chrome.tabs.onUpdated.addListener(listener)
|
|
145
|
+
setTimeout(() => {
|
|
146
|
+
chrome.tabs.onUpdated.removeListener(listener)
|
|
147
|
+
resolve()
|
|
148
|
+
}, 30000)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return { tabId: tab.id, content: `Navigated to ${url}` }
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
async function toolClick({ selector, tabId }) {
|
|
142
|
-
if (!selector) throw new Error("Selector is required")
|
|
143
|
-
const tab = await getTabById(tabId)
|
|
144
|
-
|
|
155
|
+
if (!selector) throw new Error("Selector is required")
|
|
156
|
+
const tab = await getTabById(tabId)
|
|
157
|
+
|
|
145
158
|
const result = await chrome.scripting.executeScript({
|
|
146
159
|
target: { tabId: tab.id },
|
|
147
160
|
func: (sel) => {
|
|
148
|
-
const el = document.querySelector(sel)
|
|
149
|
-
if (!el) return { success: false, error: `Element not found: ${sel}` }
|
|
150
|
-
el.click()
|
|
151
|
-
return { success: true }
|
|
161
|
+
const el = document.querySelector(sel)
|
|
162
|
+
if (!el) return { success: false, error: `Element not found: ${sel}` }
|
|
163
|
+
el.click()
|
|
164
|
+
return { success: true }
|
|
152
165
|
},
|
|
153
|
-
args: [selector]
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Click failed")
|
|
157
|
-
return `Clicked ${selector}
|
|
166
|
+
args: [selector],
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Click failed")
|
|
170
|
+
return { tabId: tab.id, content: `Clicked ${selector}` }
|
|
158
171
|
}
|
|
159
172
|
|
|
160
173
|
async function toolType({ selector, text, tabId, clear = false }) {
|
|
161
|
-
if (!selector) throw new Error("Selector is required")
|
|
162
|
-
if (text === undefined) throw new Error("Text is required")
|
|
163
|
-
const tab = await getTabById(tabId)
|
|
164
|
-
|
|
174
|
+
if (!selector) throw new Error("Selector is required")
|
|
175
|
+
if (text === undefined) throw new Error("Text is required")
|
|
176
|
+
const tab = await getTabById(tabId)
|
|
177
|
+
|
|
165
178
|
const result = await chrome.scripting.executeScript({
|
|
166
179
|
target: { tabId: tab.id },
|
|
167
180
|
func: (sel, txt, shouldClear) => {
|
|
168
|
-
const el = document.querySelector(sel)
|
|
169
|
-
if (!el) return { success: false, error: `Element not found: ${sel}` }
|
|
170
|
-
el.focus()
|
|
171
|
-
if (shouldClear) el.value = ""
|
|
181
|
+
const el = document.querySelector(sel)
|
|
182
|
+
if (!el) return { success: false, error: `Element not found: ${sel}` }
|
|
183
|
+
el.focus()
|
|
184
|
+
if (shouldClear && (el.tagName === "INPUT" || el.tagName === "TEXTAREA")) el.value = ""
|
|
185
|
+
|
|
172
186
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
173
|
-
el.value = el.value + txt
|
|
174
|
-
el.dispatchEvent(new Event("input", { bubbles: true }))
|
|
175
|
-
el.dispatchEvent(new Event("change", { bubbles: true }))
|
|
187
|
+
el.value = el.value + txt
|
|
188
|
+
el.dispatchEvent(new Event("input", { bubbles: true }))
|
|
189
|
+
el.dispatchEvent(new Event("change", { bubbles: true }))
|
|
176
190
|
} else if (el.isContentEditable) {
|
|
177
|
-
document.execCommand("insertText", false, txt)
|
|
191
|
+
document.execCommand("insertText", false, txt)
|
|
178
192
|
}
|
|
179
|
-
return { success: true }
|
|
193
|
+
return { success: true }
|
|
180
194
|
},
|
|
181
|
-
args: [selector, text, clear]
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Type failed")
|
|
185
|
-
return `Typed "${text}" into ${selector}
|
|
195
|
+
args: [selector, text, clear],
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
if (!result[0]?.result?.success) throw new Error(result[0]?.result?.error || "Type failed")
|
|
199
|
+
return { tabId: tab.id, content: `Typed "${text}" into ${selector}` }
|
|
186
200
|
}
|
|
187
201
|
|
|
188
202
|
async function toolScreenshot({ tabId }) {
|
|
189
|
-
const tab = await getTabById(tabId)
|
|
190
|
-
|
|
203
|
+
const tab = await getTabById(tabId)
|
|
204
|
+
const png = await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" })
|
|
205
|
+
return { tabId: tab.id, content: png }
|
|
191
206
|
}
|
|
192
207
|
|
|
193
208
|
async function toolSnapshot({ tabId }) {
|
|
194
|
-
const tab = await getTabById(tabId)
|
|
195
|
-
|
|
209
|
+
const tab = await getTabById(tabId)
|
|
210
|
+
|
|
196
211
|
const result = await chrome.scripting.executeScript({
|
|
197
212
|
target: { tabId: tab.id },
|
|
198
213
|
func: () => {
|
|
199
214
|
function getName(el) {
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
215
|
+
return (
|
|
216
|
+
el.getAttribute("aria-label") ||
|
|
217
|
+
el.getAttribute("alt") ||
|
|
218
|
+
el.getAttribute("title") ||
|
|
219
|
+
el.getAttribute("placeholder") ||
|
|
220
|
+
el.innerText?.slice(0, 100) ||
|
|
221
|
+
""
|
|
222
|
+
)
|
|
203
223
|
}
|
|
204
|
-
|
|
224
|
+
|
|
205
225
|
function build(el, depth = 0, uid = 0) {
|
|
206
|
-
if (depth > 10) return { nodes: [], nextUid: uid }
|
|
207
|
-
const nodes = []
|
|
208
|
-
const style = window.getComputedStyle(el)
|
|
209
|
-
if (style.display === "none" || style.visibility === "hidden") return { nodes: [], nextUid: uid }
|
|
210
|
-
|
|
211
|
-
const isInteractive =
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
226
|
+
if (depth > 10) return { nodes: [], nextUid: uid }
|
|
227
|
+
const nodes = []
|
|
228
|
+
const style = window.getComputedStyle(el)
|
|
229
|
+
if (style.display === "none" || style.visibility === "hidden") return { nodes: [], nextUid: uid }
|
|
230
|
+
|
|
231
|
+
const isInteractive =
|
|
232
|
+
["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"].includes(el.tagName) ||
|
|
233
|
+
el.getAttribute("onclick") ||
|
|
234
|
+
el.getAttribute("role") === "button" ||
|
|
235
|
+
el.isContentEditable
|
|
236
|
+
const rect = el.getBoundingClientRect()
|
|
237
|
+
|
|
215
238
|
if (rect.width > 0 && rect.height > 0 && (isInteractive || el.innerText?.trim())) {
|
|
216
|
-
const node = {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
239
|
+
const node = {
|
|
240
|
+
uid: `e${uid}`,
|
|
241
|
+
role: el.getAttribute("role") || el.tagName.toLowerCase(),
|
|
242
|
+
name: getName(el).slice(0, 200),
|
|
243
|
+
tag: el.tagName.toLowerCase(),
|
|
244
|
+
}
|
|
245
|
+
if (el.href) node.href = el.href
|
|
246
|
+
if (el.tagName === "INPUT") {
|
|
247
|
+
node.type = el.type
|
|
248
|
+
node.value = el.value
|
|
249
|
+
}
|
|
250
|
+
if (el.id) node.selector = `#${el.id}`
|
|
222
251
|
else if (el.className && typeof el.className === "string") {
|
|
223
|
-
const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".")
|
|
224
|
-
if (cls) node.selector = `${el.tagName.toLowerCase()}.${cls}
|
|
252
|
+
const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".")
|
|
253
|
+
if (cls) node.selector = `${el.tagName.toLowerCase()}.${cls}`
|
|
225
254
|
}
|
|
226
|
-
nodes.push(node)
|
|
227
|
-
uid
|
|
255
|
+
nodes.push(node)
|
|
256
|
+
uid++
|
|
228
257
|
}
|
|
229
|
-
|
|
258
|
+
|
|
230
259
|
for (const child of el.children) {
|
|
231
|
-
const r = build(child, depth + 1, uid)
|
|
232
|
-
nodes.push(...r.nodes)
|
|
233
|
-
uid = r.nextUid
|
|
260
|
+
const r = build(child, depth + 1, uid)
|
|
261
|
+
nodes.push(...r.nodes)
|
|
262
|
+
uid = r.nextUid
|
|
234
263
|
}
|
|
235
|
-
return { nodes, nextUid: uid }
|
|
264
|
+
return { nodes, nextUid: uid }
|
|
236
265
|
}
|
|
237
|
-
|
|
238
|
-
// Collect all links on the page separately for easy access
|
|
266
|
+
|
|
239
267
|
function getAllLinks() {
|
|
240
|
-
const links = []
|
|
241
|
-
const seen = new Set()
|
|
242
|
-
document.querySelectorAll("a[href]").forEach(a => {
|
|
243
|
-
const href = a.href
|
|
268
|
+
const links = []
|
|
269
|
+
const seen = new Set()
|
|
270
|
+
document.querySelectorAll("a[href]").forEach((a) => {
|
|
271
|
+
const href = a.href
|
|
244
272
|
if (href && !seen.has(href) && !href.startsWith("javascript:")) {
|
|
245
|
-
seen.add(href)
|
|
246
|
-
const text = a.innerText?.trim().slice(0, 100) || a.getAttribute("aria-label") || ""
|
|
247
|
-
links.push({ href, text })
|
|
273
|
+
seen.add(href)
|
|
274
|
+
const text = a.innerText?.trim().slice(0, 100) || a.getAttribute("aria-label") || ""
|
|
275
|
+
links.push({ href, text })
|
|
248
276
|
}
|
|
249
|
-
})
|
|
250
|
-
return links.slice(0, 100)
|
|
277
|
+
})
|
|
278
|
+
return links.slice(0, 100)
|
|
251
279
|
}
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
url: location.href,
|
|
255
|
-
title: document.title,
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
url: location.href,
|
|
283
|
+
title: document.title,
|
|
256
284
|
nodes: build(document.body).nodes.slice(0, 500),
|
|
257
|
-
links: getAllLinks()
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
return JSON.stringify(result[0]?.result, null, 2)
|
|
285
|
+
links: getAllLinks(),
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
return { tabId: tab.id, content: JSON.stringify(result[0]?.result, null, 2) }
|
|
263
291
|
}
|
|
264
292
|
|
|
265
293
|
async function toolGetTabs() {
|
|
266
|
-
const tabs = await chrome.tabs.query({})
|
|
267
|
-
|
|
294
|
+
const tabs = await chrome.tabs.query({})
|
|
295
|
+
const out = tabs.map((t) => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId }))
|
|
296
|
+
return { content: JSON.stringify(out, null, 2) }
|
|
268
297
|
}
|
|
269
298
|
|
|
270
299
|
async function toolExecuteScript({ code, tabId }) {
|
|
271
|
-
if (!code) throw new Error("Code is required")
|
|
272
|
-
const tab = await getTabById(tabId)
|
|
273
|
-
const result = await chrome.scripting.executeScript({
|
|
274
|
-
|
|
300
|
+
if (!code) throw new Error("Code is required")
|
|
301
|
+
const tab = await getTabById(tabId)
|
|
302
|
+
const result = await chrome.scripting.executeScript({
|
|
303
|
+
target: { tabId: tab.id },
|
|
304
|
+
func: new Function(code),
|
|
305
|
+
})
|
|
306
|
+
return { tabId: tab.id, content: JSON.stringify(result[0]?.result) }
|
|
275
307
|
}
|
|
276
308
|
|
|
277
309
|
async function toolScroll({ x = 0, y = 0, selector, tabId }) {
|
|
278
|
-
const tab = await getTabById(tabId)
|
|
279
|
-
const sel = selector || null
|
|
280
|
-
|
|
310
|
+
const tab = await getTabById(tabId)
|
|
311
|
+
const sel = selector || null
|
|
312
|
+
|
|
281
313
|
await chrome.scripting.executeScript({
|
|
282
314
|
target: { tabId: tab.id },
|
|
283
315
|
func: (scrollX, scrollY, sel) => {
|
|
284
|
-
if (sel) {
|
|
285
|
-
|
|
316
|
+
if (sel) {
|
|
317
|
+
const el = document.querySelector(sel)
|
|
318
|
+
if (el) {
|
|
319
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" })
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
window.scrollBy(scrollX, scrollY)
|
|
286
324
|
},
|
|
287
|
-
args: [x, y, sel]
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
return `Scrolled ${sel ? `to ${sel}` : `by (${x}, ${y})`}
|
|
325
|
+
args: [x, y, sel],
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
return { tabId: tab.id, content: `Scrolled ${sel ? `to ${sel}` : `by (${x}, ${y})`}` }
|
|
291
329
|
}
|
|
292
330
|
|
|
293
|
-
async function toolWait({ ms = 1000 }) {
|
|
294
|
-
|
|
295
|
-
|
|
331
|
+
async function toolWait({ ms = 1000, tabId }) {
|
|
332
|
+
if (typeof tabId === "number") {
|
|
333
|
+
// keep tabId in response for ownership purposes
|
|
334
|
+
}
|
|
335
|
+
await new Promise((resolve) => setTimeout(resolve, ms))
|
|
336
|
+
return { tabId, content: `Waited ${ms}ms` }
|
|
296
337
|
}
|
|
297
338
|
|
|
298
|
-
chrome.runtime.onInstalled.addListener(() => connect())
|
|
299
|
-
chrome.runtime.onStartup.addListener(() => connect())
|
|
339
|
+
chrome.runtime.onInstalled.addListener(() => connect())
|
|
340
|
+
chrome.runtime.onStartup.addListener(() => connect())
|
|
300
341
|
chrome.action.onClicked.addListener(() => {
|
|
301
|
-
connect()
|
|
302
|
-
chrome.notifications.create({
|
|
303
|
-
|
|
304
|
-
|
|
342
|
+
connect()
|
|
343
|
+
chrome.notifications.create({
|
|
344
|
+
type: "basic",
|
|
345
|
+
iconUrl: "icons/icon128.png",
|
|
346
|
+
title: "OpenCode Browser",
|
|
347
|
+
message: isConnected ? "Connected" : "Reconnecting...",
|
|
348
|
+
})
|
|
349
|
+
})
|
|
305
350
|
|
|
306
|
-
connect()
|
|
351
|
+
connect()
|