opendevbrowser 0.0.10 → 0.0.11

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,7 +4,7 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT)
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg?style=flat-square)](https://www.typescriptlang.org/)
6
6
  [![OpenCode Plugin](https://img.shields.io/badge/OpenCode-Plugin-green.svg?style=flat-square)](https://opencode.ai)
7
- [![Test Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen.svg?style=flat-square)](https://github.com/freshtechbro/opendevbrowser)
7
+ [![Test Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen.svg?style=flat-square)](https://registry.npmjs.org/opendevbrowser)
8
8
 
9
9
  > **Script-first browser automation for AI agents.** Snapshot → Refs → Actions.
10
10
 
@@ -40,6 +40,12 @@ Recommended (CLI, installs plugin + config + bundled skills + extension assets):
40
40
  npx opendevbrowser --full --global --no-prompt
41
41
  ```
42
42
 
43
+ Explicit flags (config + skills, no prompt):
44
+
45
+ ```bash
46
+ npx opendevbrowser --global --with-config --skills-global --no-prompt
47
+ ```
48
+
43
49
  Manual fallback (edit OpenCode config):
44
50
 
45
51
  ```json
@@ -106,6 +112,107 @@ Restart OpenCode, then run `opendevbrowser_status` to verify the plugin is loade
106
112
 
107
113
  ---
108
114
 
115
+ ## Tool Reference
116
+
117
+ OpenDevBrowser provides **30 tools** organized by category:
118
+
119
+ ### Session Management
120
+ | Tool | Description |
121
+ |------|-------------|
122
+ | `opendevbrowser_launch` | Launch managed Chrome session with optional profile |
123
+ | `opendevbrowser_connect` | Connect to existing Chrome CDP endpoint |
124
+ | `opendevbrowser_disconnect` | Disconnect browser session |
125
+ | `opendevbrowser_status` | Get session status and connection info |
126
+
127
+ ### Tab/Target Management
128
+ | Tool | Description |
129
+ |------|-------------|
130
+ | `opendevbrowser_targets_list` | List all browser tabs/targets |
131
+ | `opendevbrowser_target_use` | Switch to a specific tab by targetId |
132
+ | `opendevbrowser_target_new` | Open new tab (optionally with URL) |
133
+ | `opendevbrowser_target_close` | Close a tab by targetId |
134
+
135
+ ### Named Pages
136
+ | Tool | Description |
137
+ |------|-------------|
138
+ | `opendevbrowser_page` | Open or focus a named page (logical tab alias) |
139
+ | `opendevbrowser_list` | List all named pages in session |
140
+ | `opendevbrowser_close` | Close a named page |
141
+
142
+ ### Navigation & Interaction
143
+ | Tool | Description |
144
+ |------|-------------|
145
+ | `opendevbrowser_goto` | Navigate to URL |
146
+ | `opendevbrowser_wait` | Wait for load state or element |
147
+ | `opendevbrowser_snapshot` | Capture page accessibility tree with refs |
148
+ | `opendevbrowser_click` | Click element by ref |
149
+ | `opendevbrowser_type` | Type text into input by ref |
150
+ | `opendevbrowser_select` | Select dropdown option by ref |
151
+ | `opendevbrowser_scroll` | Scroll page or element |
152
+ | `opendevbrowser_run` | Execute multiple actions in sequence |
153
+
154
+ ### DOM Inspection
155
+ | Tool | Description |
156
+ |------|-------------|
157
+ | `opendevbrowser_dom_get_html` | Get outerHTML of element by ref |
158
+ | `opendevbrowser_dom_get_text` | Get innerText of element by ref |
159
+
160
+ ### DevTools & Analysis
161
+ | Tool | Description |
162
+ |------|-------------|
163
+ | `opendevbrowser_console_poll` | Poll console logs since sequence |
164
+ | `opendevbrowser_network_poll` | Poll network requests since sequence |
165
+ | `opendevbrowser_screenshot` | Capture page screenshot |
166
+ | `opendevbrowser_perf` | Get page performance metrics |
167
+ | `opendevbrowser_prompting_guide` | Get best-practice prompting guidance |
168
+
169
+ ### Export & Cloning
170
+ | Tool | Description |
171
+ |------|-------------|
172
+ | `opendevbrowser_clone_page` | Export page as React component + CSS |
173
+ | `opendevbrowser_clone_component` | Export element subtree as React component |
174
+
175
+ ### Skills
176
+ | Tool | Description |
177
+ |------|-------------|
178
+ | `opendevbrowser_skill_list` | List available skills |
179
+ | `opendevbrowser_skill_load` | Load a skill by name (with optional topic filter) |
180
+
181
+ ---
182
+
183
+ ## Bundled Skills
184
+
185
+ OpenDevBrowser includes **5 task-specific skill packs**:
186
+
187
+ | Skill | Purpose |
188
+ |-------|---------|
189
+ | `opendevbrowser-best-practices` | Core prompting patterns and workflow guidance |
190
+ | `opendevbrowser-continuity-ledger` | Long-running task state management |
191
+ | `login-automation` | Authentication flow patterns |
192
+ | `form-testing` | Form validation and submission workflows |
193
+ | `data-extraction` | Structured data scraping patterns |
194
+
195
+ Skills are discovered from (priority order):
196
+ 1. `.opencode/skill/` (project)
197
+ 2. `~/.config/opencode/skill/` (global)
198
+ 3. `.claude/skills/` (compatibility)
199
+ 4. `~/.claude/skills/` (compatibility)
200
+ 5. Custom paths via `skillPaths` config
201
+
202
+ Load a skill: `opendevbrowser_skill_load` with `name` and optional `topic` filter.
203
+
204
+ ---
205
+
206
+ ## Browser Modes
207
+
208
+ | Mode | Tool | Use Case |
209
+ |------|------|----------|
210
+ | **Managed** | `opendevbrowser_launch` | Fresh browser, full control, automatic cleanup |
211
+ | **CDP Connect** | `opendevbrowser_connect` | Attach to existing Chrome with `--remote-debugging-port` |
212
+ | **Extension Relay** | Chrome Extension | Attach to logged-in tabs via relay server |
213
+
214
+ ---
215
+
109
216
  ## Chrome Extension (Optional)
110
217
 
111
218
  The extension enables **Mode C** - attach to existing logged-in browser tabs without launching a new browser.
@@ -114,17 +221,20 @@ The extension enables **Mode C** - attach to existing logged-in browser tabs wit
114
221
 
115
222
  The plugin and extension can automatically pair:
116
223
 
117
- 1. **Plugin side**: Auto-generates secure token on first run (saved to config)
224
+ 1. **Plugin side**: Starts a local relay server and config discovery endpoint
118
225
  2. **Extension side**: Enable "Auto-Pair" toggle and click Connect
119
- 3. Extension fetches token from plugin's relay server
226
+ 3. Extension fetches relay port from discovery, then fetches token from the relay server
120
227
  4. Connection established with color indicator (green = connected)
121
228
 
122
229
  ### Manual Setup
123
230
 
124
- 1. Install extension from Chrome Web Store or load unpacked from `~/.cache/opencode/opendevbrowser-extension/`
125
- 2. Open extension popup
126
- 3. Enter same port/token as plugin config
127
- 4. Click Connect
231
+ 1. Start OpenCode once so the plugin can extract the extension assets.
232
+ 2. Load unpacked from `~/.config/opencode/opendevbrowser/extension`
233
+ (fallback: `~/.cache/opencode/node_modules/opendevbrowser/extension`).
234
+ 3. Open extension popup
235
+ 4. Enter the same relay port and token as the plugin config
236
+ (if `relayToken` is missing, either add one to `opendevbrowser.jsonc` or use Auto-Pair).
237
+ 5. Click Connect
128
238
 
129
239
  ---
130
240
 
@@ -134,42 +244,56 @@ Optional config file: `~/.config/opencode/opendevbrowser.jsonc`
134
244
 
135
245
  ```jsonc
136
246
  {
247
+ // Browser settings
137
248
  "headless": false,
138
249
  "profile": "default",
139
250
  "persistProfile": true,
251
+ "chromePath": "/path/to/chrome", // Custom Chrome executable
252
+ "flags": ["--disable-extensions"], // Additional Chrome flags
253
+
254
+ // Snapshot limits
140
255
  "snapshot": { "maxChars": 16000, "maxNodes": 1000 },
256
+
257
+ // Export limits
141
258
  "export": { "maxNodes": 1000, "inlineStyles": true },
259
+
260
+ // DevTools output
142
261
  "devtools": { "showFullUrls": false, "showFullConsole": false },
262
+
263
+ // Security (all default false for safety)
143
264
  "security": {
144
265
  "allowRawCDP": false,
145
266
  "allowNonLocalCdp": false,
146
267
  "allowUnsafeExport": false
147
268
  },
269
+
270
+ // Skills configuration
271
+ "skills": {
272
+ "nudge": {
273
+ "enabled": true,
274
+ "keywords": ["form", "login", "extract", "scrape"],
275
+ "maxAgeMs": 60000
276
+ }
277
+ },
278
+ "skillPaths": ["./custom-skills"], // Additional skill directories
279
+
280
+ // Continuity ledger
148
281
  "continuity": {
149
282
  "enabled": true,
150
283
  "filePath": "opendevbrowser_continuity.md",
151
284
  "nudge": {
152
285
  "enabled": true,
153
- "keywords": [
154
- "plan",
155
- "multi-step",
156
- "multi step",
157
- "long-running",
158
- "long running",
159
- "refactor",
160
- "migration",
161
- "rollout",
162
- "release",
163
- "upgrade",
164
- "investigate",
165
- "follow-up",
166
- "continue"
167
- ],
286
+ "keywords": ["plan", "multi-step", "refactor", "migration"],
168
287
  "maxAgeMs": 60000
169
288
  }
170
289
  },
290
+
291
+ // Extension relay
171
292
  "relayPort": 8787,
172
- "relayToken": "auto-generated-on-first-run"
293
+ "relayToken": "auto-generated-on-first-run",
294
+
295
+ // Updates
296
+ "checkForUpdates": false
173
297
  }
174
298
  ```
175
299
 
@@ -214,7 +338,7 @@ rm -rf ~/.cache/opencode/node_modules/opendevbrowser
214
338
  npx opendevbrowser --update
215
339
  ```
216
340
 
217
- Release checklist: `docs/DISTRIBUTION_PLAN.md`
341
+ Release checklist: [docs/DISTRIBUTION_PLAN.md](docs/DISTRIBUTION_PLAN.md)
218
342
 
219
343
  ---
220
344
 
@@ -226,6 +350,8 @@ npm run build # Compile to dist/
226
350
  npm run test # Run tests with coverage
227
351
  npm run lint # ESLint checks
228
352
  npm run extension:build # Compile extension
353
+ npm run version:check # Verify package/extension version alignment
354
+ npm run extension:pack # Build extension zip for releases
229
355
  ```
230
356
 
231
357
  ---
package/dist/index.js CHANGED
@@ -2276,21 +2276,30 @@ function buildContinuityNudgeMessage(filePath) {
2276
2276
  import { createServer } from "http";
2277
2277
  import { timingSafeEqual } from "crypto";
2278
2278
  import { WebSocket, WebSocketServer } from "ws";
2279
+ var DEFAULT_DISCOVERY_PORT = 8787;
2280
+ var CONFIG_PATH = "/config";
2281
+ var PAIR_PATH = "/pair";
2279
2282
  var RelayServer = class _RelayServer {
2280
2283
  running = false;
2281
2284
  baseUrl = null;
2282
2285
  port = null;
2283
2286
  server = null;
2287
+ discoveryServer = null;
2284
2288
  extensionWss = null;
2285
2289
  cdpWss = null;
2286
2290
  extensionSocket = null;
2287
2291
  cdpSocket = null;
2288
2292
  extensionInfo = null;
2289
2293
  pairingToken = null;
2294
+ configuredDiscoveryPort;
2295
+ discoveryPort = null;
2290
2296
  handshakeAttempts = /* @__PURE__ */ new Map();
2291
2297
  cdpAllowlist = null;
2292
2298
  static MAX_HANDSHAKE_ATTEMPTS = 5;
2293
2299
  static RATE_LIMIT_WINDOW_MS = 6e4;
2300
+ constructor(options = {}) {
2301
+ this.configuredDiscoveryPort = options.discoveryPort ?? DEFAULT_DISCOVERY_PORT;
2302
+ }
2294
2303
  async start(port = 8787) {
2295
2304
  if (this.running && this.baseUrl && this.port !== null) {
2296
2305
  return { url: this.baseUrl, port: this.port };
@@ -2335,7 +2344,15 @@ var RelayServer = class _RelayServer {
2335
2344
  this.server.on("request", (request, response) => {
2336
2345
  const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
2337
2346
  const origin = request.headers.origin;
2338
- if (pathname === "/pair" && request.method === "OPTIONS") {
2347
+ if (pathname === CONFIG_PATH && request.method === "OPTIONS") {
2348
+ this.handleConfigPreflight(origin, response);
2349
+ return;
2350
+ }
2351
+ if (pathname === CONFIG_PATH && request.method === "GET") {
2352
+ this.handleConfigRequest(origin, response);
2353
+ return;
2354
+ }
2355
+ if (pathname === PAIR_PATH && request.method === "OPTIONS") {
2339
2356
  if (origin && origin.startsWith("chrome-extension://")) {
2340
2357
  response.setHeader("Access-Control-Allow-Origin", origin);
2341
2358
  response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
@@ -2345,7 +2362,7 @@ var RelayServer = class _RelayServer {
2345
2362
  response.end();
2346
2363
  return;
2347
2364
  }
2348
- if (pathname === "/pair" && request.method === "GET") {
2365
+ if (pathname === PAIR_PATH && request.method === "GET") {
2349
2366
  const isLocalhost = !origin || origin.startsWith("chrome-extension://");
2350
2367
  if (!isLocalhost) {
2351
2368
  response.writeHead(403, { "Content-Type": "application/json" });
@@ -2405,6 +2422,13 @@ var RelayServer = class _RelayServer {
2405
2422
  this.port = address.port;
2406
2423
  this.baseUrl = `ws://127.0.0.1:${address.port}`;
2407
2424
  this.running = true;
2425
+ try {
2426
+ await this.startDiscoveryServer();
2427
+ } catch (error) {
2428
+ const message = error instanceof Error ? error.message : String(error);
2429
+ console.warn(`[opendevbrowser] Discovery server failed to start: ${message}`);
2430
+ this.stopDiscoveryServer();
2431
+ }
2408
2432
  return { url: this.baseUrl, port: address.port };
2409
2433
  }
2410
2434
  stop() {
@@ -2412,6 +2436,7 @@ var RelayServer = class _RelayServer {
2412
2436
  this.baseUrl = null;
2413
2437
  this.port = null;
2414
2438
  this.extensionInfo = null;
2439
+ this.stopDiscoveryServer();
2415
2440
  if (this.extensionSocket) {
2416
2441
  this.extensionSocket.close(1e3, "Relay stopped");
2417
2442
  this.extensionSocket = null;
@@ -2440,6 +2465,12 @@ var RelayServer = class _RelayServer {
2440
2465
  getCdpUrl() {
2441
2466
  return this.baseUrl ? `${this.baseUrl}/cdp` : null;
2442
2467
  }
2468
+ getDiscoveryPort() {
2469
+ if (this.port !== null && this.port === this.configuredDiscoveryPort) {
2470
+ return this.port;
2471
+ }
2472
+ return this.discoveryPort;
2473
+ }
2443
2474
  setToken(token) {
2444
2475
  const trimmed = typeof token === "string" ? token.trim() : "";
2445
2476
  this.pairingToken = trimmed.length ? trimmed : null;
@@ -2460,6 +2491,81 @@ var RelayServer = class _RelayServer {
2460
2491
  }
2461
2492
  return false;
2462
2493
  }
2494
+ isExtensionOrigin(origin) {
2495
+ return Boolean(origin && origin.startsWith("chrome-extension://"));
2496
+ }
2497
+ handleConfigPreflight(origin, response) {
2498
+ if (this.isExtensionOrigin(origin)) {
2499
+ response.setHeader("Access-Control-Allow-Origin", origin);
2500
+ response.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
2501
+ response.setHeader("Access-Control-Allow-Headers", "Content-Type");
2502
+ }
2503
+ response.writeHead(204);
2504
+ response.end();
2505
+ }
2506
+ handleConfigRequest(origin, response) {
2507
+ if (!this.isExtensionOrigin(origin)) {
2508
+ response.writeHead(403, { "Content-Type": "application/json" });
2509
+ response.end(JSON.stringify({ error: "Forbidden: extension origin required" }));
2510
+ return;
2511
+ }
2512
+ if (origin) {
2513
+ response.setHeader("Access-Control-Allow-Origin", origin);
2514
+ }
2515
+ if (this.port === null) {
2516
+ response.writeHead(503, { "Content-Type": "application/json" });
2517
+ response.end(JSON.stringify({ error: "Relay not running" }));
2518
+ return;
2519
+ }
2520
+ response.writeHead(200, {
2521
+ "Content-Type": "application/json",
2522
+ "Cache-Control": "no-store"
2523
+ });
2524
+ response.end(JSON.stringify({
2525
+ relayPort: this.port,
2526
+ pairingRequired: Boolean(this.pairingToken)
2527
+ }));
2528
+ }
2529
+ async startDiscoveryServer() {
2530
+ if (this.port === null || this.discoveryServer) {
2531
+ return;
2532
+ }
2533
+ if (this.configuredDiscoveryPort > 0 && this.configuredDiscoveryPort === this.port) {
2534
+ return;
2535
+ }
2536
+ this.discoveryServer = createServer((request, response) => {
2537
+ const pathname = new URL(request.url ?? "", "http://127.0.0.1").pathname;
2538
+ const origin = request.headers.origin;
2539
+ if (pathname === CONFIG_PATH && request.method === "OPTIONS") {
2540
+ this.handleConfigPreflight(origin, response);
2541
+ return;
2542
+ }
2543
+ if (pathname === CONFIG_PATH && request.method === "GET") {
2544
+ this.handleConfigRequest(origin, response);
2545
+ return;
2546
+ }
2547
+ response.writeHead(404);
2548
+ response.end();
2549
+ });
2550
+ await new Promise((resolve, reject) => {
2551
+ this.discoveryServer?.once("error", reject);
2552
+ this.discoveryServer?.listen(this.configuredDiscoveryPort, "127.0.0.1", () => {
2553
+ resolve();
2554
+ });
2555
+ });
2556
+ const address = this.discoveryServer.address();
2557
+ if (!address) {
2558
+ throw new Error("Discovery server did not expose a port");
2559
+ }
2560
+ this.discoveryPort = address.port;
2561
+ }
2562
+ stopDiscoveryServer() {
2563
+ if (this.discoveryServer) {
2564
+ this.discoveryServer.close();
2565
+ this.discoveryServer = null;
2566
+ }
2567
+ this.discoveryPort = null;
2568
+ }
2463
2569
  isRateLimited(ip) {
2464
2570
  const now = Date.now();
2465
2571
  const record = this.handshakeAttempts.get(ip);