@different-ai/opencode-browser 4.2.2 → 4.2.4

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.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: browser-automation
3
+ description: Reliable, composable browser automation using minimal OpenCode Browser primitives.
4
+ license: MIT
5
+ compatibility: opencode
6
+ metadata:
7
+ audience: agents
8
+ domain: browser
9
+ ---
10
+
11
+ ## What I do
12
+
13
+ - Provide a safe, composable workflow for browsing tasks
14
+ - Use `browser_query` list and index selection to click reliably
15
+ - Confirm state changes after each action
16
+
17
+ ## Best-practice workflow
18
+
19
+ 1. Inspect tabs with `browser_get_tabs`
20
+ 2. Navigate with `browser_navigate` if needed
21
+ 3. Wait for UI using `browser_query` with `timeoutMs`
22
+ 4. Discover candidates using `browser_query` with `mode=list`
23
+ 5. Click or type using `index`
24
+ 6. Confirm using `browser_query` or `browser_snapshot`
25
+
26
+ ## Query modes
27
+
28
+ - `text`: read visible text from a matched element
29
+ - `value`: read input values
30
+ - `list`: list many matches with text/metadata
31
+ - `exists`: check presence and count
32
+ - `page_text`: extract visible page text
33
+
34
+ ## Troubleshooting
35
+
36
+ - If a selector fails, run `browser_query` with `mode=page_text` to confirm the content exists
37
+ - Use `mode=list` on broad selectors (`button`, `a`, `*[role="button"]`) and choose by index
38
+ - Confirm results after each action
package/bin/cli.js CHANGED
@@ -275,13 +275,6 @@ Find it at ${color("cyan", "chrome://extensions")}:
275
275
 
276
276
  header("Step 7: Configure OpenCode");
277
277
 
278
- // OpenCode config discovery (per upstream docs):
279
- // - $HOME/.opencode.json
280
- // - $XDG_CONFIG_HOME/opencode/.opencode.json
281
- // - ./.opencode.json (project-local)
282
- // We write the project-local config to avoid touching global state.
283
- const opencodeJsonPath = join(process.cwd(), ".opencode.json");
284
-
285
278
  const desiredPlugin = "@different-ai/opencode-browser";
286
279
 
287
280
  function normalizePlugins(val) {
@@ -290,51 +283,128 @@ Find it at ${color("cyan", "chrome://extensions")}:
290
283
  return [];
291
284
  }
292
285
 
293
- function removeLegacyMcp(config) {
294
- if (config.mcp?.browser) {
295
- delete config.mcp.browser;
296
- if (Object.keys(config.mcp).length === 0) delete config.mcp;
297
- warn("Removed old MCP browser config (replaced by plugin)");
298
- }
299
- if (config.mcpServers?.browser) {
300
- delete config.mcpServers.browser;
301
- if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
302
- warn("Removed old MCP browser config (replaced by plugin)");
286
+ function stripJsoncComments(contents) {
287
+ return contents
288
+ .replace(/\/\*[\s\S]*?\*\//g, "")
289
+ .replace(/^\s*\/\/.*$/gm, "");
290
+ }
291
+
292
+ function sanitizeJson(contents) {
293
+ return stripJsoncComments(contents).replace(/,\s*(\]|\})/g, "$1");
294
+ }
295
+
296
+ function findOpenCodeConfigPath(configDir) {
297
+ const jsoncPath = join(configDir, "opencode.jsonc");
298
+ if (existsSync(jsoncPath)) return jsoncPath;
299
+ const jsonPath = join(configDir, "opencode.json");
300
+ return jsonPath;
301
+ }
302
+
303
+ const configOptions = [
304
+ "1) Project (./opencode.json or opencode.jsonc)",
305
+ "2) Global (~/.config/opencode/opencode.json)",
306
+ "3) Custom path",
307
+ "4) Skip (does nothing)",
308
+ ];
309
+
310
+ log(`\n${configOptions.join("\n")}`);
311
+ const selection = await ask("Choose config location [1-4]: ");
312
+
313
+ let configPath = null;
314
+ let configDir = null;
315
+
316
+ if (selection === "1") {
317
+ configDir = process.cwd();
318
+ configPath = findOpenCodeConfigPath(configDir);
319
+ } else if (selection === "2") {
320
+ const xdgConfig = process.env.XDG_CONFIG_HOME;
321
+ configDir = xdgConfig ? join(xdgConfig, "opencode") : join(homedir(), ".config", "opencode");
322
+ configPath = findOpenCodeConfigPath(configDir);
323
+ } else if (selection === "3") {
324
+ const customPath = await ask("Enter full path to opencode.json or opencode.jsonc: ");
325
+ if (customPath) {
326
+ configPath = customPath;
327
+ configDir = dirname(customPath);
328
+ } else {
329
+ warn("No path provided. Skipping OpenCode config.");
303
330
  }
331
+ } else if (selection === "4") {
332
+ warn("Skipping OpenCode config (does nothing).");
333
+ } else {
334
+ warn("Invalid selection. Skipping OpenCode config.");
304
335
  }
305
336
 
306
- if (existsSync(opencodeJsonPath)) {
307
- const shouldUpdate = await confirm("Found .opencode.json. Add plugin automatically?");
337
+ if (configPath && configDir) {
338
+ const hasExistingConfig = existsSync(configPath);
339
+ const shouldUpdate = hasExistingConfig
340
+ ? await confirm(`Found ${configPath}. Add plugin automatically?`)
341
+ : await confirm(`No config found at ${configPath}. Create one?`);
342
+
308
343
  if (shouldUpdate) {
309
344
  try {
310
- const config = JSON.parse(readFileSync(opencodeJsonPath, "utf-8"));
345
+ let config = { $schema: "https://opencode.ai/config.json", plugin: [] };
346
+ let canWriteConfig = true;
347
+
348
+ if (hasExistingConfig) {
349
+ const rawConfig = readFileSync(configPath, "utf-8");
350
+ try {
351
+ config = JSON.parse(sanitizeJson(rawConfig));
352
+ } catch (e) {
353
+ error(`Failed to parse ${configPath}: ${e.message}`);
354
+ const shouldOverwrite = await confirm("Config is invalid JSON. Back up and recreate it?");
355
+ if (shouldOverwrite) {
356
+ const backupPath = `${configPath}.bak-${Date.now()}`;
357
+ writeFileSync(backupPath, rawConfig);
358
+ warn(`Backed up invalid config to ${backupPath}`);
359
+ config = { $schema: "https://opencode.ai/config.json", plugin: [] };
360
+ } else {
361
+ canWriteConfig = false;
362
+ }
363
+ }
364
+ }
365
+
366
+ if (canWriteConfig) {
367
+ config.plugin = normalizePlugins(config.plugin);
368
+ if (!config.plugin.includes(desiredPlugin)) config.plugin.push(desiredPlugin);
369
+ if (typeof config.$schema !== "string") config.$schema = "https://opencode.ai/config.json";
370
+
371
+ ensureDir(configDir);
372
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
373
+ success(`Updated ${configPath} with plugin`);
374
+ } else {
375
+ warn(`Skipped updating ${configPath}. Fix JSON manually and rerun install.`);
376
+ }
377
+ } catch (e) {
378
+ error(`Failed to update ${configPath}: ${e.message}`);
379
+ }
380
+ }
381
+ }
311
382
 
312
- // Make sure plugin is an array.
313
- config.plugin = normalizePlugins(config.plugin);
314
- if (!config.plugin.includes(desiredPlugin)) config.plugin.push(desiredPlugin);
383
+ header("Step 8: Optional Agent Skill");
315
384
 
316
- removeLegacyMcp(config);
385
+ log(`
386
+ Agent Skills are reusable instructions discovered by OpenCode.
317
387
 
318
- // Ensure schema is correct if present.
319
- if (typeof config.$schema !== "string") config.$schema = "https://opencode.ai/config.json";
388
+ Format rules (summary):
389
+ - Place a skill at .opencode/skill/<name>/SKILL.md
390
+ - SKILL.md must start with YAML frontmatter with name + description
391
+ - name must match the directory and use: ^[a-z0-9]+(-[a-z0-9]+)*$
392
+ `);
320
393
 
321
- writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
322
- success("Updated .opencode.json with plugin");
323
- } catch (e) {
324
- error(`Failed to update .opencode.json: ${e.message}`);
325
- }
394
+ const skillName = "browser-automation";
395
+ const skillSrc = join(PACKAGE_ROOT, ".opencode", "skill", skillName, "SKILL.md");
396
+ const skillDstDir = join(process.cwd(), ".opencode", "skill", skillName);
397
+ const skillDst = join(skillDstDir, "SKILL.md");
398
+
399
+ if (existsSync(skillSrc)) {
400
+ const shouldAddSkill = await confirm(`Add ${skillName} skill to this repo?`);
401
+ if (shouldAddSkill) {
402
+ ensureDir(skillDstDir);
403
+ copyFileSync(skillSrc, skillDst);
404
+ success(`Added skill: ${skillDst}`);
326
405
  }
327
406
  } else {
328
- const shouldCreate = await confirm("No .opencode.json found. Create one?");
329
- if (shouldCreate) {
330
- const config = {
331
- $schema: "https://opencode.ai/config.json",
332
- theme: "opencode",
333
- plugin: [desiredPlugin],
334
- };
335
- writeFileSync(opencodeJsonPath, JSON.stringify(config, null, 2) + "\n");
336
- success("Created .opencode.json with plugin");
337
- }
407
+ warn("Skill template missing from package; skipping.");
338
408
  }
339
409
 
340
410
  header("Installation Complete!");
@@ -410,7 +480,7 @@ async function uninstall() {
410
480
  ${color("bright", "Note:")}
411
481
  - The unpacked extension folder remains at: ${EXTENSION_DIR}
412
482
  - Remove it manually in ${color("cyan", "chrome://extensions")}
413
- - Remove ${color("bright", "@different-ai/opencode-browser")} from your opencode.json plugin list if desired.
483
+ - Remove ${color("bright", "@different-ai/opencode-browser")} from your opencode.json/opencode.jsonc plugin list if desired.
414
484
  `);
415
485
  }
416
486
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "OpenCode Browser Automation",
4
- "version": "4.2.1",
4
+ "version": "4.2.4",
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.2.2",
3
+ "version": "4.2.4",
4
4
  "description": "Browser automation plugin for OpenCode (native messaging + per-tab ownership).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "./plugin": "./dist/plugin.js"
13
13
  },
14
14
  "files": [
15
+ ".opencode",
15
16
  "bin",
16
17
  "dist",
17
18
  "extension",