diff --git a/README.md b/README.md
index 6212ea9..6aa900f 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,31 @@
The most agent-ergonomic browser automation
+> [!IMPORTANT]
+> **This package has moved to [`opera-devtools-mcp`](https://github.com/operasoftware/opera-devtools-mcp).**
+> `opera-browser-cli` is no longer developed here; the CLI now ships inside `opera-devtools-mcp`,
+> still as the `opera-browser-cli` command.
+>
+> ```sh
+> npm install -g opera-browser-cli # or: npm update -g opera-browser-cli
+> ```
+>
+> That installs the **compatibility launcher** (`0.1.55`): the same package name and the same
+> command, delegating to the new implementation inside `opera-devtools-mcp`. No uninstall, no
+> `--force`, no changes to your scripts, and your `~/.opera-browser-cli/config` is reused.
+> Add the MCP server as its own global install if you want it too:
+> `npm install -g opera-devtools-mcp`.
+>
+> When you want to drop the launcher and keep a single global package:
+>
+> ```sh
+> npm install -g opera-devtools-mcp@0.9.0 opera-browser-cli@0.2.0
+> npm rm -g opera-browser-cli # optional: drops the inert tombstone
+> ```
+>
+> Existing `0.1.x` installs keep working; they are simply no longer maintained.
+> Run `opera-browser-cli doctor` for the same instructions on the installed CLI.
+
`opera-browser-cli` is a fork of [chrome-devtools-axi](https://github.com/kunchenguid/chrome-devtools-axi).
It wraps [opera-devtools-mcp](https://github.com/operasoftware/opera-devtools-mcp) with an [AXI](https://axi.md)-compliant CLI.
@@ -12,7 +37,7 @@ It wraps [opera-devtools-mcp](https://github.com/operasoftware/opera-devtools-mc
## Quick Start
```sh
-npm install -g opera-browser-cli
+npm install -g opera-browser-cli # installs the launcher that delivers the new implementation
opera-browser-cli open https://example.com
```
@@ -69,6 +94,10 @@ Prerequisites: **Node.js >= 20**, **Opera** browser ([Opera Neon](https://www.op
npm install -g opera-browser-cli
```
+Development has moved to `opera-devtools-mcp`, which ships this CLI as the
+`opera-browser-cli` command; this package name installs the compatibility
+launcher that delivers it. See the notice at the top of this file.
+
No setup step is required. The first command you run detects your Opera
installation, writes `~/.opera-browser-cli/config`, and continues:
diff --git a/launcher/README.md b/launcher/README.md
new file mode 100644
index 0000000..5bcf9ee
--- /dev/null
+++ b/launcher/README.md
@@ -0,0 +1,37 @@
+# opera-browser-cli (compatibility launcher)
+
+`opera-browser-cli` has moved into
+[`opera-devtools-mcp`](https://github.com/operasoftware/opera-devtools-mcp),
+which now ships both the MCP server and the `opera-browser-cli` command.
+
+This version is a **launcher**: it keeps the package name and the bin name so
+that the command you already run installs the new implementation —
+
+```sh
+npm i -g opera-browser-cli # or: npm update -g opera-browser-cli
+opera-browser-cli --version # the new implementation's version
+```
+
+— and delegates every invocation to `opera-devtools-mcp`, installed as its
+dependency. Nothing about the command line changes.
+
+When you are ready to drop the launcher and keep only one global package:
+
+```sh
+npm i -g opera-devtools-mcp@0.9.0 opera-browser-cli@0.2.0
+npm rm -g opera-browser-cli # optional: drops the inert tombstone
+```
+
+or, in two steps:
+
+```sh
+npm rm -g opera-browser-cli
+npm i -g opera-devtools-mcp@0.9.0
+```
+
+`opera-browser-cli --version` and `opera-browser-cli doctor` print the same
+recipes whenever the CLI notices it was launched from here.
+
+See
+[`docs/npm-package-transition.md`](https://github.com/operasoftware/opera-devtools-mcp/blob/main/docs/npm-package-transition.md)
+for the full transition, including what each release train publishes.
\ No newline at end of file
diff --git a/launcher/bin/cli.js b/launcher/bin/cli.js
new file mode 100755
index 0000000..fb8a8de
--- /dev/null
+++ b/launcher/bin/cli.js
@@ -0,0 +1,157 @@
+#!/usr/bin/env node
+
+/**
+ * `opera-browser-cli` compatibility launcher.
+ *
+ * The CLI moved into `opera-devtools-mcp`, but npm has no way to remove another
+ * package's global binstub except a **same-name upgrade**: it retires the
+ * previous version of the incoming package's name — unlinking its binstubs —
+ * before the new package's bin-conflict check runs. So this package keeps the
+ * old name and the old bin name and depends on the real implementation, which
+ * makes `npm i -g opera-browser-cli` / `npm update -g opera-browser-cli` — the
+ * command users already run — install the new CLI without `--force` and
+ * without a manual uninstall.
+ *
+ * The launcher only resolves and spawns; it never reimplements the CLI. The
+ * implementation entry is ESM (so it is spawned, not `require`d), and stdio is
+ * inherited for a zero-copy pass-through of TTYs and pipes.
+ *
+ * Marker it hands the child (read by `--version` and `doctor` in
+ * `opera-devtools-mcp`, `src/opera/launcherNotice.ts`):
+ * OPERA_CLI_LAUNCHER=1 — this CLI came from the launcher
+ * OPERA_CLI_LAUNCHER_PREFIX= — the global prefix it resolved
+ *
+ * Retire the launcher when `opera-browser-cli@0.2.0` (a bin-less tombstone) and
+ * `opera-devtools-mcp@0.9.0` (dual bin) exist:
+ * npm i -g opera-devtools-mcp@0.9.0 opera-browser-cli@0.2.0
+ * npm rm -g opera-browser-cli # optional: drops the inert tombstone
+ */
+
+'use strict';
+
+const {execSync, spawnSync} = require('node:child_process');
+const fs = require('node:fs');
+const path = require('node:path');
+
+/** Where the implementation's CLI entry lives inside its package. */
+const CLI_ENTRY = path.join('build', 'src', 'bin', 'opera-browser-cli.js');
+
+/** What npm sets while it runs an install script, when it is set at all. */
+function envPrefix() {
+ const prefix = process.env.npm_config_prefix ?? process.env.NPM_CONFIG_PREFIX;
+ return prefix ? prefix : null;
+}
+
+/** The prefix npm installed this launcher under, asked of npm as a last resort. */
+function npmPrefix() {
+ try {
+ return (
+ execSync('npm config get prefix', {
+ encoding: 'utf8',
+ stdio: ['ignore', 'pipe', 'ignore'],
+ timeout: 5_000,
+ }).trim() || null
+ );
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * The prefix the launcher itself sits under, read from its own path: npm links
+ * `/bin/opera-browser-cli` to `/lib/node_modules//bin/cli.js`
+ * on POSIX and `/node_modules//bin/cli.js` on Windows. Deriving it
+ * costs nothing; asking npm would spawn a process on every single command.
+ */
+function ownPathPrefix() {
+ const packageDir = path.dirname(__dirname);
+ if (path.basename(packageDir) !== 'opera-browser-cli') {
+ return null;
+ }
+ const nodeModules = path.dirname(packageDir);
+ if (path.basename(nodeModules) !== 'node_modules') {
+ return null;
+ }
+ const parent = path.dirname(nodeModules);
+ return path.basename(parent) === 'lib' ? path.dirname(parent) : parent;
+}
+
+function globalPrefix() {
+ return ownPathPrefix() ?? envPrefix() ?? npmPrefix();
+}
+
+/**
+ * Where the implementation may be, best first: a sibling root is a package the
+ * user installed directly and is therefore the one they upgrade, while the
+ * nested copy is whatever `^0.8.0` resolved when *this* launcher was installed.
+ */
+function implementationCandidates() {
+ const candidates = [];
+ const prefix = globalPrefix();
+ if (prefix) {
+ candidates.push(
+ path.join(prefix, 'lib', 'node_modules', 'opera-devtools-mcp'),
+ path.join(prefix, 'node_modules', 'opera-devtools-mcp'),
+ );
+ }
+ try {
+ candidates.push(path.dirname(require.resolve('opera-devtools-mcp/package.json')));
+ } catch {
+ // No nested copy either: the candidates tried so far are what the error lists.
+ }
+ return candidates;
+}
+
+function resolveImplementation() {
+ const candidates = implementationCandidates();
+ const usable = candidates.find(dir => fs.existsSync(path.join(dir, CLI_ENTRY)));
+ if (usable) {
+ return usable;
+ }
+ process.stderr.write(
+ [
+ 'opera-browser-cli: the compatibility launcher cannot find the new implementation.',
+ ...candidates.map(dir => ` tried ${dir}`),
+ 'Install it directly, then retire this launcher:',
+ ' npm i -g opera-devtools-mcp@latest',
+ ' npm rm -g opera-browser-cli',
+ '',
+ ].join('\n'),
+ );
+ process.exit(1);
+}
+
+const prefix = globalPrefix();
+const result = spawnSync(
+ process.execPath,
+ [path.join(resolveImplementation(), CLI_ENTRY), ...process.argv.slice(2)],
+ {
+ stdio: 'inherit',
+ env: {
+ ...process.env,
+ OPERA_CLI_LAUNCHER: '1',
+ OPERA_CLI_LAUNCHER_PREFIX: prefix ?? '',
+ },
+ },
+);
+
+if (result.error) {
+ process.stderr.write(
+ `opera-browser-cli: could not start the new implementation: ${result.error.message}\n`,
+ );
+ process.exit(1);
+}
+
+if (result.signal) {
+ // A child killed by a signal (`Ctrl-C` sends SIGINT to the whole foreground
+ // process group) has no exit status, and `?? 1` would report the user's
+ // cancellation as a failure — visible to `set -e` and to CI runners. Re-raise
+ // it instead; the ones Windows cannot raise on itself fall through to 1.
+ try {
+ process.kill(process.pid, result.signal);
+ } catch {
+ // Unsupported signal: the exit code below is all that is left.
+ }
+}
+
+process.exit(result.status ?? 1);
\ No newline at end of file
diff --git a/launcher/package.json b/launcher/package.json
new file mode 100644
index 0000000..6d3291a
--- /dev/null
+++ b/launcher/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "opera-browser-cli",
+ "version": "0.1.55",
+ "description": "Compatibility launcher — delegates to opera-devtools-mcp",
+ "license": "MIT",
+ "bin": {
+ "opera-browser-cli": "./bin/cli.js"
+ },
+ "dependencies": {
+ "opera-devtools-mcp": "^0.8.0"
+ },
+ "files": [
+ "bin/"
+ ],
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+}
\ No newline at end of file
diff --git a/tombstone/README.md b/tombstone/README.md
new file mode 100644
index 0000000..b1f091d
--- /dev/null
+++ b/tombstone/README.md
@@ -0,0 +1,29 @@
+# opera-browser-cli is retired
+
+The CLI lives in
+[`opera-devtools-mcp`](https://github.com/operasoftware/opera-devtools-mcp)
+now — one package provides both the MCP server and the `opera-browser-cli`
+command.
+
+```sh
+npm rm -g opera-browser-cli
+npm i -g opera-devtools-mcp@latest
+```
+
+If you installed `opera-browser-cli@0.1.55`, that version was a **launcher**: it
+kept the package name so the command you already run delivered the new
+implementation, and it delegated every invocation to `opera-devtools-mcp`.
+
+This version (`0.2.0`) is the tombstone that retires the launcher. It has no
+bin and no dependencies; installing it is what lets npm unlink the launcher's
+global binstub on a same-name upgrade, which is the only way npm removes a
+foreign binstub without `--force`. Installing it *together with* the new MCP
+package does the whole move in one command:
+
+```sh
+npm i -g opera-devtools-mcp@0.9.0 opera-browser-cli@0.2.0
+npm rm -g opera-browser-cli # optional: drops this inert package
+```
+
+`opera-browser-cli --version` and `opera-browser-cli doctor` print both recipes
+whenever the CLI notices it was launched by the launcher.
\ No newline at end of file
diff --git a/tombstone/package.json b/tombstone/package.json
new file mode 100644
index 0000000..8194e24
--- /dev/null
+++ b/tombstone/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "opera-browser-cli",
+ "version": "0.2.0",
+ "description": "Retired. Use opera-devtools-mcp.",
+ "license": "MIT",
+ "files": [
+ "README.md"
+ ],
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+}
\ No newline at end of file