@different-ai/opencode-browser 4.3.0 → 4.3.2

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 CHANGED
@@ -4,20 +4,32 @@ Browser automation plugin for [OpenCode](https://github.com/opencode-ai/opencode
4
4
 
5
5
  Control your real Chromium browser (Chrome/Brave/Arc/Edge) using your existing profile (logins, cookies, bookmarks). No DevTools Protocol, no security prompts.
6
6
 
7
+
8
+ https://github.com/user-attachments/assets/1496b3b3-419b-436c-b412-8cda2fed83d6
9
+
10
+
7
11
  ## Why this architecture
8
12
 
9
13
  This version is optimized for reliability and predictable multi-session behavior:
10
-
14
+ - **No MCP** -> just opencode plugin
11
15
  - **No WebSocket port** → no port conflicts
12
16
  - **Chrome Native Messaging** between extension and a local host process
13
17
  - A local **broker** multiplexes multiple OpenCode plugin sessions and enforces **per-tab ownership**
14
18
 
15
19
  ## Installation
16
20
 
21
+ > Help me improve this!
22
+
17
23
  ```bash
18
- npx @different-ai/opencode-browser install
24
+ bunx @different-ai/opencode-browser@latest install
19
25
  ```
20
26
 
27
+
28
+ https://github.com/user-attachments/assets/d5767362-fbf3-4023-858b-90f06d9f0b25
29
+
30
+
31
+
32
+
21
33
  The installer will:
22
34
 
23
35
  1. Copy the extension to `~/.opencode-browser/extension/`
@@ -62,6 +74,7 @@ Core primitives:
62
74
  - `browser_query` (modes: `text`, `value`, `list`, `exists`, `page_text`; optional `timeoutMs`/`pollMs`)
63
75
  - `browser_click`
64
76
  - `browser_type`
77
+ - `browser_select`
65
78
  - `browser_scroll`
66
79
  - `browser_wait`
67
80
 
@@ -70,6 +83,14 @@ Diagnostics:
70
83
  - `browser_screenshot`
71
84
  - `browser_version`
72
85
 
86
+ ## Roadmap
87
+
88
+ - [ ] Add tab management tools (`browser_set_active_tab`, `browser_close_tab`)
89
+ - [ ] Add navigation helpers (`browser_back`, `browser_forward`, `browser_reload`)
90
+ - [ ] Add keyboard input tool (`browser_key`)
91
+ - [ ] Add download support (`browser_download`, `browser_list_downloads`)
92
+ - [ ] Add upload support (`browser_set_file_input`)
93
+
73
94
  ## Troubleshooting
74
95
 
75
96
  **Extension says native host not available**
package/bin/cli.js CHANGED
@@ -31,6 +31,7 @@ import { execSync } from "child_process";
31
31
  const __filename = fileURLToPath(import.meta.url);
32
32
  const __dirname = dirname(__filename);
33
33
  const PACKAGE_ROOT = join(__dirname, "..");
34
+ const PACKAGE_JSON = join(PACKAGE_ROOT, "package.json");
34
35
 
35
36
  const BASE_DIR = join(homedir(), ".opencode-browser");
36
37
  const EXTENSION_DIR = join(BASE_DIR, "extension");
@@ -108,6 +109,14 @@ function resolveNodePath() {
108
109
  return process.execPath;
109
110
  }
110
111
 
112
+ function getPackageVersion() {
113
+ try {
114
+ const pkg = JSON.parse(readFileSync(PACKAGE_JSON, "utf-8"));
115
+ if (typeof pkg?.version === "string") return pkg.version;
116
+ } catch {}
117
+ return null;
118
+ }
119
+
111
120
  function writeHostWrapper(nodePath) {
112
121
  ensureDir(BASE_DIR);
113
122
  const script = `#!/bin/sh\n"${nodePath}" "${NATIVE_HOST_DST}"\n`;
@@ -293,6 +302,24 @@ async function install() {
293
302
  ensureDir(BASE_DIR);
294
303
  const srcExtensionDir = join(PACKAGE_ROOT, "extension");
295
304
  copyDirRecursive(srcExtensionDir, EXTENSION_DIR);
305
+
306
+ const packageVersion = getPackageVersion();
307
+ if (packageVersion) {
308
+ const manifestPath = join(EXTENSION_DIR, "manifest.json");
309
+ if (existsSync(manifestPath)) {
310
+ try {
311
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
312
+ if (manifest.version !== packageVersion) {
313
+ manifest.version = packageVersion;
314
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
315
+ success(`Updated extension manifest version to ${packageVersion}`);
316
+ }
317
+ } catch (e) {
318
+ warn(`Could not update extension manifest: ${e.message || String(e)}`);
319
+ }
320
+ }
321
+ }
322
+
296
323
  success(`Extension files copied to: ${EXTENSION_DIR}`);
297
324
 
298
325
  header("Step 3: Load & Pin Extension");
package/dist/plugin.js CHANGED
@@ -12562,6 +12562,25 @@ var plugin = async (ctx) => {
12562
12562
  return toolResultText(data, `Typed "${text}" into ${selector}`);
12563
12563
  }
12564
12564
  }),
12565
+ browser_select: tool({
12566
+ description: "Select an option in a native select element",
12567
+ args: {
12568
+ selector: schema.string(),
12569
+ value: schema.string().optional(),
12570
+ label: schema.string().optional(),
12571
+ optionIndex: schema.number().optional(),
12572
+ index: schema.number().optional(),
12573
+ tabId: schema.number().optional()
12574
+ },
12575
+ async execute({ selector, value, label, optionIndex, index, tabId }, ctx2) {
12576
+ const data = await brokerRequest("tool", {
12577
+ tool: "select",
12578
+ args: { selector, value, label, optionIndex, index, tabId }
12579
+ });
12580
+ const summary = value ?? label ?? (optionIndex != null ? String(optionIndex) : "option");
12581
+ return toolResultText(data, `Selected ${summary} in ${selector}`);
12582
+ }
12583
+ }),
12565
12584
  browser_screenshot: tool({
12566
12585
  description: "Take a screenshot of the current page. Returns base64 image data URL.",
12567
12586
  args: {
@@ -104,6 +104,7 @@ async function executeTool(toolName, args) {
104
104
  navigate: toolNavigate,
105
105
  click: toolClick,
106
106
  type: toolType,
107
+ select: toolSelect,
107
108
  screenshot: toolScreenshot,
108
109
  snapshot: toolSnapshot,
109
110
  query: toolQuery,
@@ -253,6 +254,12 @@ async function pageOps(command, args) {
253
254
  return false
254
255
  }
255
256
 
257
+ function setSelectValue(el, value) {
258
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, "value")?.set
259
+ if (setter) setter.call(el, value)
260
+ else el.value = value
261
+ }
262
+
256
263
  function getInputValues() {
257
264
  const out = []
258
265
  const nodes = document.querySelectorAll("input, textarea")
@@ -382,6 +389,66 @@ async function pageOps(command, args) {
382
389
  return { ok: false, error: `Element is not typable: ${match.selectorUsed} (${tag.toLowerCase()})` }
383
390
  }
384
391
 
392
+ if (command === "select") {
393
+ const value = typeof options.value === "string" ? options.value : null
394
+ const label = typeof options.label === "string" ? options.label : null
395
+ const optionIndex = Number.isFinite(options.optionIndex) ? options.optionIndex : null
396
+ const match = resolveMatches(selectors, index)
397
+ if (!match.chosen) {
398
+ return { ok: false, error: `Element not found for selectors: ${selectors.join(", ")}` }
399
+ }
400
+
401
+ const tag = match.chosen.tagName
402
+ if (tag !== "SELECT") {
403
+ return { ok: false, error: `Element is not a select: ${match.selectorUsed} (${tag.toLowerCase()})` }
404
+ }
405
+
406
+ if (value === null && label === null && optionIndex === null) {
407
+ return { ok: false, error: "value, label, or optionIndex is required" }
408
+ }
409
+
410
+ const selectEl = match.chosen
411
+ const optionList = Array.from(selectEl.options || [])
412
+ let option = null
413
+
414
+ if (value !== null) {
415
+ option = optionList.find((opt) => opt.value === value)
416
+ }
417
+
418
+ if (!option && label !== null) {
419
+ const target = label.trim()
420
+ option = optionList.find((opt) => (opt.label || opt.textContent || "").trim() === target)
421
+ }
422
+
423
+ if (!option && optionIndex !== null) {
424
+ option = optionList[optionIndex]
425
+ }
426
+
427
+ if (!option) {
428
+ return { ok: false, error: "Option not found" }
429
+ }
430
+
431
+ try {
432
+ selectEl.scrollIntoView({ block: "center", inline: "center" })
433
+ } catch {}
434
+
435
+ try {
436
+ selectEl.focus()
437
+ } catch {}
438
+
439
+ setSelectValue(selectEl, option.value)
440
+ option.selected = true
441
+ selectEl.dispatchEvent(new Event("input", { bubbles: true }))
442
+ selectEl.dispatchEvent(new Event("change", { bubbles: true }))
443
+
444
+ return {
445
+ ok: true,
446
+ selectorUsed: match.selectorUsed,
447
+ value: selectEl.value,
448
+ label: (option.label || option.textContent || "").trim(),
449
+ }
450
+ }
451
+
385
452
  if (command === "scroll") {
386
453
  const scrollX = Number.isFinite(options.x) ? options.x : 0
387
454
  const scrollY = Number.isFinite(options.y) ? options.y : 0
@@ -540,6 +607,22 @@ async function toolType({ selector, text, tabId, clear = false, index = 0 }) {
540
607
  return { tabId: tab.id, content: `Typed "${text}" into ${used}` }
541
608
  }
542
609
 
610
+ async function toolSelect({ selector, value, label, optionIndex, tabId, index = 0 }) {
611
+ if (!selector) throw new Error("Selector is required")
612
+ if (value === undefined && label === undefined && optionIndex === undefined) {
613
+ throw new Error("value, label, or optionIndex is required")
614
+ }
615
+ const tab = await getTabById(tabId)
616
+
617
+ const result = await runInPage(tab.id, "select", { selector, value, label, optionIndex, index })
618
+ if (!result?.ok) throw new Error(result?.error || "Select failed")
619
+ const used = result.selectorUsed || selector
620
+ const valueText = result.value ? String(result.value) : ""
621
+ const labelText = result.label ? String(result.label) : ""
622
+ const summary = labelText && valueText && labelText !== valueText ? `${labelText} (${valueText})` : labelText || valueText
623
+ return { tabId: tab.id, content: `Selected ${summary || "option"} in ${used}` }
624
+ }
625
+
543
626
  async function toolScreenshot({ tabId }) {
544
627
  const tab = await getTabById(tabId)
545
628
  const png = await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" })
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCode Browser Automation",
4
- "version": "4.3.0",
4
+ "version": "4.3.2",
5
5
  "description": "Browser automation for OpenCode",
6
6
  "permissions": [
7
7
  "tabs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@different-ai/opencode-browser",
3
- "version": "4.3.0",
3
+ "version": "4.3.2",
4
4
  "description": "Browser automation plugin for OpenCode (native messaging + per-tab ownership).",
5
5
  "type": "module",
6
6
  "bin": {