From 10c1b4fa5a47b41246edb79041848c2304c12428 Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Mon, 25 May 2026 17:38:41 +0200 Subject: [PATCH 01/14] feat(cli): add parseChatArgs for --model flag Co-authored-by: Cursor --- src/cli.ts | 18 ++++++++++++++++++ test/cli.test.ts | 29 ++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index 63db721..f3d59cf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2267,6 +2267,24 @@ async function handleMake(args: string[]): Promise { const VALID_RESEARCH_TYPES = ["local", "one-minute", "deep"] as const; type ResearchType = (typeof VALID_RESEARCH_TYPES)[number]; +export function parseChatArgs(args: string[]): { + prompt: string; + model?: string; +} { + let model: string | undefined; + const promptParts: string[] = []; + for (let i = 0; i < args.length; i++) { + if (args[i] === "--model") { + if (i + 1 < args.length) { + model = args[++i]; + } + } else { + promptParts.push(args[i]); + } + } + return { prompt: promptParts.join(" "), model }; +} + export function parseResearchArgs(args: string[]): { prompt: string; researchType?: ResearchType; diff --git a/test/cli.test.ts b/test/cli.test.ts index 25f5468..a1705c1 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { formatStopOutput, formatScreenshotOutput, getCommandHelp, parseScreenshotArgs } from "../src/cli.js"; +import { formatStopOutput, formatScreenshotOutput, getCommandHelp, parseChatArgs, parseScreenshotArgs } from "../src/cli.js"; describe("formatStopOutput", () => { it("returns stopped status when bridge was running", () => { @@ -81,3 +81,30 @@ describe("formatScreenshotOutput", () => { expect(output).toContain("./shot.png"); }); }); + +describe("parseChatArgs", () => { + it("parses prompt only", () => { + const result = parseChatArgs(["Hello", "world"]); + expect(result).toEqual({ prompt: "Hello world", model: undefined }); + }); + + it("parses --model flag with prompt", () => { + const result = parseChatArgs(["--model", "gpt-4o", "What", "is", "this?"]); + expect(result).toEqual({ prompt: "What is this?", model: "gpt-4o" }); + }); + + it("parses --model at end of args", () => { + const result = parseChatArgs(["Hello", "--model", "claude-sonnet-4"]); + expect(result).toEqual({ prompt: "Hello", model: "claude-sonnet-4" }); + }); + + it("returns empty prompt when only --model is given", () => { + const result = parseChatArgs(["--model", "gpt-4o"]); + expect(result).toEqual({ prompt: "", model: "gpt-4o" }); + }); + + it("ignores --model without a value", () => { + const result = parseChatArgs(["Hello", "--model"]); + expect(result).toEqual({ prompt: "Hello", model: undefined }); + }); +}); From d322270d89c3332599b0fe761ecaac38539c2485 Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Mon, 25 May 2026 17:39:57 +0200 Subject: [PATCH 02/14] feat(cli): wire --model flag into handleChat Co-authored-by: Cursor --- src/cli.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index f3d59cf..ed21071 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2227,13 +2227,18 @@ async function callAiTool( } async function handleChat(args: string[]): Promise { - const prompt = args.join(" "); + const { prompt, model } = parseChatArgs(args); if (!prompt) { throw new CdpError("Missing prompt", "VALIDATION_ERROR", [ 'Run `opera-browser-cli chat "What is on this page?"` to chat with Opera AI', + "Use --model to select a model (run `opera-browser-cli models` to list)", ]); } - const result = await callAiTool("chat", "opera_chat", { prompt }); + const toolArgs: Record = { prompt }; + if (model !== undefined) { + toolArgs["model"] = model; + } + const result = await callAiTool("chat", "opera_chat", toolArgs); checkAiResultForSignInError("chat", result); return formatMcpResult("result", result, []); } From 09481c60d840d6ab9276881e0bcb5f0c9e12980f Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Mon, 25 May 2026 17:40:42 +0200 Subject: [PATCH 03/14] feat(cli): add models command for listing available AI models Co-authored-by: Cursor --- src/cli.ts | 27 +++++++++++++++++++++++++++ src/client.ts | 1 + 2 files changed, 28 insertions(+) diff --git a/src/cli.ts b/src/cli.ts index ed21071..2d60632 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2332,6 +2332,32 @@ async function handleResearch(args: string[]): Promise { return formatMcpResult("result", result, []); } +async function handleModels(): Promise { + let raw: string; + try { + raw = await callTool("opera_list_models", {}); + } catch (error) { + if (error instanceof CdpError) { + throw new CdpError( + "Model listing not supported by connected browser. Upgrade Opera or check connection.", + "UNSUPPORTED_OPERATION", + ['Run `opera-browser-cli doctor` to check the connection'], + ); + } + throw error; + } + const data = JSON.parse(raw) as { + models: Array<{ id: string; name: string; isDefault: boolean }>; + }; + const lines = ["Available models:"]; + for (const m of data.models) { + const marker = m.isDefault ? "* " : " "; + const suffix = m.isDefault ? " (default)" : ""; + lines.push(` ${marker}${m.id}${suffix}`); + } + return lines.join("\n"); +} + async function handleRun(): Promise { if (process.stdin.isTTY) { throw new CdpError("No script provided on stdin", "VALIDATION_ERROR", [ @@ -2478,6 +2504,7 @@ const COMMANDS: Record = { "invoke-do": withoutFullFlag(handleInvokeDo), make: withoutFullFlag(handleMake), research: withoutFullFlag(handleResearch), + models: withoutFullFlag(handleModels), setup: withoutFullFlag(handleSetup), logs: withoutFullFlag(handleLogs), doctor: withoutFullFlag(handleDoctor), diff --git a/src/client.ts b/src/client.ts index 99085f8..f7610a3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -69,6 +69,7 @@ export type ErrorCode = | "PAGE_CLOSED" | "BROWSER_ERROR" | "VALIDATION_ERROR" + | "UNSUPPORTED_OPERATION" | "UNKNOWN"; export class CdpError extends AxiError { From 0e6c48c5204933a415a4d1d7a263d918c2abc16d Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Mon, 25 May 2026 17:41:20 +0200 Subject: [PATCH 04/14] docs(cli): update help text for --model flag and models command Co-authored-by: Cursor --- src/cli.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 2d60632..cbc3f02 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -54,7 +54,8 @@ commands[41]: resize , emulate, console, console-get , network, network-get [id], lighthouse, perf-start, perf-stop, perf-insight , heap , start, stop, - chat , invoke-do , make , research , + chat [--model ] , invoke-do , make , + research , models, setup, logs, doctor flags[2]: @@ -77,7 +78,8 @@ environment: Run \`opera-browser-cli setup\` to configure interactively. opera ai: - chat is available on any Opera browser. + chat is available on any Opera browser. Use --model to select a model. + Run "models" to list available models. invoke-do, make, and research require Opera Neon with an active sign-in. Run \`opera-browser-cli setup\` to configure the executable path, or set OPERA_CLI_EXECUTABLE_PATH="/Applications/Opera Neon.app/Contents/MacOS/Opera". @@ -543,15 +545,18 @@ examples: opera-browser-cli heap ./snapshot.heapsnapshot`, // Opera AI - chat: `usage: opera-browser-cli chat + chat: `usage: opera-browser-cli chat [--model ] Send a chat message to the Opera AI. args: Message to send (required) +options: + --model AI model to use (run "opera-browser-cli models" to list) + examples: opera-browser-cli chat "Hello, who are you?" - opera-browser-cli chat "What can you help me with?"`, + opera-browser-cli chat --model claude-sonnet-4 "Summarize this page"`, "invoke-do": `usage: opera-browser-cli invoke-do Ask the Opera AI to perform a complex browsing task. @@ -590,6 +595,12 @@ examples: opera-browser-cli research "advances in CRISPR gene editing" --type deep opera-browser-cli research "best practices for React performance" --type one-minute`, + models: `usage: opera-browser-cli models +List available AI models for chat. + +examples: + opera-browser-cli models`, + setup: `usage: opera-browser-cli setup Interactive configuration wizard. Detects Opera Neon and writes settings to ~/.opera-browser-cli/config, which opera-browser-cli auto-loads on every run. From 087550d7a0b42bbfa0742986874ae90adf199697 Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Mon, 25 May 2026 17:41:40 +0200 Subject: [PATCH 05/14] docs: add chat-model-selector spec and plan to CLAUDE.md Co-authored-by: Cursor --- CLAUDE.md | 1 + specs/chat-model-selector-plan.md | 480 ++++++++++++++++++++++++++++++ specs/chat-model-selector.md | 225 ++++++++++++++ 3 files changed, 706 insertions(+) create mode 100644 specs/chat-model-selector-plan.md create mode 100644 specs/chat-model-selector.md diff --git a/CLAUDE.md b/CLAUDE.md index a6885f0..b5b5c10 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,6 +27,7 @@ Always check there before starting implementation work. | Spec | Status | |---|---| | [`specs/fix-parallel-streaming-routing.md`](specs/fix-parallel-streaming-routing.md) | Planned — parallel chunk routing for concurrent Opera AI calls | +| [`specs/chat-model-selector.md`](specs/chat-model-selector.md) | Planned — model selector for chat command | ## Common issues diff --git a/specs/chat-model-selector-plan.md b/specs/chat-model-selector-plan.md new file mode 100644 index 0000000..3a3dc10 --- /dev/null +++ b/specs/chat-model-selector-plan.md @@ -0,0 +1,480 @@ +# Chat Model Selector Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add `--model` flag to the `chat` command and a new `models` command for discovering available AI models. + +**Architecture:** Extend the existing CDP → MCP → CLI pipeline. Add optional `model` field to `opera_chat` MCP tool, add new `opera_list_models` MCP tool, thread both through the CLI with arg parsing modeled after the existing `--type` flag on `research`. + +**Tech Stack:** TypeScript, Zod (MCP schemas), Vitest (tests), axi-sdk-js (CLI framework) + +--- + +## File Map + +| File | Action | Responsibility | +|------|--------|---------------| +| `opera-devtools-mcp/src/tools/opera.ts` | Modify | Add `model` param to `operaChat`, add `operaListModels` tool | +| `opera-devtools-mcp/src/bin/chrome-devtools-cli-options.ts` | Modify | Add `model` arg to `opera_chat`, add `opera_list_models` entry | +| `opera-browser-cli/src/cli.ts` | Modify | Add `parseChatArgs`, update `handleChat`, add `handleModels`, update help text | +| `opera-browser-cli/test/cli.test.ts` | Modify | Add tests for `parseChatArgs` | + +--- + +### Task 1: Add `model` parameter to `opera_chat` MCP tool + +**Files:** +- Modify: `opera-devtools-mcp/src/tools/opera.ts:121-148` + +- [ ] **Step 1: Add `model` to the schema** + +In `opera-devtools-mcp/src/tools/opera.ts`, update the `operaChat` definition: + +```typescript +export const operaChat = definePageTool({ + name: 'opera_chat', + description: + "Send a chat prompt to Opera's built-in AI and return the response. Only available when connected to Opera Neon.", + blockedByDialog: false, + annotations: { + category: ToolCategory.OPERA, + readOnlyHint: false, + }, + schema: { + prompt: zod.string().describe('The prompt to send to Opera AI.'), + model: zod + .string() + .optional() + .describe( + 'Model ID to use for the chat. Omit to use the browser default. Use opera_list_models to discover available IDs.', + ), + }, + handler: async (request, response) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const session = getCDPSession(request.page.pptrPage as any); + try { + const payload: Record = { + action: 'chat', + prompt: request.params.prompt, + }; + if (request.params.model !== undefined) { + payload['model'] = request.params.model; + } + const result = await dispatchAction(session, payload); + response.appendResponseLine(result); + } catch (e) { + response.appendResponseLine( + `Opera.dispatchAction(chat) failed with error: ${(e as Error).message}`, + ); + } + }, +}); +``` + +- [ ] **Step 2: Verify the MCP package compiles** + +Run: `cd opera-devtools-mcp && npm run build` +Expected: Build succeeds with no type errors. + +- [ ] **Step 3: Commit** + +```bash +git add opera-devtools-mcp/src/tools/opera.ts +git commit -m "feat(mcp): add optional model param to opera_chat tool" +``` + +--- + +### Task 2: Add `opera_list_models` MCP tool + +**Files:** +- Modify: `opera-devtools-mcp/src/tools/opera.ts` (append after `operaResearch`) + +- [ ] **Step 1: Add the tool definition** + +Append to `opera-devtools-mcp/src/tools/opera.ts`: + +```typescript +export const operaListModels = definePageTool({ + name: 'opera_list_models', + description: + 'List available AI models for Opera chat. Returns model IDs, display names, and which is the default.', + blockedByDialog: false, + annotations: { + category: ToolCategory.OPERA, + readOnlyHint: true, + }, + schema: {}, + handler: async (request, response) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const session = getCDPSession(request.page.pptrPage as any); + try { + const result = await session.send('Opera.getAvailableModels'); + response.appendResponseLine(JSON.stringify(result)); + } catch (e) { + response.appendResponseLine( + `Opera.getAvailableModels failed with error: ${(e as Error).message}`, + ); + } + }, +}); +``` + +- [ ] **Step 2: Verify the MCP package compiles** + +Run: `cd opera-devtools-mcp && npm run build` +Expected: Build succeeds. The new tool is auto-registered via `Object.values(operaTools)` in `tools.ts`. + +- [ ] **Step 3: Commit** + +```bash +git add opera-devtools-mcp/src/tools/opera.ts +git commit -m "feat(mcp): add opera_list_models tool" +``` + +--- + +### Task 3: Update generated CLI options for MCP package + +**Files:** +- Modify: `opera-devtools-mcp/src/bin/chrome-devtools-cli-options.ts:469-481` + +- [ ] **Step 1: Add `model` arg to `opera_chat` and add `opera_list_models` entry** + +Update the `opera_chat` entry in `chrome-devtools-cli-options.ts`: + +```typescript + opera_chat: { + description: + "Send a chat prompt to Opera's built-in AI and return the response. Only available when connected to Opera Neon.", + category: 'Opera', + args: { + prompt: { + name: 'prompt', + type: 'string', + description: 'The prompt to send to Opera AI.', + required: true, + }, + model: { + name: 'model', + type: 'string', + description: + 'Model ID to use for the chat. Omit to use the browser default. Use opera_list_models to discover available IDs.', + required: false, + }, + }, + }, +``` + +Add a new entry for `opera_list_models` (insert alphabetically near the other `opera_` entries): + +```typescript + opera_list_models: { + description: + 'List available AI models for Opera chat. Returns model IDs, display names, and which is the default.', + category: 'Opera', + args: {}, + }, +``` + +- [ ] **Step 2: Verify build** + +Run: `cd opera-devtools-mcp && npm run build` +Expected: Build succeeds. + +- [ ] **Step 3: Commit** + +```bash +git add opera-devtools-mcp/src/bin/chrome-devtools-cli-options.ts +git commit -m "feat(mcp): update generated CLI options for model selector" +``` + +--- + +### Task 4: Add `parseChatArgs` with tests (CLI layer) + +**Files:** +- Modify: `opera-browser-cli/src/cli.ts` (add export `parseChatArgs`) +- Modify: `opera-browser-cli/test/cli.test.ts` (add tests) + +- [ ] **Step 1: Write the failing tests** + +Add to `opera-browser-cli/test/cli.test.ts`: + +```typescript +import { parseChatArgs } from "../src/cli.js"; + +describe("parseChatArgs", () => { + it("parses prompt only", () => { + const result = parseChatArgs(["Hello", "world"]); + expect(result).toEqual({ prompt: "Hello world", model: undefined }); + }); + + it("parses --model flag with prompt", () => { + const result = parseChatArgs(["--model", "gpt-4o", "What", "is", "this?"]); + expect(result).toEqual({ prompt: "What is this?", model: "gpt-4o" }); + }); + + it("parses --model at end of args", () => { + const result = parseChatArgs(["Hello", "--model", "claude-sonnet-4"]); + expect(result).toEqual({ prompt: "Hello", model: "claude-sonnet-4" }); + }); + + it("returns empty prompt when only --model is given", () => { + const result = parseChatArgs(["--model", "gpt-4o"]); + expect(result).toEqual({ prompt: "", model: "gpt-4o" }); + }); + + it("ignores --model without a value", () => { + const result = parseChatArgs(["Hello", "--model"]); + expect(result).toEqual({ prompt: "Hello", model: undefined }); + }); +}); +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd opera-browser-cli && npx vitest run test/cli.test.ts` +Expected: FAIL — `parseChatArgs` is not exported from `../src/cli.js` + +- [ ] **Step 3: Implement `parseChatArgs`** + +Add to `opera-browser-cli/src/cli.ts` (near `parseResearchArgs`): + +```typescript +export function parseChatArgs(args: string[]): { + prompt: string; + model?: string; +} { + let model: string | undefined; + const promptParts: string[] = []; + for (let i = 0; i < args.length; i++) { + if (args[i] === "--model" && i + 1 < args.length) { + model = args[++i]; + } else { + promptParts.push(args[i]); + } + } + return { prompt: promptParts.join(" "), model }; +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd opera-browser-cli && npx vitest run test/cli.test.ts` +Expected: All `parseChatArgs` tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add opera-browser-cli/src/cli.ts opera-browser-cli/test/cli.test.ts +git commit -m "feat(cli): add parseChatArgs for --model flag" +``` + +--- + +### Task 5: Update `handleChat` to use `parseChatArgs` + +**Files:** +- Modify: `opera-browser-cli/src/cli.ts:2229-2239` + +- [ ] **Step 1: Update `handleChat`** + +Replace the current `handleChat` function: + +```typescript +async function handleChat(args: string[]): Promise { + const { prompt, model } = parseChatArgs(args); + if (!prompt) { + throw new CdpError("Missing prompt", "VALIDATION_ERROR", [ + 'Run `opera-browser-cli chat "What is on this page?"` to chat with Opera AI', + "Use --model to select a model (run `opera-browser-cli models` to list)", + ]); + } + const toolArgs: Record = { prompt }; + if (model !== undefined) { + toolArgs["model"] = model; + } + const result = await callAiTool("chat", "opera_chat", toolArgs); + checkAiResultForSignInError("chat", result); + return formatMcpResult("result", result, []); +} +``` + +- [ ] **Step 2: Verify build** + +Run: `cd opera-browser-cli && npx tsc --noEmit` +Expected: No type errors. + +- [ ] **Step 3: Commit** + +```bash +git add opera-browser-cli/src/cli.ts +git commit -m "feat(cli): wire --model flag into handleChat" +``` + +--- + +### Task 6: Add `handleModels` command + +**Files:** +- Modify: `opera-browser-cli/src/cli.ts` (add handler + register command) + +- [ ] **Step 1: Add `handleModels` function** + +Add near the other AI command handlers in `opera-browser-cli/src/cli.ts`: + +```typescript +async function handleModels(): Promise { + let raw: string; + try { + raw = await callTool("opera_list_models", {}); + } catch (error) { + if (error instanceof CdpError) { + throw new CdpError( + "Model listing not supported by connected browser. Upgrade Opera or check connection.", + "UNSUPPORTED_OPERATION", + ['Run `opera-browser-cli doctor` to check the connection'], + ); + } + throw error; + } + const data = JSON.parse(raw) as { + models: Array<{ id: string; name: string; isDefault: boolean }>; + }; + const lines = ["Available models:"]; + for (const m of data.models) { + const marker = m.isDefault ? "* " : " "; + const suffix = m.isDefault ? " (default)" : ""; + lines.push(` ${marker}${m.id}${suffix}`); + } + return lines.join("\n"); +} +``` + +- [ ] **Step 2: Register the command in the dispatch map** + +In the `commands` object (around line 2454), add: + +```typescript + models: withoutFullFlag(handleModels), +``` + +- [ ] **Step 3: Verify build** + +Run: `cd opera-browser-cli && npx tsc --noEmit` +Expected: No type errors. + +- [ ] **Step 4: Commit** + +```bash +git add opera-browser-cli/src/cli.ts +git commit -m "feat(cli): add models command for listing available AI models" +``` + +--- + +### Task 7: Update CLI help text + +**Files:** +- Modify: `opera-browser-cli/src/cli.ts` (help strings) + +- [ ] **Step 1: Update `chat` help text** + +Replace the `chat` entry in the `HELP` object: + +```typescript + chat: `usage: opera-browser-cli chat [--model ] +Send a chat message to the Opera AI. + +args: + Message to send (required) + +options: + --model AI model to use (run "opera-browser-cli models" to list) + +examples: + opera-browser-cli chat "Hello, who are you?" + opera-browser-cli chat --model claude-sonnet-4 "Summarize this page"`, +``` + +- [ ] **Step 2: Add `models` help text** + +Add a new entry to the `HELP` object: + +```typescript + models: `usage: opera-browser-cli models +List available AI models for chat. + +examples: + opera-browser-cli models`, +``` + +- [ ] **Step 3: Update `TOP_HELP` command list** + +In the `TOP_HELP` string, update the command listing line that contains `chat`: + +From: +``` + chat , invoke-do , make , research , +``` + +To: +``` + chat [--model ] , invoke-do , make , + research , models, +``` + +- [ ] **Step 4: Update the `opera ai:` section of `TOP_HELP`** + +From: +``` +opera ai: + chat is available on any Opera browser. + invoke-do, make, and research require Opera Neon with an active sign-in. +``` + +To: +``` +opera ai: + chat is available on any Opera browser. Use --model to select a model. + Run "models" to list available models. + invoke-do, make, and research require Opera Neon with an active sign-in. +``` + +- [ ] **Step 5: Verify build and existing help tests still pass** + +Run: `cd opera-browser-cli && npx vitest run test/cli.test.ts` +Expected: All tests pass (the `getCommandHelp` tests should still work). + +- [ ] **Step 6: Commit** + +```bash +git add opera-browser-cli/src/cli.ts +git commit -m "docs(cli): update help text for --model flag and models command" +``` + +--- + +### Task 8: Update CLAUDE.md specs table + +**Files:** +- Modify: `opera-browser-cli/CLAUDE.md` + +- [ ] **Step 1: Add the new spec to the table** + +Update the specs table in `opera-browser-cli/CLAUDE.md`: + +```markdown +| Spec | Status | +|---|---| +| [`specs/fix-parallel-streaming-routing.md`](specs/fix-parallel-streaming-routing.md) | Planned — parallel chunk routing for concurrent Opera AI calls | +| [`specs/chat-model-selector.md`](specs/chat-model-selector.md) | Planned — model selector for chat command | +``` + +- [ ] **Step 2: Commit** + +```bash +git add opera-browser-cli/CLAUDE.md +git commit -m "docs: add chat-model-selector spec to CLAUDE.md" +``` diff --git a/specs/chat-model-selector.md b/specs/chat-model-selector.md new file mode 100644 index 0000000..4215be6 --- /dev/null +++ b/specs/chat-model-selector.md @@ -0,0 +1,225 @@ +# Chat Model Selector + +**Status:** Planned +**Date:** 2026-05-25 +**Scope:** `opera-browser-cli` CLI + `opera-devtools-mcp` MCP layer + CDP contract for browser team + +## Overview + +Add a model selector to the Opera CLI Chat feature, allowing users to choose which AI model is used for chat sessions. The browser does not yet support model selection via CDP — this spec defines the full client-side implementation and the CDP contract the browser team must fulfill. + +## CLI UX + +### `--model` flag on `chat` + +``` +opera-browser-cli chat --model "What is on this page?" +opera-browser-cli chat "Hello" # no flag → browser default +``` + +When `--model` is omitted, no model field is sent in the CDP payload — the browser uses its own default. + +### `models` command + +New top-level command to discover available models: + +``` +opera-browser-cli models +``` + +Output: + +``` +Available models: + * gpt-4o (default) + claude-sonnet-4 + gemini-2.5-pro +``` + +The `*` marks the browser's reported default model. + +### Error handling + +| Scenario | Behavior | +|----------|----------| +| `--model` with invalid ID | Surface browser error: `Model "foo" is not available. Run "opera-browser-cli models" to see available models.` | +| `models` command when browser doesn't support listing | `Model listing not supported by connected browser. Upgrade Opera or check connection.` | +| `--model` when browser doesn't support model param | Forward the field; if browser errors, surface it with upgrade hint | + +## CDP Contract (Requirements for Browser Team) + +### New method: `Opera.getAvailableModels` + +Returns the list of AI models available for chat. + +**Request:** No parameters. + +```json +{ "method": "Opera.getAvailableModels" } +``` + +**Response:** + +```json +{ + "models": [ + { "id": "gpt-4o", "name": "GPT-4o", "isDefault": true }, + { "id": "claude-sonnet-4", "name": "Claude Sonnet 4", "isDefault": false }, + { "id": "gemini-2.5-pro", "name": "Gemini 2.5 Pro", "isDefault": false } + ] +} +``` + +**Model object schema:** + +| Field | Type | Description | +|-------|------|-------------| +| `id` | `string` | Stable identifier used in `--model` flag and `dispatchAction` payload | +| `name` | `string` | Human-readable display name for CLI output | +| `isDefault` | `boolean` | Exactly one model has `isDefault: true` — the browser's current default | + +### Extended `Opera.dispatchAction` payload for chat + +Current (unchanged when no model specified): + +```json +{ "action": "chat", "prompt": "Hello" } +``` + +With model selection: + +```json +{ "action": "chat", "prompt": "Hello", "model": "claude-sonnet-4" } +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `action` | `string` | Yes | `"chat"` | +| `prompt` | `string` | Yes | User's message | +| `model` | `string` | No | Model ID from `getAvailableModels`. Omit to use browser default. | + +### Error response for invalid model + +When `model` is provided but not recognized: + +```json +{ + "error": "MODEL_NOT_AVAILABLE", + "message": "Model \"foo\" is not available", + "availableModels": ["gpt-4o", "claude-sonnet-4", "gemini-2.5-pro"] +} +``` + +## MCP Layer Changes (opera-devtools-mcp) + +### Updated `opera_chat` tool + +Add optional `model` parameter to the schema: + +```typescript +schema: { + prompt: zod.string().describe('The prompt to send to Opera AI.'), + model: zod.string().optional().describe('Model ID to use. Omit for browser default.'), +} +``` + +Handler passes `model` through to CDP: + +```typescript +handler: async (request, response) => { + const session = getCDPSession(request.page.pptrPage); + const result = await dispatchAction(session, { + action: 'chat', + prompt: request.params.prompt, + ...(request.params.model && { model: request.params.model }), + }); + response.appendResponseLine(result); +} +``` + +### New `opera_list_models` tool + +```typescript +definePageTool({ + name: 'opera_list_models', + description: 'List available AI models for Opera chat.', + blockedByDialog: false, + annotations: { + category: ToolCategory.OPERA, + readOnlyHint: true, + }, + schema: {}, + handler: async (request, response) => { + const session = getCDPSession(request.page.pptrPage); + const result = await session.send('Opera.getAvailableModels'); + response.appendResponseLine(JSON.stringify(result)); + }, +}); +``` + +## CLI Layer Changes (opera-browser-cli) + +### Updated `handleChat` + +1. Parse `--model ` from args before joining remaining tokens as prompt +2. Pass `{ prompt, model }` (model omitted if not specified) to `callTool("opera_chat", ...)` +3. On `MODEL_NOT_AVAILABLE` error, format a helpful message with hint to run `models` + +### New `handleModels` command + +1. Call `callTool("opera_list_models", {})` +2. Parse JSON response +3. Format as bulleted list: `* (default)` for default model, ` ` for others +4. Register as top-level command in the command dispatch map + +### Command registration + +```typescript +models: handleModels, +``` + +### CLI help text + +The `chat` command help is updated to include the `--model` flag: + +``` +usage: opera-browser-cli chat [--model ] + +Send a chat message to the Opera AI. + +args: + Message to send (required) + +options: + --model AI model to use (run "opera-browser-cli models" to list) + +examples: + opera-browser-cli chat "Hello, who are you?" + opera-browser-cli chat --model claude-sonnet-4 "Summarize this page" +``` + +The `models` command help: + +``` +usage: opera-browser-cli models + +List available AI models for chat. + +examples: + opera-browser-cli models +``` + +Both commands are listed in the top-level help alongside existing AI commands. + +## Scope boundaries + +- `--model` applies to `chat` only (not `invoke-do`, `make`, `research`) — extensible later +- No client-side model persistence — each invocation is stateless +- No interactive model picker — discovery via `models` command, selection via `--model` flag +- Graceful degradation when connected browser doesn't support the new CDP methods + +## Future extensions + +- Extend `--model` to other AI commands (`invoke-do`, `make`, `research`) +- Add `--model` to the `research` command's `researchType`-style options +- Persist a default model preference client-side (e.g. `opera-browser-cli config set model `) From 7e8dbc73f3a5f46c6959e2d5bc69549fa2399fa6 Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Mon, 25 May 2026 18:01:53 +0200 Subject: [PATCH 06/14] docs: update spec to use dispatchAction for model listing Co-authored-by: Cursor --- specs/chat-model-selector.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/specs/chat-model-selector.md b/specs/chat-model-selector.md index 4215be6..ba8d45a 100644 --- a/specs/chat-model-selector.md +++ b/specs/chat-model-selector.md @@ -48,17 +48,15 @@ The `*` marks the browser's reported default model. ## CDP Contract (Requirements for Browser Team) -### New method: `Opera.getAvailableModels` +Everything goes through `Opera.dispatchAction` — no new CDP methods required. -Returns the list of AI models available for chat. - -**Request:** No parameters. +### List models action ```json -{ "method": "Opera.getAvailableModels" } +{ "action": "listModels" } ``` -**Response:** +**Response** (returned as `result` string, JSON-encoded): ```json { @@ -74,11 +72,11 @@ Returns the list of AI models available for chat. | Field | Type | Description | |-------|------|-------------| -| `id` | `string` | Stable identifier used in `--model` flag and `dispatchAction` payload | +| `id` | `string` | Stable identifier used in `--model` flag and chat payload | | `name` | `string` | Human-readable display name for CLI output | | `isDefault` | `boolean` | Exactly one model has `isDefault: true` — the browser's current default | -### Extended `Opera.dispatchAction` payload for chat +### Chat action with model selection Current (unchanged when no model specified): @@ -96,7 +94,7 @@ With model selection: |-------|------|----------|-------------| | `action` | `string` | Yes | `"chat"` | | `prompt` | `string` | Yes | User's message | -| `model` | `string` | No | Model ID from `getAvailableModels`. Omit to use browser default. | +| `model` | `string` | No | Model ID from `listModels`. Omit to use browser default. | ### Error response for invalid model @@ -151,8 +149,8 @@ definePageTool({ schema: {}, handler: async (request, response) => { const session = getCDPSession(request.page.pptrPage); - const result = await session.send('Opera.getAvailableModels'); - response.appendResponseLine(JSON.stringify(result)); + const result = await dispatchAction(session, { action: 'listModels' }); + response.appendResponseLine(result); }, }); ``` From 5654721b6c26b92a31e0e9e1a5512f598ef1c174 Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Tue, 26 May 2026 16:21:31 +0200 Subject: [PATCH 07/14] chore: bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c4e59db..b22cd6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opera-browser-cli", - "version": "0.1.35", + "version": "0.1.36", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opera-browser-cli", - "version": "0.1.35", + "version": "0.1.36", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", diff --git a/package.json b/package.json index ad0aeef..4f79500 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opera-browser-cli", - "version": "0.1.35", + "version": "0.1.36", "description": "AXI-compliant opera-devtools-mcp wrapper — combined operations, TOON output, contextual suggestions", "type": "module", "repository": { From e0be6eee793f6c9113b2863ad20f4eae38c274e2 Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Wed, 27 May 2026 11:02:52 +0200 Subject: [PATCH 08/14] chore: remove plan for model selector --- specs/chat-model-selector-plan.md | 480 ------------------------------ 1 file changed, 480 deletions(-) delete mode 100644 specs/chat-model-selector-plan.md diff --git a/specs/chat-model-selector-plan.md b/specs/chat-model-selector-plan.md deleted file mode 100644 index 3a3dc10..0000000 --- a/specs/chat-model-selector-plan.md +++ /dev/null @@ -1,480 +0,0 @@ -# Chat Model Selector Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add `--model` flag to the `chat` command and a new `models` command for discovering available AI models. - -**Architecture:** Extend the existing CDP → MCP → CLI pipeline. Add optional `model` field to `opera_chat` MCP tool, add new `opera_list_models` MCP tool, thread both through the CLI with arg parsing modeled after the existing `--type` flag on `research`. - -**Tech Stack:** TypeScript, Zod (MCP schemas), Vitest (tests), axi-sdk-js (CLI framework) - ---- - -## File Map - -| File | Action | Responsibility | -|------|--------|---------------| -| `opera-devtools-mcp/src/tools/opera.ts` | Modify | Add `model` param to `operaChat`, add `operaListModels` tool | -| `opera-devtools-mcp/src/bin/chrome-devtools-cli-options.ts` | Modify | Add `model` arg to `opera_chat`, add `opera_list_models` entry | -| `opera-browser-cli/src/cli.ts` | Modify | Add `parseChatArgs`, update `handleChat`, add `handleModels`, update help text | -| `opera-browser-cli/test/cli.test.ts` | Modify | Add tests for `parseChatArgs` | - ---- - -### Task 1: Add `model` parameter to `opera_chat` MCP tool - -**Files:** -- Modify: `opera-devtools-mcp/src/tools/opera.ts:121-148` - -- [ ] **Step 1: Add `model` to the schema** - -In `opera-devtools-mcp/src/tools/opera.ts`, update the `operaChat` definition: - -```typescript -export const operaChat = definePageTool({ - name: 'opera_chat', - description: - "Send a chat prompt to Opera's built-in AI and return the response. Only available when connected to Opera Neon.", - blockedByDialog: false, - annotations: { - category: ToolCategory.OPERA, - readOnlyHint: false, - }, - schema: { - prompt: zod.string().describe('The prompt to send to Opera AI.'), - model: zod - .string() - .optional() - .describe( - 'Model ID to use for the chat. Omit to use the browser default. Use opera_list_models to discover available IDs.', - ), - }, - handler: async (request, response) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const session = getCDPSession(request.page.pptrPage as any); - try { - const payload: Record = { - action: 'chat', - prompt: request.params.prompt, - }; - if (request.params.model !== undefined) { - payload['model'] = request.params.model; - } - const result = await dispatchAction(session, payload); - response.appendResponseLine(result); - } catch (e) { - response.appendResponseLine( - `Opera.dispatchAction(chat) failed with error: ${(e as Error).message}`, - ); - } - }, -}); -``` - -- [ ] **Step 2: Verify the MCP package compiles** - -Run: `cd opera-devtools-mcp && npm run build` -Expected: Build succeeds with no type errors. - -- [ ] **Step 3: Commit** - -```bash -git add opera-devtools-mcp/src/tools/opera.ts -git commit -m "feat(mcp): add optional model param to opera_chat tool" -``` - ---- - -### Task 2: Add `opera_list_models` MCP tool - -**Files:** -- Modify: `opera-devtools-mcp/src/tools/opera.ts` (append after `operaResearch`) - -- [ ] **Step 1: Add the tool definition** - -Append to `opera-devtools-mcp/src/tools/opera.ts`: - -```typescript -export const operaListModels = definePageTool({ - name: 'opera_list_models', - description: - 'List available AI models for Opera chat. Returns model IDs, display names, and which is the default.', - blockedByDialog: false, - annotations: { - category: ToolCategory.OPERA, - readOnlyHint: true, - }, - schema: {}, - handler: async (request, response) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const session = getCDPSession(request.page.pptrPage as any); - try { - const result = await session.send('Opera.getAvailableModels'); - response.appendResponseLine(JSON.stringify(result)); - } catch (e) { - response.appendResponseLine( - `Opera.getAvailableModels failed with error: ${(e as Error).message}`, - ); - } - }, -}); -``` - -- [ ] **Step 2: Verify the MCP package compiles** - -Run: `cd opera-devtools-mcp && npm run build` -Expected: Build succeeds. The new tool is auto-registered via `Object.values(operaTools)` in `tools.ts`. - -- [ ] **Step 3: Commit** - -```bash -git add opera-devtools-mcp/src/tools/opera.ts -git commit -m "feat(mcp): add opera_list_models tool" -``` - ---- - -### Task 3: Update generated CLI options for MCP package - -**Files:** -- Modify: `opera-devtools-mcp/src/bin/chrome-devtools-cli-options.ts:469-481` - -- [ ] **Step 1: Add `model` arg to `opera_chat` and add `opera_list_models` entry** - -Update the `opera_chat` entry in `chrome-devtools-cli-options.ts`: - -```typescript - opera_chat: { - description: - "Send a chat prompt to Opera's built-in AI and return the response. Only available when connected to Opera Neon.", - category: 'Opera', - args: { - prompt: { - name: 'prompt', - type: 'string', - description: 'The prompt to send to Opera AI.', - required: true, - }, - model: { - name: 'model', - type: 'string', - description: - 'Model ID to use for the chat. Omit to use the browser default. Use opera_list_models to discover available IDs.', - required: false, - }, - }, - }, -``` - -Add a new entry for `opera_list_models` (insert alphabetically near the other `opera_` entries): - -```typescript - opera_list_models: { - description: - 'List available AI models for Opera chat. Returns model IDs, display names, and which is the default.', - category: 'Opera', - args: {}, - }, -``` - -- [ ] **Step 2: Verify build** - -Run: `cd opera-devtools-mcp && npm run build` -Expected: Build succeeds. - -- [ ] **Step 3: Commit** - -```bash -git add opera-devtools-mcp/src/bin/chrome-devtools-cli-options.ts -git commit -m "feat(mcp): update generated CLI options for model selector" -``` - ---- - -### Task 4: Add `parseChatArgs` with tests (CLI layer) - -**Files:** -- Modify: `opera-browser-cli/src/cli.ts` (add export `parseChatArgs`) -- Modify: `opera-browser-cli/test/cli.test.ts` (add tests) - -- [ ] **Step 1: Write the failing tests** - -Add to `opera-browser-cli/test/cli.test.ts`: - -```typescript -import { parseChatArgs } from "../src/cli.js"; - -describe("parseChatArgs", () => { - it("parses prompt only", () => { - const result = parseChatArgs(["Hello", "world"]); - expect(result).toEqual({ prompt: "Hello world", model: undefined }); - }); - - it("parses --model flag with prompt", () => { - const result = parseChatArgs(["--model", "gpt-4o", "What", "is", "this?"]); - expect(result).toEqual({ prompt: "What is this?", model: "gpt-4o" }); - }); - - it("parses --model at end of args", () => { - const result = parseChatArgs(["Hello", "--model", "claude-sonnet-4"]); - expect(result).toEqual({ prompt: "Hello", model: "claude-sonnet-4" }); - }); - - it("returns empty prompt when only --model is given", () => { - const result = parseChatArgs(["--model", "gpt-4o"]); - expect(result).toEqual({ prompt: "", model: "gpt-4o" }); - }); - - it("ignores --model without a value", () => { - const result = parseChatArgs(["Hello", "--model"]); - expect(result).toEqual({ prompt: "Hello", model: undefined }); - }); -}); -``` - -- [ ] **Step 2: Run tests to verify they fail** - -Run: `cd opera-browser-cli && npx vitest run test/cli.test.ts` -Expected: FAIL — `parseChatArgs` is not exported from `../src/cli.js` - -- [ ] **Step 3: Implement `parseChatArgs`** - -Add to `opera-browser-cli/src/cli.ts` (near `parseResearchArgs`): - -```typescript -export function parseChatArgs(args: string[]): { - prompt: string; - model?: string; -} { - let model: string | undefined; - const promptParts: string[] = []; - for (let i = 0; i < args.length; i++) { - if (args[i] === "--model" && i + 1 < args.length) { - model = args[++i]; - } else { - promptParts.push(args[i]); - } - } - return { prompt: promptParts.join(" "), model }; -} -``` - -- [ ] **Step 4: Run tests to verify they pass** - -Run: `cd opera-browser-cli && npx vitest run test/cli.test.ts` -Expected: All `parseChatArgs` tests PASS. - -- [ ] **Step 5: Commit** - -```bash -git add opera-browser-cli/src/cli.ts opera-browser-cli/test/cli.test.ts -git commit -m "feat(cli): add parseChatArgs for --model flag" -``` - ---- - -### Task 5: Update `handleChat` to use `parseChatArgs` - -**Files:** -- Modify: `opera-browser-cli/src/cli.ts:2229-2239` - -- [ ] **Step 1: Update `handleChat`** - -Replace the current `handleChat` function: - -```typescript -async function handleChat(args: string[]): Promise { - const { prompt, model } = parseChatArgs(args); - if (!prompt) { - throw new CdpError("Missing prompt", "VALIDATION_ERROR", [ - 'Run `opera-browser-cli chat "What is on this page?"` to chat with Opera AI', - "Use --model to select a model (run `opera-browser-cli models` to list)", - ]); - } - const toolArgs: Record = { prompt }; - if (model !== undefined) { - toolArgs["model"] = model; - } - const result = await callAiTool("chat", "opera_chat", toolArgs); - checkAiResultForSignInError("chat", result); - return formatMcpResult("result", result, []); -} -``` - -- [ ] **Step 2: Verify build** - -Run: `cd opera-browser-cli && npx tsc --noEmit` -Expected: No type errors. - -- [ ] **Step 3: Commit** - -```bash -git add opera-browser-cli/src/cli.ts -git commit -m "feat(cli): wire --model flag into handleChat" -``` - ---- - -### Task 6: Add `handleModels` command - -**Files:** -- Modify: `opera-browser-cli/src/cli.ts` (add handler + register command) - -- [ ] **Step 1: Add `handleModels` function** - -Add near the other AI command handlers in `opera-browser-cli/src/cli.ts`: - -```typescript -async function handleModels(): Promise { - let raw: string; - try { - raw = await callTool("opera_list_models", {}); - } catch (error) { - if (error instanceof CdpError) { - throw new CdpError( - "Model listing not supported by connected browser. Upgrade Opera or check connection.", - "UNSUPPORTED_OPERATION", - ['Run `opera-browser-cli doctor` to check the connection'], - ); - } - throw error; - } - const data = JSON.parse(raw) as { - models: Array<{ id: string; name: string; isDefault: boolean }>; - }; - const lines = ["Available models:"]; - for (const m of data.models) { - const marker = m.isDefault ? "* " : " "; - const suffix = m.isDefault ? " (default)" : ""; - lines.push(` ${marker}${m.id}${suffix}`); - } - return lines.join("\n"); -} -``` - -- [ ] **Step 2: Register the command in the dispatch map** - -In the `commands` object (around line 2454), add: - -```typescript - models: withoutFullFlag(handleModels), -``` - -- [ ] **Step 3: Verify build** - -Run: `cd opera-browser-cli && npx tsc --noEmit` -Expected: No type errors. - -- [ ] **Step 4: Commit** - -```bash -git add opera-browser-cli/src/cli.ts -git commit -m "feat(cli): add models command for listing available AI models" -``` - ---- - -### Task 7: Update CLI help text - -**Files:** -- Modify: `opera-browser-cli/src/cli.ts` (help strings) - -- [ ] **Step 1: Update `chat` help text** - -Replace the `chat` entry in the `HELP` object: - -```typescript - chat: `usage: opera-browser-cli chat [--model ] -Send a chat message to the Opera AI. - -args: - Message to send (required) - -options: - --model AI model to use (run "opera-browser-cli models" to list) - -examples: - opera-browser-cli chat "Hello, who are you?" - opera-browser-cli chat --model claude-sonnet-4 "Summarize this page"`, -``` - -- [ ] **Step 2: Add `models` help text** - -Add a new entry to the `HELP` object: - -```typescript - models: `usage: opera-browser-cli models -List available AI models for chat. - -examples: - opera-browser-cli models`, -``` - -- [ ] **Step 3: Update `TOP_HELP` command list** - -In the `TOP_HELP` string, update the command listing line that contains `chat`: - -From: -``` - chat , invoke-do , make , research , -``` - -To: -``` - chat [--model ] , invoke-do , make , - research , models, -``` - -- [ ] **Step 4: Update the `opera ai:` section of `TOP_HELP`** - -From: -``` -opera ai: - chat is available on any Opera browser. - invoke-do, make, and research require Opera Neon with an active sign-in. -``` - -To: -``` -opera ai: - chat is available on any Opera browser. Use --model to select a model. - Run "models" to list available models. - invoke-do, make, and research require Opera Neon with an active sign-in. -``` - -- [ ] **Step 5: Verify build and existing help tests still pass** - -Run: `cd opera-browser-cli && npx vitest run test/cli.test.ts` -Expected: All tests pass (the `getCommandHelp` tests should still work). - -- [ ] **Step 6: Commit** - -```bash -git add opera-browser-cli/src/cli.ts -git commit -m "docs(cli): update help text for --model flag and models command" -``` - ---- - -### Task 8: Update CLAUDE.md specs table - -**Files:** -- Modify: `opera-browser-cli/CLAUDE.md` - -- [ ] **Step 1: Add the new spec to the table** - -Update the specs table in `opera-browser-cli/CLAUDE.md`: - -```markdown -| Spec | Status | -|---|---| -| [`specs/fix-parallel-streaming-routing.md`](specs/fix-parallel-streaming-routing.md) | Planned — parallel chunk routing for concurrent Opera AI calls | -| [`specs/chat-model-selector.md`](specs/chat-model-selector.md) | Planned — model selector for chat command | -``` - -- [ ] **Step 2: Commit** - -```bash -git add opera-browser-cli/CLAUDE.md -git commit -m "docs: add chat-model-selector spec to CLAUDE.md" -``` From ebbb507f66baa2fe29d8c2370f44ff8bc5c0e0f3 Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Wed, 27 May 2026 11:23:13 +0200 Subject: [PATCH 09/14] docs: enhance SKILL.md with details on AI model selection and listing --- SKILL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SKILL.md b/SKILL.md index 4240ffa..1bf6c52 100644 --- a/SKILL.md +++ b/SKILL.md @@ -8,7 +8,8 @@ description: Browser automation and web interaction using the opera-browser-cli `opera-browser-cli` controls an Opera browser session. - **Standard commands** (`open`, `click`, `fill`, `screenshot`, etc.) — work with any Opera browser session. -- **`chat`** — available on any Opera browser. +- **`chat`** — available on any Opera browser. Use `--model ` to select an AI model. +- **`models`** — list available AI models for chat (shows IDs and which is the default). - **`invoke-do`, `make`, `research`** — require **Opera Neon** with an active sign-in. Run `opera-browser-cli --help` for the full command list, or `opera-browser-cli --help` for per-command flags and examples. From bda6b25ec5b6f1445db87a4ffcdb2552d7de46a0 Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Wed, 27 May 2026 11:50:16 +0200 Subject: [PATCH 10/14] chore: downgrade version to 0.1.35 in package.json and package-lock.json --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b22cd6c..c4e59db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opera-browser-cli", - "version": "0.1.36", + "version": "0.1.35", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opera-browser-cli", - "version": "0.1.36", + "version": "0.1.35", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.1", diff --git a/package.json b/package.json index 4f79500..ad0aeef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opera-browser-cli", - "version": "0.1.36", + "version": "0.1.35", "description": "AXI-compliant opera-devtools-mcp wrapper — combined operations, TOON output, contextual suggestions", "type": "module", "repository": { From e6918a579cfdec28f832c401b3c1fd09fef571d9 Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Wed, 27 May 2026 17:34:10 +0200 Subject: [PATCH 11/14] fix(cli): improve error handling for model listing response --- src/cli.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index cbc3f02..5e0f397 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2357,9 +2357,18 @@ async function handleModels(): Promise { } throw error; } - const data = JSON.parse(raw) as { - models: Array<{ id: string; name: string; isDefault: boolean }>; - }; + + let data: { models: Array<{ id: string; name: string; isDefault: boolean }> }; + try { + data = JSON.parse(raw); + } catch { + throw new CdpError( + raw || "Model listing returned an invalid response", + /Tool.*not found/i.test(raw) ? "UNSUPPORTED_OPERATION" : "UNKNOWN", + ['Run `opera-browser-cli doctor` to check the connection'], + ); + } + const lines = ["Available models:"]; for (const m of data.models) { const marker = m.isDefault ? "* " : " "; From a4503978ff61e265c78fc9d802e141ea3dc6415f Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Wed, 27 May 2026 20:10:28 +0200 Subject: [PATCH 12/14] refactor(cli): enhance error handling for AI tool responses with structured error descriptors --- src/cli.ts | 88 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 5e0f397..e4c20ae 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -7,6 +7,7 @@ import { encode } from "@toon-format/toon"; import { runAxiCli } from "axi-sdk-js"; import { CdpError, + type ErrorCode, callTool, ensureBridge, getBridgeStatus, @@ -2183,26 +2184,69 @@ function requireNeon(command: string): void { ); } +interface CdpResultErrorDescriptor { + match: (result: string) => boolean; + message: string | ((command: string) => string); + code: ErrorCode; + suggestions: (command: string) => string[]; +} + /** - * Opera Neon returns the "not signed in" message as text content on a - * successful tool call (no MCP isError flag), so callTool resolves rather - * than throws. Detect it here and convert to a CdpError so the UX matches - * the thrown-error path. + * Error conditions that Opera returns as plain text content on a successful + * tool call (no MCP isError flag). Each descriptor is checked in order; + * the first match is converted to a CdpError. */ -function checkAiResultForSignInError(command: string, result: string): void { - if ( - result.includes("User is not signed in") || - (result.includes("Opera.dispatchAction") && - result.includes("not signed in")) - ) { - throw new CdpError( - "Opera: user is not signed in", - "BROWSER_ERROR", - [ - `Re-run \`opera-browser-cli ${command}\` after signing in`, - "Run `opera-browser-cli doctor` to inspect the current configuration", - ], - ); +const CDP_RESULT_ERRORS: readonly CdpResultErrorDescriptor[] = [ + { + match: (r) => + r.includes("User is not signed in") || + (r.includes("Opera.dispatchAction") && r.includes("not signed in")), + message: "Opera: user is not signed in", + code: "BROWSER_ERROR", + suggestions: (cmd) => [ + `Re-run \`opera-browser-cli ${cmd}\` after signing in`, + "Run `opera-browser-cli doctor` to inspect the current configuration", + ], + }, + { + match: (r) => r.includes("Subscription required"), + message: "Opera: an active subscription is required", + code: "BROWSER_ERROR", + suggestions: (cmd) => [ + "Check your Opera subscription at https://auth.opera.com/account/", + `Re-run \`opera-browser-cli ${cmd}\` after activating a subscription`, + ], + }, + { + match: (r) => r.includes("User consent required"), + message: "Opera: user consent has not been accepted", + code: "BROWSER_ERROR", + suggestions: (cmd) => [ + "Open Opera and accept the consent prompt before using AI features", + `Re-run \`opera-browser-cli ${cmd}\` after accepting consent`, + ], + }, + { + match: (r) => r.includes("is only available on Opera Neon"), + message: (cmd) => `Opera: ${cmd} is only available on Opera Neon`, + code: "BROWSER_ERROR", + suggestions: () => [ + "Install Opera Neon from https://www.operaneon.com", + "Run `opera-browser-cli setup` to configure the Opera Neon executable path", + "Run `opera-browser-cli doctor` to inspect the current configuration", + ], + }, +]; + +function checkAiResultForCdpError(command: string, result: string): void { + for (const descriptor of CDP_RESULT_ERRORS) { + if (descriptor.match(result)) { + const message = + typeof descriptor.message === "function" + ? descriptor.message(command) + : descriptor.message; + throw new CdpError(message, descriptor.code, descriptor.suggestions(command)); + } } } @@ -2250,7 +2294,7 @@ async function handleChat(args: string[]): Promise { toolArgs["model"] = model; } const result = await callAiTool("chat", "opera_chat", toolArgs); - checkAiResultForSignInError("chat", result); + checkAiResultForCdpError("chat", result); return formatMcpResult("result", result, []); } @@ -2263,7 +2307,7 @@ async function handleInvokeDo(args: string[]): Promise { } requireNeon("invoke-do"); const result = await callAiTool("invoke-do", "opera_do", { prompt }); - checkAiResultForSignInError("invoke-do", result); + checkAiResultForCdpError("invoke-do", result); return formatMcpResult("result", result, []); } @@ -2276,7 +2320,7 @@ async function handleMake(args: string[]): Promise { } requireNeon("make"); const result = await callAiTool("make", "opera_make", { prompt }); - checkAiResultForSignInError("make", result); + checkAiResultForCdpError("make", result); return formatMcpResult("result", result, []); } @@ -2339,7 +2383,7 @@ async function handleResearch(args: string[]): Promise { const toolArgs: Record = { prompt }; if (researchType !== undefined) toolArgs.researchType = researchType; const result = await callAiTool("research", "opera_research", toolArgs); - checkAiResultForSignInError("research", result); + checkAiResultForCdpError("research", result); return formatMcpResult("result", result, []); } From 1bf53adf9cdb535ec8bb04a86d7206eb23664f8a Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Thu, 28 May 2026 11:20:59 +0200 Subject: [PATCH 13/14] refactor(cli): centralize error message handling with constants for improved readability --- src/cli.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index e4c20ae..fc5f7d9 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -38,6 +38,14 @@ const HOME_DESCRIPTION = const VERSION = readPackageVersion(); const RAW_STDOUT_MARKER = "__OPERA_BROWSER_CLI_RAW__"; + +const CdpResultErrorKey = { + NOT_SIGNED_IN: "[OPERA_CDP_ERR:NOT_SIGNED_IN]", + SUBSCRIPTION_REQUIRED: "[OPERA_CDP_ERR:SUBSCRIPTION_REQUIRED]", + CONSENT_REQUIRED: "[OPERA_CDP_ERR:CONSENT_REQUIRED]", + NEON_ONLY: "[OPERA_CDP_ERR:NEON_ONLY]", +} as const; + type CliStdout = Pick; export type MainOptions = { @@ -2198,9 +2206,7 @@ interface CdpResultErrorDescriptor { */ const CDP_RESULT_ERRORS: readonly CdpResultErrorDescriptor[] = [ { - match: (r) => - r.includes("User is not signed in") || - (r.includes("Opera.dispatchAction") && r.includes("not signed in")), + match: (r) => r.includes(CdpResultErrorKey.NOT_SIGNED_IN), message: "Opera: user is not signed in", code: "BROWSER_ERROR", suggestions: (cmd) => [ @@ -2209,7 +2215,7 @@ const CDP_RESULT_ERRORS: readonly CdpResultErrorDescriptor[] = [ ], }, { - match: (r) => r.includes("Subscription required"), + match: (r) => r.includes(CdpResultErrorKey.SUBSCRIPTION_REQUIRED), message: "Opera: an active subscription is required", code: "BROWSER_ERROR", suggestions: (cmd) => [ @@ -2218,7 +2224,7 @@ const CDP_RESULT_ERRORS: readonly CdpResultErrorDescriptor[] = [ ], }, { - match: (r) => r.includes("User consent required"), + match: (r) => r.includes(CdpResultErrorKey.CONSENT_REQUIRED), message: "Opera: user consent has not been accepted", code: "BROWSER_ERROR", suggestions: (cmd) => [ @@ -2227,7 +2233,7 @@ const CDP_RESULT_ERRORS: readonly CdpResultErrorDescriptor[] = [ ], }, { - match: (r) => r.includes("is only available on Opera Neon"), + match: (r) => r.includes(CdpResultErrorKey.NEON_ONLY), message: (cmd) => `Opera: ${cmd} is only available on Opera Neon`, code: "BROWSER_ERROR", suggestions: () => [ From a3a078e02e434afc7e2c54959c31d9ed1f35c5fb Mon Sep 17 00:00:00 2001 From: Patryk Srednicki Date: Thu, 28 May 2026 13:35:41 +0200 Subject: [PATCH 14/14] refactor(cli): streamline model handling by requiring neon and improving error checking --- src/cli.ts | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index fc5f7d9..18691bf 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2394,19 +2394,10 @@ async function handleResearch(args: string[]): Promise { } async function handleModels(): Promise { - let raw: string; - try { - raw = await callTool("opera_list_models", {}); - } catch (error) { - if (error instanceof CdpError) { - throw new CdpError( - "Model listing not supported by connected browser. Upgrade Opera or check connection.", - "UNSUPPORTED_OPERATION", - ['Run `opera-browser-cli doctor` to check the connection'], - ); - } - throw error; - } + requireNeon("models"); + const raw = await callTool("opera_list_models", {}); + checkAiResultForCdpError("models", raw); + let data: { models: Array<{ id: string; name: string; isDefault: boolean }> }; try {