@different-ai/opencode-browser 2.1.0 → 4.0.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.
@@ -1,285 +1,351 @@
1
- const PLUGIN_URL = "ws://localhost:19222";
2
- const KEEPALIVE_ALARM = "keepalive";
1
+ const NATIVE_HOST_NAME = "com.opencode.browser_automation"
2
+ const KEEPALIVE_ALARM = "keepalive"
3
3
 
4
- let ws = null;
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 (ws && ws.readyState === WebSocket.OPEN) return;
20
- if (ws) {
21
- try { ws.close(); } catch {}
22
- ws = null;
17
+ if (port) {
18
+ try { port.disconnect() } catch {}
19
+ port = null
23
20
  }
24
-
21
+
25
22
  try {
26
- ws = new WebSocket(PLUGIN_URL);
27
-
28
- ws.onopen = () => {
29
- console.log("[OpenCode] Connected to plugin");
30
- isConnected = true;
31
- updateBadge(true);
32
- };
33
-
34
- ws.onmessage = async (event) => {
35
- try {
36
- const message = JSON.parse(event.data);
37
- await handleMessage(message);
38
- } catch (e) {
39
- console.error("[OpenCode] Parse error:", e);
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
- ws.onclose = () => {
44
- console.log("[OpenCode] Disconnected");
45
- isConnected = false;
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
- console.error("[OpenCode] Connect failed:", e);
57
- isConnected = false;
58
- updateBadge(false);
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 (ws && ws.readyState === WebSocket.OPEN) {
69
- ws.send(JSON.stringify(message));
70
- return true;
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: { content: result } });
87
+ const result = await executeTool(tool, args || {})
88
+ send({ type: "tool_response", id, result })
89
89
  } catch (error) {
90
- send({ type: "tool_response", id, error: { content: error.message || String(error) } });
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(() => { chrome.tabs.onUpdated.removeListener(listener); resolve(); }, 30000);
136
- });
137
-
138
- return `Navigated to ${url}`;
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
- return await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" });
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 el.getAttribute("aria-label") || el.getAttribute("alt") ||
201
- el.getAttribute("title") || el.getAttribute("placeholder") ||
202
- el.innerText?.slice(0, 100) || "";
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 = ["A", "BUTTON", "INPUT", "TEXTAREA", "SELECT"].includes(el.tagName) ||
212
- el.getAttribute("onclick") || el.getAttribute("role") === "button" || el.isContentEditable;
213
- const rect = el.getBoundingClientRect();
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 = { uid: `e${uid}`, role: el.getAttribute("role") || el.tagName.toLowerCase(),
217
- name: getName(el).slice(0, 200), tag: el.tagName.toLowerCase() };
218
- if (el.tagName === "A" && el.href) node.href = el.href;
219
- if (el.tagName === "INPUT") { node.type = el.type; node.value = el.value; }
220
- if (el.id) node.selector = `#${el.id}`;
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}`
221
251
  else if (el.className && typeof el.className === "string") {
222
- const cls = el.className.trim().split(/\s+/).slice(0, 2).join(".");
223
- 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}`
224
254
  }
225
- nodes.push(node);
226
- uid++;
255
+ nodes.push(node)
256
+ uid++
227
257
  }
228
-
258
+
229
259
  for (const child of el.children) {
230
- const r = build(child, depth + 1, uid);
231
- nodes.push(...r.nodes);
232
- uid = r.nextUid;
260
+ const r = build(child, depth + 1, uid)
261
+ nodes.push(...r.nodes)
262
+ uid = r.nextUid
233
263
  }
234
- return { nodes, nextUid: uid };
264
+ return { nodes, nextUid: uid }
235
265
  }
236
-
237
- return { url: location.href, title: document.title, nodes: build(document.body).nodes.slice(0, 500) };
238
- }
239
- });
240
-
241
- return JSON.stringify(result[0]?.result, null, 2);
266
+
267
+ function getAllLinks() {
268
+ const links = []
269
+ const seen = new Set()
270
+ document.querySelectorAll("a[href]").forEach((a) => {
271
+ const href = a.href
272
+ if (href && !seen.has(href) && !href.startsWith("javascript:")) {
273
+ seen.add(href)
274
+ const text = a.innerText?.trim().slice(0, 100) || a.getAttribute("aria-label") || ""
275
+ links.push({ href, text })
276
+ }
277
+ })
278
+ return links.slice(0, 100)
279
+ }
280
+
281
+ return {
282
+ url: location.href,
283
+ title: document.title,
284
+ nodes: build(document.body).nodes.slice(0, 500),
285
+ links: getAllLinks(),
286
+ }
287
+ },
288
+ })
289
+
290
+ return { tabId: tab.id, content: JSON.stringify(result[0]?.result, null, 2) }
242
291
  }
243
292
 
244
293
  async function toolGetTabs() {
245
- const tabs = await chrome.tabs.query({});
246
- return JSON.stringify(tabs.map(t => ({ id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId })), null, 2);
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) }
247
297
  }
248
298
 
249
299
  async function toolExecuteScript({ code, tabId }) {
250
- if (!code) throw new Error("Code is required");
251
- const tab = await getTabById(tabId);
252
- const result = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: new Function(code) });
253
- return JSON.stringify(result[0]?.result);
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) }
254
307
  }
255
308
 
256
309
  async function toolScroll({ x = 0, y = 0, selector, tabId }) {
257
- const tab = await getTabById(tabId);
258
- const sel = selector || null;
259
-
310
+ const tab = await getTabById(tabId)
311
+ const sel = selector || null
312
+
260
313
  await chrome.scripting.executeScript({
261
314
  target: { tabId: tab.id },
262
315
  func: (scrollX, scrollY, sel) => {
263
- if (sel) { const el = document.querySelector(sel); if (el) { el.scrollIntoView({ behavior: "smooth", block: "center" }); return; } }
264
- window.scrollBy(scrollX, scrollY);
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)
265
324
  },
266
- args: [x, y, sel]
267
- });
268
-
269
- 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})`}` }
270
329
  }
271
330
 
272
- async function toolWait({ ms = 1000 }) {
273
- await new Promise(resolve => setTimeout(resolve, ms));
274
- return `Waited ${ms}ms`;
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` }
275
337
  }
276
338
 
277
- chrome.runtime.onInstalled.addListener(() => connect());
278
- chrome.runtime.onStartup.addListener(() => connect());
339
+ chrome.runtime.onInstalled.addListener(() => connect())
340
+ chrome.runtime.onStartup.addListener(() => connect())
279
341
  chrome.action.onClicked.addListener(() => {
280
- connect();
281
- chrome.notifications.create({ type: "basic", iconUrl: "icons/icon128.png", title: "OpenCode Browser",
282
- message: isConnected ? "Connected" : "Reconnecting..." });
283
- });
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
+ })
284
350
 
285
- connect();
351
+ connect()
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCode Browser Automation",
4
- "version": "2.0.0",
4
+ "version": "4.0.0",
5
5
  "description": "Browser automation for OpenCode",
6
6
  "permissions": [
7
7
  "tabs",
@@ -9,7 +9,8 @@
9
9
  "scripting",
10
10
  "storage",
11
11
  "notifications",
12
- "alarms"
12
+ "alarms",
13
+ "nativeMessaging"
13
14
  ],
14
15
  "host_permissions": [
15
16
  "<all_urls>"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@different-ai/opencode-browser",
3
- "version": "2.1.0",
4
- "description": "Browser automation plugin for OpenCode. Control your real Chrome browser with existing logins and cookies.",
3
+ "version": "4.0.0",
4
+ "description": "Browser automation plugin for OpenCode (native messaging + per-tab ownership).",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opencode-browser": "./bin/cli.js"
@@ -13,19 +13,22 @@
13
13
  },
14
14
  "files": [
15
15
  "bin",
16
- "src",
16
+ "src/plugin.ts",
17
17
  "extension",
18
18
  "README.md"
19
19
  ],
20
20
  "scripts": {
21
- "install-extension": "node bin/cli.js install"
21
+ "install": "node bin/cli.js install",
22
+ "uninstall": "node bin/cli.js uninstall",
23
+ "status": "node bin/cli.js status"
22
24
  },
23
25
  "keywords": [
24
26
  "opencode",
25
27
  "browser",
26
28
  "automation",
27
29
  "chrome",
28
- "plugin"
30
+ "plugin",
31
+ "native-messaging"
29
32
  ],
30
33
  "author": "Benjamin Shafii",
31
34
  "license": "MIT",