opera-browser-cli 0.1.45 → 0.1.46

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
@@ -13,10 +13,13 @@ It wraps [opera-devtools-mcp](https://github.com/operasoftware/opera-devtools-mc
13
13
 
14
14
  ```sh
15
15
  npm install -g opera-browser-cli
16
- opera-browser-cli setup # interactive wizard — run in a terminal where you can answer prompts
17
16
  opera-browser-cli open https://example.com
18
17
  ```
19
18
 
19
+ That is the whole setup. The first command detects your Opera installation,
20
+ writes `~/.opera-browser-cli/config`, and gets on with it. Run
21
+ `opera-browser-cli setup` only when you want to change what it chose.
22
+
20
23
  Once installed, `open` navigates to a URL and returns a structured snapshot you can act on:
21
24
 
22
25
  ```sh
@@ -64,21 +67,30 @@ Prerequisites: **Node.js >= 20**, **Opera** browser ([Opera Neon](https://www.op
64
67
  npm install -g opera-browser-cli
65
68
  ```
66
69
 
67
- Run first-time setup this is an interactive wizard, so run it in a terminal where you can answer prompts:
70
+ No setup step is required. The first command you run detects your Opera
71
+ installation, writes `~/.opera-browser-cli/config`, and continues:
68
72
 
69
73
  ```sh
70
- opera-browser-cli setup
74
+ opera-browser-cli --version
75
+ opera-browser-cli open https://example.com
71
76
  ```
72
77
 
73
- This detects Opera installations, lets you pick one, saves configuration to `~/.opera-browser-cli/config`, and installs the skill to `~/.claude/skills/opera-browser-cli/SKILL.md` (Claude Code) and `~/.agents/skills/opera-browser-cli/SKILL.md` (generic cross-agent path used by Codex and other agents).
74
-
75
- Verify:
78
+ `setup` exists for when you want to change that choice pick a different
79
+ browser or profile, or install the agent skill files:
76
80
 
77
81
  ```sh
78
- opera-browser-cli --version
79
- opera-browser-cli open https://example.com
82
+ opera-browser-cli setup # interactive wizard
83
+ opera-browser-cli setup -y # detect and accept, no prompts
84
+ opera-browser-cli setup --executable "/Applications/Opera Neon.app/Contents/MacOS/Opera" \
85
+ --profile skip --headless
80
86
  ```
81
87
 
88
+ It saves to `~/.opera-browser-cli/config` and installs the skill to
89
+ `~/.claude/skills/opera-browser-cli/SKILL.md` (Claude Code) and
90
+ `~/.agents/skills/opera-browser-cli/SKILL.md` (generic cross-agent path used by
91
+ Codex and other agents). The non-interactive form needs no terminal, so agents
92
+ and provisioning scripts can run it too.
93
+
82
94
  ### From source
83
95
 
84
96
  ```sh
@@ -86,7 +98,7 @@ opera-browser-cli open https://example.com
86
98
  npm install && npm run build && npm link
87
99
  ```
88
100
 
89
- Then run `opera-browser-cli setup` as above.
101
+ Then just run a command configuration happens on first use.
90
102
 
91
103
  ### Usage examples
92
104
 
@@ -127,7 +139,7 @@ OPERA_CLI_BROWSER_URL=http://127.0.0.1:9222 opera-browser-cli open https://examp
127
139
  ```
128
140
 
129
141
  - **Persistent bridge** — a detached process keeps the MCP session alive across commands, so Chrome doesn't restart every invocation
130
- - **Auto-lifecycle** — the bridge starts on first command and writes a PID file to `~/.opera-browser-cli/bridge.pid`
142
+ - **Auto-lifecycle** — the bridge starts on first command, writes a PID file to `~/.opera-browser-cli/bridge.pid`, and restarts itself on version skew or a dropped connection
131
143
  - **Snapshot parsing** — accessibility tree snapshots are extracted and analyzed for interactive elements (`uid=` refs)
132
144
  - **TOON encoding** — structured metadata uses [TOON format](https://www.npmjs.com/package/@toon-format/toon) for compact, token-efficient output
133
145
 
@@ -219,14 +231,83 @@ opera-browser-cli eval "(() => { const rows = [...document.querySelectorAll('tr'
219
231
  |----------|--------------------------------------------------|
220
232
  | `setup` | Interactive first-time setup (browser path, etc) |
221
233
  | `doctor` | Check configuration and environment |
234
+ | `doctor --fix` | Repair what can be repaired mechanically |
235
+ | `login` | Sign in to your Opera account (needed for AI) |
222
236
  | `logs` | Show bridge server logs |
223
237
 
238
+ ### Using your real Opera profile
239
+
240
+ By default the CLI launches its own browser. To drive **your** Opera — with your
241
+ logins, your session — the browser has to have been started with a debugging
242
+ port. That flag cannot be added to a browser that is already open, so there are
243
+ two ways in:
244
+
245
+ ```sh
246
+ opera-browser-cli launch-args # prints the command to start Opera with a port
247
+ ```
248
+
249
+ Start Opera that way once, and every later command finds it automatically — the
250
+ port is recorded in `DevToolsActivePort` inside the profile, so nothing needs
251
+ configuring. Or let the CLI do it for you:
252
+
253
+ ```sh
254
+ opera-browser-cli open example.com # detects the conflict, offers to restart Opera
255
+ opera-browser-cli open example.com --takeover # skip the prompt (scripts, agents)
256
+ ```
257
+
258
+ If Opera is already running on the configured profile and has no debugging port,
259
+ the CLI asks whether to restart it (tabs are restored). Without a terminal to ask
260
+ in, it quietly uses a separate profile instead — an agent will never quit your
261
+ browser on its own. Restarting is always SIGTERM, never SIGKILL: a forced kill
262
+ risks a corrupted profile.
263
+
264
+ ```sh
265
+ opera-browser-cli attach --port 9222 # connect to a specific endpoint
266
+ opera-browser-cli attach --clear # go back to a CLI-launched browser
267
+ ```
268
+
269
+ > **Note:** a debugging port has no authentication of its own — the CLI's bearer
270
+ > token protects the bridge, not the browser. Any local process can drive a
271
+ > browser with an open port, and this one is signed into everything you are. The
272
+ > CLI lets the browser pick a random port rather than a predictable 9222, binds
273
+ > it to loopback, and never passes `--remote-allow-origins`, which is what stops
274
+ > a web page from driving it. Close the browser when you are done.
275
+
224
276
  ### Bridge
225
277
 
226
- | Command | Description |
227
- |---------|-------------------------|
228
- | `start` | Start the bridge server |
229
- | `stop` | Stop the bridge server |
278
+ | Command | Description |
279
+ |-----------|--------------------------------------------------------------------|
280
+ | `start` | Start the bridge server |
281
+ | `stop` | Stop the bridge server (escalates to SIGKILL; clears a stale PID) |
282
+ | `restart` | Stop and start again — forces a clean state |
283
+ | `status` | Report bridge pid, port, and running version without starting one |
284
+
285
+ You should rarely need any of these. The bridge starts on first use, and repairs
286
+ itself without being asked:
287
+
288
+ - **Upgraded package** — a bridge running pre-upgrade code is detected by version
289
+ and replaced on the next command.
290
+ - **Crashed or killed bridge** — the next command restarts it and retries. Opera AI
291
+ commands are the exception: they are never silently re-run, since they may already
292
+ have acted on the page.
293
+ - **Port in use** — the next port in the range is used instead of failing.
294
+ - **Several commands at once** — a start lock means exactly one bridge comes up.
295
+ - **Stale PID file** — cleared automatically, and never signalled if the PID could
296
+ belong to an unrelated process from before a reboot.
297
+
298
+ ### Exit codes
299
+
300
+ Scripts and agents can branch on why a command failed without parsing messages:
301
+
302
+ | Code | Meaning | Caller action |
303
+ |---|---|---|
304
+ | 0 | Success | — |
305
+ | 1 | Unknown / internal | Report |
306
+ | 2 | Bad arguments, or unsupported on this browser | Fix the command |
307
+ | 3 | Environment not ready after auto-recovery | Run `doctor` |
308
+ | 4 | Sign-in, subscription, or consent required | Ask the user |
309
+ | 5 | Timed out | Retry |
310
+ | 6 | Stale element ref or closed page | Re-snapshot, then retry |
230
311
 
231
312
  Running with no command shows the CLI home view. It prepends `bin` and
232
313
  `description` metadata, then includes the current snapshot when a browser
@@ -265,14 +346,15 @@ session is active or the no-session status/help block when one is not.
265
346
 
266
347
  | Variable | Default | Purpose |
267
348
  |-----------------------------|----------------------------------|------------------------------------------------------------------|
268
- | `OPERA_CLI_PORT` | `9225` | Bridge server port |
349
+ | `OPERA_CLI_PORT` | `9225` | Base bridge port; the next 9 are tried if it is occupied |
269
350
  | `OPERA_CLI_MCP_BIN` | _(bundled `opera-devtools-mcp`)_ | Override the MCP server binary |
270
351
  | `OPERA_CLI_EXECUTABLE_PATH` | _(system Chrome)_ | Custom browser binary |
271
352
  | `OPERA_CLI_BROWSER_URL` | — | Connect to an existing browser instance instead of launching one |
272
353
  | `OPERA_CLI_USER_DATA_DIR` | — | Persistent Chrome profile directory (skips isolated mode) |
273
- | `OPERA_CLI_HEADED` | — | Set to `1` to run in headed (visible) mode |
354
+ | `OPERA_CLI_HEADED` | `1` when an Opera binary is configured | `1` headed, `0` headless. Opera AI needs a window to sign in |
274
355
  | `OPERA_CLI_CHROME_ARGS` | — | Extra Chrome flags, space-separated |
275
356
  | `OPERA_CLI_ENABLE_HOOKS` | — | Set to `1` to auto-install session hooks on startup |
357
+ | `OPERA_CLI_TAKEOVER` | — | Set to `1` to restart a running Opera without asking |
276
358
 
277
359
  State is stored in `~/.opera-browser-cli/`:
278
360
 
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: opera-browser-cli
3
- description: Browser automation and web interaction using the opera-browser-cli tool. Use for navigating pages, clicking elements, filling forms, taking screenshots, inspecting console/network, running performance audits, and Opera AI features (chat available on any Opera browser; invoke-do, make, research require Opera Neon).
3
+ description: Browser automation and web interaction using the opera-browser-cli tool. Use for navigating pages, clicking elements, filling forms, taking screenshots, inspecting console/network, running performance audits, and Opera AI features (chat available on any Opera browser; invoke-do, make, research require Opera Neon). When a browser is already running without automation enabled, this tool can restart it with a debug port (takeover) or use a separate profile — ask the user which they prefer.
4
4
  metadata: {"openclaw": {"requires": {"bins": ["opera-browser-cli"]}}}
5
5
  ---
6
6
 
@@ -47,6 +47,35 @@ Commands that accept these flags: `open`, `snapshot`, `click`, `fill`, `type`, `
47
47
 
48
48
  To wire this CLI into a Docker-based OpenClaw setup (Chromium sidecar, shared netns, config bootstrap), see [`openclaw/README.md`](openclaw/README.md).
49
49
 
50
+ ## Exit codes
51
+
52
+ Branch on the exit code rather than parsing messages:
53
+
54
+ | Code | Meaning | What to do |
55
+ |---|---|---|
56
+ | 0 | Success | — |
57
+ | 2 | Bad arguments, or the browser cannot do this | Fix the command; do not retry as-is |
58
+ | 3 | Environment not ready after auto-recovery | Run `opera-browser-cli doctor` |
59
+ | 4 | Sign-in, subscription, or consent needed | Ask the user — you cannot fix this |
60
+ | 5 | Timed out | Retry |
61
+ | 6 | Stale element ref or closed page | Re-run `snapshot`, then retry with fresh refs |
62
+ | 1 | Anything else | Report it |
63
+
64
+ ## Recovery is automatic
65
+
66
+ The bridge restarts itself on version skew, a crash, or a dropped connection, and falls back to another port if one is taken. Do not run `stop`/`restart` speculatively — just re-run the command. The exception is the Opera AI tools (`invoke-do`, `make`, `research`, `chat`): if one reports the connection dropped mid-call, it was **not** retried, because it may already have acted. Ask before re-running it.
67
+
68
+ ## When the browser can't be automated
69
+
70
+ The CLI only drives a browser started with a debug port. If the user's Opera is already open **without** one, the CLI can't attach to that window — `open`/AI then fail with **"Could not connect to Chrome"**. Run `opera-browser-cli doctor` (it reports the profile state); the bridge self-heals, so don't restart it blindly.
71
+
72
+ **Always ask the user** — restarting their browser is their call, not a judgement you infer:
73
+ > "Your Opera is open but wasn't started with automation. May I restart it with a debug port (tabs restored)? Or should I use a separate profile (you'd sign in there)?"
74
+
75
+ - **They approve restart** → run with `--takeover` (or `OPERA_CLI_TAKEOVER=1`). Restarts with a debug port, restores tabs, attaches — drives the real browser thereafter.
76
+ - **They decline** → no flag → separate profile at `~/.opera-browser-cli/profile` (they sign in there; AI may then return exit `4` — surface it).
77
+ - `opera-browser-cli launch-args` prints the flags to start Opera attachable so a restart is never needed later.
78
+
50
79
  ## Sign-in errors
51
80
 
52
- If you hit `Opera: user is not signed in` on an AI command, suggest signing in to their Opera account. Run `opera-browser-cli setup` or `opera-browser-cli doctor` to configure or diagnose.
81
+ If an AI command fails with `AUTH_REQUIRED` (exit code 4) — not signed in, no subscription, or consent pending tell the user to run `opera-browser-cli login`, which opens the Opera account page in a visible window. `opera-browser-cli login --check` verifies the current state. Run `opera-browser-cli doctor` to diagnose anything else.
@@ -13,6 +13,7 @@
13
13
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
14
14
  import type { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
15
15
  import { type IncomingMessage, type Server, type ServerResponse } from "node:http";
16
+ import { type BridgeHealth } from "./identity.js";
16
17
  export interface LastSnapshotCache {
17
18
  raw: string;
18
19
  pageUrl: string | null;
@@ -44,10 +45,38 @@ export interface BridgeClient {
44
45
  close(): Promise<void>;
45
46
  }
46
47
  export declare function isBridgeClientConnected(client: BridgeClient): Promise<boolean>;
48
+ /**
49
+ * "Usable" health for the bridge: the MCP server is up AND, when the bridge is
50
+ * in attach mode, the browser at the attach URL is actually reachable.
51
+ *
52
+ * In attach mode devtools-mcp stays connected over stdio even when the browser
53
+ * it points at is gone (it reaches for the browser lazily on a tool call), so
54
+ * MCP liveness alone cannot tell that a bridge is wedged on a dead URL. Probing
55
+ * the attach endpoint directly keeps `/health` honest, which lets the CLI stop
56
+ * reusing a wedged bridge and rebuild it against the current target.
57
+ */
58
+ export declare function isBridgeHealthConnected(client: BridgeClient): Promise<boolean>;
47
59
  export declare function getErrorMessage(error: unknown): string;
48
60
  export declare function extractToolText(content: BridgeContentBlock[]): string;
49
61
  export declare function parseBridgeCallPayload(body: string): BridgeCallPayload;
50
62
  export declare function resolveBridgeScript(importMetaDir: string): string;
63
+ export type BridgeLauncher = {
64
+ ok: true;
65
+ command: string;
66
+ args: string[];
67
+ } | {
68
+ ok: false;
69
+ reason: string;
70
+ };
71
+ /**
72
+ * Decide how to launch the bridge process.
73
+ *
74
+ * Prefers the built JavaScript. The TypeScript entrypoint is used only in a
75
+ * source checkout (or under OPERA_CLI_DEV=1), and only when tsx is already
76
+ * installed — never via `npx`, which blocks on an install prompt when the
77
+ * package is uncached and would hang invisibly behind a redirected stdio.
78
+ */
79
+ export declare function resolveBridgeLauncher(importMetaDir: string, execPath?: string): BridgeLauncher;
51
80
  /** True when a Host/Origin host component (with optional port) is loopback. */
52
81
  export declare function isLoopbackHost(hostHeader: string | undefined): boolean;
53
82
  /** Browsers always attach Origin; the CLI never does. Absent = trusted; present = must be loopback. */
@@ -65,9 +94,48 @@ export interface BridgeAccessResult {
65
94
  */
66
95
  export declare function checkRequestAccess(req: IncomingMessage, token: string | null, requireToken: boolean): BridgeAccessResult;
67
96
  export declare function generateBridgeToken(): string;
97
+ export declare function buildHealth(connected: boolean): BridgeHealth;
68
98
  export declare function handleBridgeRequest(client: BridgeClient, req: IncomingMessage, res: ServerResponse, captureNextId?: () => Promise<string>, token?: string | null): Promise<void>;
69
- export declare function createBridgeServer(client: BridgeClient, captureNextId?: () => Promise<string>, token?: string | null): Server;
99
+ /**
100
+ * Build the HTTP server.
101
+ *
102
+ * `resolve` is called per request rather than the client being captured up
103
+ * front, so the port can be bound before the MCP connection exists. Until it
104
+ * returns a client every route answers 503 with a well-formed health body —
105
+ * which is exactly what a caller probing /health during startup should see.
106
+ */
107
+ export declare function createBridgeServer(resolve: () => {
108
+ client: BridgeClient | null;
109
+ captureNextId?: () => Promise<string>;
110
+ }, token?: string | null): Server;
111
+ /** Exit code signalling "port taken, try the next one" (EX_TEMPFAIL). */
112
+ export declare const EXIT_PORT_IN_USE = 75;
113
+ /**
114
+ * Whether to launch a visible browser.
115
+ *
116
+ * Headless is the safe default for a tool — it is the only thing that works on
117
+ * a server with no display. But a configured Opera binary means the user wants
118
+ * *their* browser, and every Opera AI feature needs a window: sign-in and
119
+ * consent cannot be completed headlessly, so a headless AI command fails on a
120
+ * state the user has no way to fix.
121
+ *
122
+ * So: headed when an Opera executable is configured, headless otherwise, and
123
+ * OPERA_CLI_HEADED=0 or =1 overrides either way. A machine with no Opera
124
+ * installed — CI, Docker, a plain-Chrome setup — keeps the old behaviour.
125
+ */
126
+ export declare function shouldRunHeaded(): boolean;
70
127
  export declare function buildTransportArgs(): string[];
128
+ export interface McpBinStatus {
129
+ bin: string;
130
+ found: boolean;
131
+ source: "env" | "dependency" | "path";
132
+ }
133
+ /**
134
+ * Where opera-devtools-mcp is coming from, and whether it is actually there.
135
+ * Used by `doctor` so a missing MCP server is named before it costs a failed
136
+ * bridge start.
137
+ */
138
+ export declare function resolveMcpBinStatus(): McpBinStatus;
71
139
  /**
72
140
  * Wraps transport.send to intercept outgoing JSON-RPC request IDs so each
73
141
  * streaming callTool call can register its own log writer before the first