diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3853ba..8481ae6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,4 +18,9 @@ jobs: cache: npm - run: npm ci + + - name: Disable AppArmor + shell: bash + run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns + - run: npm test diff --git a/README.md b/README.md index 9bcbd03..d151c00 100644 --- a/README.md +++ b/README.md @@ -197,9 +197,10 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles - [`get_memory_snapshot_details`](docs/tool-reference.md#get_memory_snapshot_details) - [`get_nodes_by_class`](docs/tool-reference.md#get_nodes_by_class) - [`load_memory_snapshot`](docs/tool-reference.md#load_memory_snapshot) -- **Opera** (4 tools) +- **Opera** (5 tools) - [`opera_chat`](docs/tool-reference.md#opera_chat) - [`opera_do`](docs/tool-reference.md#opera_do) + - [`opera_list_models`](docs/tool-reference.md#opera_list_models) - [`opera_make`](docs/tool-reference.md#opera_make) - [`opera_research`](docs/tool-reference.md#opera_research) - **Extensions** (5 tools) diff --git a/docs/tool-reference.md b/docs/tool-reference.md index 2985f4a..b39949d 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -44,9 +44,10 @@ - [`get_memory_snapshot_details`](#get_memory_snapshot_details) - [`get_nodes_by_class`](#get_nodes_by_class) - [`load_memory_snapshot`](#load_memory_snapshot) -- **[Opera](#opera)** (4 tools) +- **[Opera](#opera)** (5 tools) - [`opera_chat`](#opera_chat) - [`opera_do`](#opera_do) + - [`opera_list_models`](#opera_list_models) - [`opera_make`](#opera_make) - [`opera_research`](#opera_research) - **[Extensions](#extensions)** (5 tools) @@ -500,6 +501,7 @@ in the DevTools Elements panel (if any). **Parameters:** - **prompt** (string) **(required)**: The prompt to send to Opera AI. +- **model** (string) _(optional)_: Model ID to use for the chat. Omit to use the browser default. Use [`opera_list_models`](#opera_list_models) to discover available IDs. --- @@ -513,6 +515,14 @@ in the DevTools Elements panel (if any). --- +### `opera_list_models` + +**Description:** List available AI models for Opera chat. Returns model IDs, display names, and which is the default. Only available when connected to Opera Neon. + +**Parameters:** None + +--- + ### `opera_make` **Description:** Ask Opera's built-in AI to create or generate content and return the result. Only available when connected to Opera Neon. diff --git a/package-lock.json b/package-lock.json index abf0d4d..474c680 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "opera-devtools-mcp", - "version": "0.2.0", + "version": "0.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opera-devtools-mcp", - "version": "0.2.0", + "version": "0.2.2", "license": "Apache-2.0", "bin": { "opera-devtools": "build/src/bin/opera-devtools.js", diff --git a/src/bin/chrome-devtools-cli-options.ts b/src/bin/chrome-devtools-cli-options.ts index 7ef2827..b57d58a 100644 --- a/src/bin/chrome-devtools-cli-options.ts +++ b/src/bin/chrome-devtools-cli-options.ts @@ -477,6 +477,13 @@ export const commands: Commands = { 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, + }, }, }, opera_do: { @@ -492,6 +499,12 @@ export const commands: Commands = { }, }, }, + opera_list_models: { + description: + 'List available AI models for Opera chat. Returns model IDs, display names, and which is the default. Only available when connected to Opera Neon.', + category: 'Opera', + args: {}, + }, opera_make: { description: "Ask Opera's built-in AI to create or generate content and return the result. Only available when connected to Opera Neon.", diff --git a/src/telemetry/tool_call_metrics.json b/src/telemetry/tool_call_metrics.json index be3e38a..2a1a53e 100644 --- a/src/telemetry/tool_call_metrics.json +++ b/src/telemetry/tool_call_metrics.json @@ -634,6 +634,10 @@ { "name": "prompt_length", "argType": "number" + }, + { + "name": "model_length", + "argType": "number" } ] }, @@ -667,5 +671,9 @@ "argType": "string" } ] + }, + { + "name": "opera_list_models", + "args": [] } ] diff --git a/src/tools/opera.ts b/src/tools/opera.ts index d68f58f..1b0a086 100644 --- a/src/tools/opera.ts +++ b/src/tools/opera.ts @@ -20,7 +20,7 @@ const getCDPSession = (page: {_client(): CDPSession}): CDPSession => page._client(); const MAX_SW_RETRIES = 5; -const SW_RETRY_DELAY_MS = 1000; +const SW_RETRY_DELAY_MS = 2500; const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); @@ -129,15 +129,25 @@ export const operaChat = definePageTool({ }, 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 result = await dispatchAction(session, { + 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( @@ -261,3 +271,27 @@ export const operaResearch = definePageTool({ } }, }); + +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. Only available when connected to Opera Neon.', + 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 dispatchAction(session, {action: 'listModels'}); + response.appendResponseLine(result); + } catch (e) { + response.appendResponseLine( + `Opera.dispatchAction(listModels) failed with error: ${(e as Error).message}`, + ); + } + }, +});