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.
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Decide what browser the bridge should talk to, before it starts.
3
+ *
4
+ * The constraint that shapes all of this: --remote-debugging-port is a
5
+ * startup-only flag. A browser the user opened normally cannot be attached to,
6
+ * ever. So there is no way to "connect to the Opera that is already open" —
7
+ * only ways to arrange that the open Opera was started with a port in the first
8
+ * place, and a way to detect it when it was.
9
+ *
10
+ * That gives three states for a configured profile:
11
+ *
12
+ * free → let opera-devtools-mcp launch it, as before.
13
+ * locked, debug port live → attach. No prompt, no restart, nothing to do.
14
+ * locked, no debug port → a conflict only the user can resolve, by
15
+ * letting us restart their browser.
16
+ *
17
+ * The second case is the one that makes this feel automatic: once a browser has
18
+ * been started with a port — by us, or by the user following `launch-args` —
19
+ * every later command finds it on its own via DevToolsActivePort.
20
+ */
21
+ import { spawn } from "node:child_process";
22
+ import { existsSync, unlinkSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import { findAttachableEndpoint, inspectProfileLock, probeDevToolsEndpoint, readDevToolsPort, } from "./profile.js";
25
+ export async function resolveBrowserTarget(ctx) {
26
+ // An explicit browser URL is the user telling us they manage the browser.
27
+ if (ctx.browserUrl) {
28
+ return { mode: "attach", url: ctx.browserUrl, note: "OPERA_CLI_BROWSER_URL" };
29
+ }
30
+ // No persistent profile means an isolated one, which nothing else can hold.
31
+ if (!ctx.userDataDir) {
32
+ return { mode: "managed", note: "isolated profile" };
33
+ }
34
+ // A live debug port wins outright: the browser is running and reachable, so
35
+ // there is no conflict to resolve regardless of what the lock says.
36
+ const attachable = await findAttachableEndpoint(ctx.userDataDir);
37
+ if (attachable !== null) {
38
+ return {
39
+ mode: "attach",
40
+ url: attachable.url,
41
+ note: `running ${attachable.identity.browser}`,
42
+ };
43
+ }
44
+ const lock = inspectProfileLock(ctx.userDataDir);
45
+ if (lock.state === "free") {
46
+ return { mode: "managed", note: "profile is free" };
47
+ }
48
+ return { mode: "conflict", userDataDir: ctx.userDataDir, lock };
49
+ }
50
+ // ---------------------------------------------------------------------------
51
+ // Takeover
52
+ // ---------------------------------------------------------------------------
53
+ function sleep(ms) {
54
+ return new Promise((r) => setTimeout(r, ms));
55
+ }
56
+ /**
57
+ * Ask a running browser to quit, and wait for it to let go of the profile.
58
+ *
59
+ * SIGTERM only. Chromium treats it as a clean shutdown — session saved, profile
60
+ * flushed — whereas SIGKILL risks a corrupted profile and loses the user's
61
+ * tabs. If it will not go, we say so rather than escalating: this is somebody's
62
+ * browser, and forcing it is not ours to decide.
63
+ */
64
+ export async function quitBrowser(lock, userDataDir, timeoutMs = 20_000) {
65
+ if (lock.pid === null)
66
+ return { ok: false, reason: "no-pid" };
67
+ try {
68
+ process.kill(lock.pid, "SIGTERM");
69
+ }
70
+ catch {
71
+ // Already gone between inspection and now — that is a success.
72
+ return { ok: true };
73
+ }
74
+ const deadline = Date.now() + timeoutMs;
75
+ while (Date.now() < deadline) {
76
+ await sleep(250);
77
+ if (inspectProfileLock(userDataDir).state === "free")
78
+ return { ok: true };
79
+ }
80
+ return { ok: false, reason: "timeout" };
81
+ }
82
+ /**
83
+ * Start a browser we can attach to, and that outlives us.
84
+ *
85
+ * `--remote-debugging-port=0` has Chromium pick a free port itself and record
86
+ * it in DevToolsActivePort. That satisfies two requirements at once: we never
87
+ * squat a predictable port like 9222, and the port is discoverable by every
88
+ * later command without being written to any config.
89
+ *
90
+ * The browser is detached deliberately. Having just restarted the user's
91
+ * browser, closing it again when the CLI's bridge stops would be a poor trade.
92
+ */
93
+ export async function launchAttachableBrowser(executablePath, userDataDir, extraArgs = [], timeoutMs = 30_000) {
94
+ if (!executablePath || !existsSync(executablePath)) {
95
+ return { ok: false, reason: "no-executable" };
96
+ }
97
+ // Chromium rewrites this on startup, but clearing it first means a stale port
98
+ // from a previous run can never be mistaken for the new browser's.
99
+ const portFile = join(userDataDir, "DevToolsActivePort");
100
+ try {
101
+ unlinkSync(portFile);
102
+ }
103
+ catch {
104
+ // Absent already — fine.
105
+ }
106
+ const args = [
107
+ "--remote-debugging-port=0",
108
+ // Explicit even though it is the default: the debug port must never be
109
+ // reachable from off-box.
110
+ "--remote-debugging-address=127.0.0.1",
111
+ `--user-data-dir=${userDataDir}`,
112
+ // We just took their browser away; give the tabs back.
113
+ "--restore-last-session",
114
+ ...extraArgs,
115
+ ];
116
+ let child;
117
+ try {
118
+ child = spawn(executablePath, args, { stdio: "ignore", detached: true });
119
+ }
120
+ catch (error) {
121
+ return {
122
+ ok: false,
123
+ reason: "spawn-failed",
124
+ detail: error instanceof Error ? error.message : String(error),
125
+ };
126
+ }
127
+ child.unref();
128
+ let spawnError = null;
129
+ child.on("error", (error) => {
130
+ spawnError = error.message;
131
+ });
132
+ const deadline = Date.now() + timeoutMs;
133
+ while (Date.now() < deadline) {
134
+ if (spawnError !== null) {
135
+ return { ok: false, reason: "spawn-failed", detail: spawnError };
136
+ }
137
+ const port = readDevToolsPort(userDataDir);
138
+ if (port !== null && (await probeDevToolsEndpoint(port)) !== null) {
139
+ return { ok: true, url: `http://127.0.0.1:${port}` };
140
+ }
141
+ await sleep(250);
142
+ }
143
+ return { ok: false, reason: "timeout" };
144
+ }
145
+ /**
146
+ * The flags a user needs to start Opera themselves so the CLI can attach.
147
+ *
148
+ * Deliberately not `--remote-allow-origins=*`: Chromium's default rejection of
149
+ * CDP WebSocket upgrades that carry an Origin header is what stops a web page
150
+ * from driving the browser, and this profile is logged into everything.
151
+ */
152
+ export function browserLaunchArgs(userDataDir) {
153
+ const args = [
154
+ "--remote-debugging-port=0",
155
+ "--remote-debugging-address=127.0.0.1",
156
+ ];
157
+ if (userDataDir)
158
+ args.push(`--user-data-dir=${userDataDir}`);
159
+ return args;
160
+ }
161
+ //# sourceMappingURL=browser-target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-target.js","sourceRoot":"","sources":["../../src/browser-target.ts"],"names":[],"mappings":"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"}
package/dist/src/cli.d.ts CHANGED
@@ -1,9 +1,10 @@
1
+ import { type ErrorCode, type BridgeStatus, type StopResult } from "./client.js";
1
2
  type CliStdout = Pick<NodeJS.WriteStream, "write">;
2
3
  export type MainOptions = {
3
4
  argv?: string[];
4
5
  stdout?: CliStdout;
5
6
  };
6
- export declare const TOP_HELP = "usage: opera-browser-cli [command] [args] [flags]\ncommands[41]:\n open <url>, snapshot, screenshot <path>, click @<uid>, fill @<uid> <text>,\n type <text>, press <key>, scroll <dir>, back, wait <ms|text>, eval <js>,\n run,\n hover @<uid>, drag @<from> @<to>, fillform @<uid>=<val>..., dialog <action>,\n upload @<uid> <path>, pages, newpage <url>, selectpage <id>, closepage <id>,\n resize <w> <h>, emulate, console, console-get <id>, network,\n network-get [id], lighthouse, perf-start, perf-stop,\n perf-insight <set> <name>, heap <path>, start, stop,\n chat [--model <id>] <prompt>, invoke-do <prompt>, make <prompt>,\n research <prompt>, models,\n setup, logs, doctor\n\nflags[2]:\n --help, -v/-V/--version\n\nenvironment:\n OPERA_CLI_HEADED Set to 1 to run Chrome in headed (visible) mode\n OPERA_CLI_CHROME_ARGS Whitespace-separated Chrome flags forwarded to the browser\n (no shell-style quoting; flags with spaces are not supported)\n e.g. \"--enable-gpu --ignore-gpu-blocklist\"\n OPERA_CLI_PORT Bridge server port (default: 9225)\n OPERA_CLI_BROWSER_URL Connect to an existing Chrome instance instead of launching one\n e.g. \"http://127.0.0.1:9222\"\n OPERA_CLI_USER_DATA_DIR Persistent Chrome profile directory (skips --isolated mode)\n e.g. \"/path/to/.chrome-profile\"\n OPERA_CLI_EXECUTABLE_PATH Path to a custom browser binary (e.g. Opera Neon)\n OPERA_CLI_ENABLE_HOOKS Set to 1 to auto-install session hooks on startup\n\n Environment variables can also be set in ~/.opera-browser-cli/config (KEY=VALUE, one per line).\n Run `opera-browser-cli setup` to configure interactively.\n\nopera ai:\n chat is available on any Opera browser. Use --model to select a model.\n Run \"models\" to list available models.\n invoke-do, make, and research require Opera Neon with an active sign-in.\n Run `opera-browser-cli setup` to configure the executable path, or set\n OPERA_CLI_EXECUTABLE_PATH=\"/Applications/Opera Neon.app/Contents/MacOS/Opera\".\n\ngpu:\n Headless Chrome cannot access hardware GPU on most Linux systems.\n For GPU-accelerated WebGL, use headed mode with GPU flags:\n OPERA_CLI_HEADED=1\n OPERA_CLI_CHROME_ARGS=\"--enable-gpu --ignore-gpu-blocklist\"\n For WebGPU, Vulkan must also be enabled (required for the Dawn backend):\n OPERA_CLI_CHROME_ARGS=\"--enable-gpu --ignore-gpu-blocklist --enable-unsafe-webgpu --enable-features=Vulkan\"\n\ntips:\n Pipe output through grep/head to extract specific data from large pages.\n";
7
+ export declare const TOP_HELP = "usage: opera-browser-cli [command] [args] [flags]\ncommands[46]:\n open <url>, snapshot, screenshot <path>, click @<uid>, fill @<uid> <text>,\n type <text>, press <key>, scroll <dir>, back, wait <ms|text>, eval <js>,\n run,\n hover @<uid>, drag @<from> @<to>, fillform @<uid>=<val>..., dialog <action>,\n upload @<uid> <path>, pages, newpage <url>, selectpage <id>, closepage <id>,\n resize <w> <h>, emulate, console, console-get <id>, network,\n network-get [id], lighthouse, perf-start, perf-stop,\n perf-insight <set> <name>, heap <path>, start, stop, restart, status,\n attach, launch-args, login,\n chat [--model <id>] <prompt>, invoke-do <prompt>, make <prompt>,\n research <prompt>, models,\n setup, logs, doctor\n\nexit codes:\n 0 ok 2 bad arguments 3 environment not ready 4 sign-in required\n 5 timed out (retry) 6 stale page ref (re-snapshot) 1 other\n\nflags[3]:\n --help, -v/-V/--version, --takeover\n\nenvironment:\n OPERA_CLI_HEADED Set to 1 to run Chrome in headed (visible) mode\n OPERA_CLI_CHROME_ARGS Whitespace-separated Chrome flags forwarded to the browser\n (no shell-style quoting; flags with spaces are not supported)\n e.g. \"--enable-gpu --ignore-gpu-blocklist\"\n OPERA_CLI_PORT Base bridge port (default: 9225); the next 9 ports are\n tried in turn if it is occupied\n OPERA_CLI_BROWSER_URL Connect to an existing Chrome instance instead of launching one\n e.g. \"http://127.0.0.1:9222\"\n OPERA_CLI_USER_DATA_DIR Persistent Chrome profile directory (skips --isolated mode)\n e.g. \"/path/to/.chrome-profile\"\n OPERA_CLI_EXECUTABLE_PATH Path to a custom browser binary (e.g. Opera Neon)\n OPERA_CLI_ENABLE_HOOKS Set to 1 to auto-install session hooks on startup\n OPERA_CLI_TAKEOVER Set to 1 to allow restarting a running Opera without\n asking (same as the --takeover flag)\n\n Environment variables can also be set in ~/.opera-browser-cli/config (KEY=VALUE, one per line).\n Run `opera-browser-cli setup` to configure interactively.\n\nopera ai:\n chat is available on any Opera browser. Use --model to select a model.\n Run \"models\" to list available models.\n invoke-do, make, and research require Opera Neon with an active sign-in.\n Run `opera-browser-cli setup` to configure the executable path, or set\n OPERA_CLI_EXECUTABLE_PATH=\"/Applications/Opera Neon.app/Contents/MacOS/Opera\".\n\ngpu:\n Headless Chrome cannot access hardware GPU on most Linux systems.\n For GPU-accelerated WebGL, use headed mode with GPU flags:\n OPERA_CLI_HEADED=1\n OPERA_CLI_CHROME_ARGS=\"--enable-gpu --ignore-gpu-blocklist\"\n For WebGPU, Vulkan must also be enabled (required for the Dawn backend):\n OPERA_CLI_CHROME_ARGS=\"--enable-gpu --ignore-gpu-blocklist --enable-unsafe-webgpu --enable-features=Vulkan\"\n\ntips:\n Pipe output through grep/head to extract specific data from large pages.\n";
7
8
  export declare function getCommandHelp(command: string): string | null;
8
9
  export interface ScreenshotArgs {
9
10
  filePath: string | null;
@@ -61,9 +62,59 @@ export declare function parsePerfStartArgs(args: string[]): {
61
62
  autoStop?: boolean;
62
63
  filePath?: string;
63
64
  };
65
+ /**
66
+ * Exit codes, so a caller can branch on *why* something failed without parsing
67
+ * the message. Documented in README.md and SKILL.md — treat as a contract.
68
+ *
69
+ * 2 fix the command 3 environment not ready 4 ask the user
70
+ * 5 retry later 6 page state moved; re-snapshot
71
+ */
72
+ export declare const EXIT_CODES: Record<ErrorCode, number>;
73
+ export declare function exitCodeForCdpError(error: unknown): number;
74
+ export declare function formatCliError(error: unknown): {
75
+ output: string;
76
+ exitCode: number;
77
+ };
64
78
  /** Wrap plain JS expressions for MCP evaluate_script, but pass functions through unchanged. */
65
79
  export declare function wrapJsExpression(js: string): string;
66
- export declare function formatStopOutput(wasStopped: boolean): string;
80
+ export declare function formatStopOutput(result: StopResult): string;
81
+ export declare function formatStatusOutput(status: BridgeStatus): string;
82
+ export interface SetupArgs {
83
+ interactive: boolean;
84
+ executable: string | undefined;
85
+ profile: string | undefined;
86
+ headed: boolean | undefined;
87
+ }
88
+ export declare function parseSetupArgs(args: string[]): SetupArgs;
89
+ export declare function parseAttachArgs(args: string[]): {
90
+ port: number | null;
91
+ clear: boolean;
92
+ };
93
+ export declare function parseLogsArgs(args: string[]): {
94
+ lines: number;
95
+ follow: boolean;
96
+ errorsOnly: boolean;
97
+ };
98
+ export declare function filterLogLines(lines: string[], errorsOnly: boolean): string[];
99
+ /**
100
+ * Pre-flight check for AI commands. Fails fast if Opera Neon is clearly
101
+ * not configured, so we don't pay the 30s bridge-startup tax just to surface
102
+ * a confusing protocol error.
103
+ *
104
+ * Skipped when OPERA_CLI_BROWSER_URL is set — the user manages the browser
105
+ * themselves and presumably knows it's Opera Neon.
106
+ */
107
+ export type BrowserKind = "neon" | "opera" | "other" | "unknown";
108
+ /**
109
+ * What kind of browser we are about to drive.
110
+ *
111
+ * The old check only asked whether the configured path existed, which cannot
112
+ * tell Neon from Opera from Chrome — so it passed in exactly the two cases that
113
+ * fail: a plain Opera (no invoke-do/make/research) and a non-Opera browser
114
+ * (no Opera AI at all). Attached browsers report their real identity; launched
115
+ * ones are identified by their build, which is how Opera names its binaries.
116
+ */
117
+ export declare function classifyBrowser(executablePath: string | undefined, attachedBrowser?: string | undefined): BrowserKind;
67
118
  declare const VALID_RESEARCH_TYPES: readonly ["local", "one-minute", "deep"];
68
119
  type ResearchType = (typeof VALID_RESEARCH_TYPES)[number];
69
120
  export declare function parseChatArgs(args: string[]): {
@@ -74,5 +125,24 @@ export declare function parseResearchArgs(args: string[]): {
74
125
  prompt: string;
75
126
  researchType?: ResearchType;
76
127
  };
128
+ /**
129
+ * Work out which browser this command should drive, before the bridge starts.
130
+ *
131
+ * Runs in the CLI rather than the bridge because resolving a conflict may need
132
+ * to ask the user something, and the bridge is detached with no terminal.
133
+ *
134
+ * This runs even when a bridge is already alive. A bridge fixes its browser
135
+ * (attach URL, profile, flags) at startup, so a healthy bridge is only "the
136
+ * question is settled" while it is still driving the right browser. The case
137
+ * that must never be silently skipped is a conflict: the user's own Opera is
138
+ * running on the configured profile without a debug port. That used to be
139
+ * bypassed whenever any bridge was running, so the restart prompt never fired
140
+ * and the CLI kept driving a stale headless / separate-profile browser.
141
+ */
142
+ export declare function preflightBrowser(argv: string[], takeover: boolean): Promise<void>;
143
+ export declare function extractTakeoverFlag(argv: string[]): {
144
+ argv: string[];
145
+ takeover: boolean;
146
+ };
77
147
  export declare function main(options?: MainOptions | string[]): Promise<void>;
78
148
  export {};