codex-control-chrome-mcp 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ This project follows semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.1] - 2026-07-04
10
+
11
+ ### Fixed
12
+
13
+ - `chrome_evaluate`, `chrome_navigate`, and `chrome_screenshot` now self-heal a stale debugger attachment instead of dead-ending on `Debugger is not attached`. Each tool already attaches before sending CDP commands, but the extension tracks attachments in memory: when Chrome tears an attachment down out-of-band (a competing debugger client, DevTools, or a renderer/process swap), that flag can go stale, so a fresh `attach` short-circuits and reports success without actually re-attaching and the next command fails with `Debugger is not attached`. The tab then stayed wedged — `attach` kept reporting `ok` while every command failed — and only a new tab recovered. These tools now retry once on that specific error by issuing a `detach` (which clears the stale flag; it may itself report "not attached", which is expected and ignored) followed by a real `attach`, so a previously wedged tab recovers automatically. The self-heal wrapper is covered by regression tests. This addresses recovery in the bridge only; the underlying churn comes from multiple debugger clients contending for the same tab, so reducing competing browser-control stacks still helps.
14
+
15
+ ### Changed
16
+
17
+ - The native host launcher no longer runs `npx` on every connection. When installed to a stable location (e.g. `npm install -g codex-control-chrome-mcp`), `install-native-host` writes a launcher that execs Node directly against the installed CLI, so the Codex extension connects immediately instead of waiting for npx to resolve/download the package on each Native Messaging connect. A cold or evicted npx cache could take seconds and exceed the extension's connect timeout, which showed up as the bridge "not connecting". The launcher prefers the Node.js used at install time and falls back to `node` on `PATH`, so a Node upgrade (which moves the versioned Homebrew/nvm path) does not strand it.
18
+ - Recommended setup is now a global install followed by `codex-control-chrome-mcp install-native-host`, with the MCP stdio client pointed at the installed `codex-control-chrome-mcp` binary. Running `npx … install-native-host` still works: because that copy lives in npm's garbage-collectable `_npx` cache, the installer detects it and keeps the previous npx launcher as a fallback (install globally and re-run for the fast path).
19
+
20
+ ### Fixed
21
+
22
+ - `package-lock.json` recorded a stale `1.1.0` project version; it now tracks the package version.
23
+
9
24
  ## [1.2.1] - 2026-07-03
10
25
 
11
26
  ### Fixed
package/README.md CHANGED
@@ -40,40 +40,42 @@ Commands accept `--browser <chrome\|edge\|all>`. Without the flag, `install-nati
40
40
 
41
41
  Install and enable the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) in the Chrome or Edge profile you want to automate.
42
42
 
43
- Install the native host (auto-detects the browsers present):
43
+ Install the package globally, then register the native host (auto-detects the browsers present):
44
44
 
45
45
  ```bash
46
- npx -y codex-control-chrome-mcp@latest install-native-host
46
+ npm install -g codex-control-chrome-mcp
47
+ codex-control-chrome-mcp install-native-host
47
48
  ```
48
49
 
49
- To target one browser explicitly:
50
+ A global install gives the native host a stable path, so its launcher execs Node directly against the installed CLI with no per-connection `npx` cost. To target one browser explicitly:
50
51
 
51
52
  ```bash
52
- npx -y codex-control-chrome-mcp@latest install-native-host --browser edge
53
+ codex-control-chrome-mcp install-native-host --browser edge
53
54
  ```
54
55
 
55
- Configure your Agent to use the MCP stdio server:
56
+ Configure your Agent to use the MCP stdio server through the installed binary:
56
57
 
57
58
  ```json
58
59
  {
59
60
  "type": "stdio",
60
- "command": "npx",
61
- "args": ["-y", "codex-control-chrome-mcp@latest", "--stdio"],
61
+ "command": "codex-control-chrome-mcp",
62
+ "args": ["--stdio"],
62
63
  "startup_timeout_sec": 30
63
64
  }
64
65
  ```
65
66
 
66
- Check status:
67
+ If your MCP client does not inherit a shell `PATH` that includes npm's global bin directory (common for GUI apps), use the absolute path from `npm prefix -g` — for example `/opt/homebrew/bin/codex-control-chrome-mcp` — as `command`.
68
+
69
+ Check status and uninstall:
67
70
 
68
71
  ```bash
69
- npx -y codex-control-chrome-mcp@latest status
72
+ codex-control-chrome-mcp status
73
+ codex-control-chrome-mcp uninstall-native-host # restores the previous Codex native host manifest
70
74
  ```
71
75
 
72
- Uninstall and restore the previous Codex native host manifest:
76
+ ### Without a global install
73
77
 
74
- ```bash
75
- npx -y codex-control-chrome-mcp@latest uninstall-native-host
76
- ```
78
+ `npx -y codex-control-chrome-mcp@latest <command>` still works for every command. Be aware that `npx … install-native-host` writes a launcher that re-runs `npx` on every extension connection; a cold or evicted npx cache can add seconds and may exceed the extension's connect timeout (the bridge then looks stuck "not connecting"). The installer detects the npx temp copy and keeps that launcher as a fallback — install globally and re-run `install-native-host` for the fast, stable path.
77
79
 
78
80
  ## Agent Skill
79
81
 
@@ -107,7 +107,7 @@ If the MCP stdio server is stopped but the native host proxy is still running, C
107
107
  ```text
108
108
  Other Agent
109
109
  -> MCP stdio
110
- -> npx -y codex-control-chrome-mcp@latest --stdio
110
+ -> codex-control-chrome-mcp --stdio
111
111
  -> /tmp/codex-browser-use/codex-control-chrome-mcp-*.sock
112
112
  -> codex-control-chrome-mcp --native-host
113
113
  -> Chrome native messaging stdio
@@ -189,17 +189,17 @@ This project can shim those APIs and point `nativePipe.createConnection()` at it
189
189
 
190
190
  The initial implementation exposes direct MCP tools first. Vendoring `browser-client.mjs` should be a later phase after the native host bridge is stable.
191
191
 
192
- ## NPX Runtime Model
192
+ ## Runtime And Launch Model
193
193
 
194
- The intended distribution model is npm plus NPX, not a single packaged binary.
194
+ The package is distributed on npm and run as its installed CLI. A global install (`npm install -g codex-control-chrome-mcp`) gives both entrypoints — the `--stdio` MCP server and the `--native-host` bridge — a stable path.
195
195
 
196
- Agent-side MCP config should use:
196
+ Agent-side MCP config should use the installed binary:
197
197
 
198
198
  ```json
199
199
  {
200
200
  "type": "stdio",
201
- "command": "npx",
202
- "args": ["-y", "codex-control-chrome-mcp@latest", "--stdio"]
201
+ "command": "codex-control-chrome-mcp",
202
+ "args": ["--stdio"]
203
203
  }
204
204
  ```
205
205
 
@@ -209,10 +209,15 @@ Chrome Native Messaging manifests cannot store command arguments. Therefore `ins
209
209
  ~/.codex-control-chrome-mcp/native-host-launcher
210
210
  ```
211
211
 
212
- That launcher runs (the `--browser` flag tells the host which browser's proxy config to read):
212
+ The launcher execs Node directly against the installed CLI (no `npx` in the hot path, so the extension connects without waiting on a package resolve/download) and injects the mode and browser. The `--browser` flag tells the host which browser's proxy config to read:
213
213
 
214
214
  ```sh
215
- npx -y codex-control-chrome-mcp@latest --native-host --browser chrome "$@"
215
+ #!/usr/bin/env sh
216
+ node_bin='/path/to/node'
217
+ [ -x "$node_bin" ] || node_bin=node
218
+ exec "$node_bin" '/path/to/codex-control-chrome-mcp/bin/codex-control-chrome-mcp.js' --native-host --browser chrome "$@"
216
219
  ```
217
220
 
218
- The native host manifest points to the launcher, not directly to `npx`.
221
+ It prefers the Node.js used at install time and falls back to `node` on `PATH` so a Node upgrade does not strand it. The native host manifest points to the launcher, not to the CLI directly (a manifest cannot pass `--browser`).
222
+
223
+ When the CLI is instead run through `npx` — its copy lives in npm's garbage-collectable `_npx` cache — `install-native-host` detects that and falls back to a launcher that runs `npx -y codex-control-chrome-mcp@latest --native-host …`. That is stable but pays npx's startup cost on every connect; a global install avoids it.
package/docs/install.md CHANGED
@@ -33,8 +33,11 @@ Commands accept `--browser <chrome|edge|all>`. Without it, `install-native-host`
33
33
 
34
34
  First install and enable the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) in the Chrome or Edge profile you want to automate.
35
35
 
36
+ Install the package globally, then register the native host:
37
+
36
38
  ```bash
37
- npx -y codex-control-chrome-mcp@latest install-native-host
39
+ npm install -g codex-control-chrome-mcp
40
+ codex-control-chrome-mcp install-native-host
38
41
  ```
39
42
 
40
43
  Without `--browser`, this installs for every supported browser whose profile directory exists. Pass `--browser <chrome|edge|all>` to target one explicitly.
@@ -46,12 +49,17 @@ This writes a stable per-browser launcher (Edge uses the `-edge` suffix):
46
49
  ~/.codex-control-chrome-mcp/native-host-launcher-edge
47
50
  ```
48
51
 
49
- The launcher invokes (the `--browser` flag tells the host which browser's config to read for proxy mode):
52
+ Chrome/Edge Native Messaging manifests cannot carry arguments, so the manifest points at this launcher rather than the CLI directly. The launcher execs Node against the installed CLI — no `npx` in the hot path, so the extension connects immediately — and injects `--native-host --browser <name>` (the `--browser` flag tells the host which browser's config to read for proxy mode). It prefers the Node.js used at install time and falls back to `node` on `PATH`, so a Node upgrade does not strand it:
50
53
 
51
- ```bash
52
- npx -y codex-control-chrome-mcp@latest --native-host --browser chrome "$@"
54
+ ```sh
55
+ #!/usr/bin/env sh
56
+ node_bin='/path/to/node'
57
+ [ -x "$node_bin" ] || node_bin=node
58
+ exec "$node_bin" '/usr/local/lib/node_modules/codex-control-chrome-mcp/bin/codex-control-chrome-mcp.js' --native-host --browser chrome "$@"
53
59
  ```
54
60
 
61
+ If you instead run `npx … install-native-host`, the package resolves inside npm's garbage-collectable `_npx` cache, so the installer falls back to a launcher that runs `npx -y codex-control-chrome-mcp@latest --native-host …` on every connect. That works but pays npx's startup cost each time (a cold cache can exceed the extension's connect timeout); install globally and re-run for the direct launcher.
62
+
55
63
  For each selected browser the installer:
56
64
 
57
65
  1. Reads the existing `com.openai.codexextension.json` in that browser's directory.
@@ -91,17 +99,19 @@ Example manifest paths on Linux:
91
99
 
92
100
  ## MCP Client Config
93
101
 
94
- Use stdio mode from other Agent tools:
102
+ With the package installed globally, point the client at the installed binary:
95
103
 
96
104
  ```json
97
105
  {
98
106
  "type": "stdio",
99
- "command": "npx",
100
- "args": ["-y", "codex-control-chrome-mcp@latest", "--stdio"],
107
+ "command": "codex-control-chrome-mcp",
108
+ "args": ["--stdio"],
101
109
  "startup_timeout_sec": 30
102
110
  }
103
111
  ```
104
112
 
113
+ If the client does not inherit a `PATH` that includes npm's global bin directory (common for GUI apps), use the absolute path from `npm prefix -g` (e.g. `/opt/homebrew/bin/codex-control-chrome-mcp`) as `command`. Without a global install, `"command": "npx"` with `"args": ["-y", "codex-control-chrome-mcp@latest", "--stdio"]` also works.
114
+
105
115
  If the Agent supports skills, install or reference the bundled skill folder from this package or repository:
106
116
 
107
117
  ```text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-control-chrome-mcp",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "MCP server and native host bridge for controlling Chrome through the Codex Chrome extension.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/installer.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import {
4
5
  APP_STATE_DIR,
5
6
  CODEX_EXTENSION_ID,
@@ -26,15 +27,19 @@ export async function installNativeHost({
26
27
  proxy = true,
27
28
  browser = DEFAULT_BROWSER,
28
29
  paths = defaultInstallPaths(browser),
30
+ selfBinPath = resolveSelfBinPath(),
29
31
  } = {}) {
30
32
  const { manifestPath, appStateDir, configPath, installStatePath } = paths;
31
33
  const browserId = paths.browser ?? browser;
32
34
  await fs.mkdir(path.dirname(manifestPath), { recursive: true });
33
35
  await fs.mkdir(appStateDir, { recursive: true });
34
36
 
35
- const hostPath = binPath
36
- ? await createLocalLauncher(path.resolve(binPath), appStateDir, browserId)
37
- : await createNpxLauncher(appStateDir, browserId);
37
+ const { hostPath, launchMode } = await createLauncher({
38
+ binPath,
39
+ selfBinPath,
40
+ appStateDir,
41
+ browser: browserId,
42
+ });
38
43
  const existing = await readJsonIfExists(manifestPath);
39
44
  const previousState = await readJsonIfExists(installStatePath);
40
45
  const previousBackup = previousState?.backupPath
@@ -68,7 +73,7 @@ export async function installNativeHost({
68
73
  manifestPath,
69
74
  hostPath,
70
75
  binPath: binPath ? path.resolve(binPath) : null,
71
- launchMode: binPath ? "direct-bin" : "npx-launcher",
76
+ launchMode,
72
77
  backupPath,
73
78
  officialHostPath,
74
79
  proxy,
@@ -252,6 +257,39 @@ async function backupManifest(manifestPath) {
252
257
  return backupPath;
253
258
  }
254
259
 
260
+ /**
261
+ * Choose how the Native Messaging host is launched:
262
+ * - `--bin <path>`: a caller-supplied CLI entry (local dev) -> direct launcher.
263
+ * - installed to a stable location (e.g. `npm i -g`): our own resolved entry
264
+ * -> direct launcher, so every connect skips npx's resolve/download cost.
265
+ * - run from npx's temp cache (`npx … install-native-host`): that copy can be
266
+ * garbage-collected, so a launcher must not point at it -> npx launcher
267
+ * (stable, but pays npx startup on every connect). Install globally and
268
+ * re-run for the fast path.
269
+ */
270
+ async function createLauncher({ binPath, selfBinPath, appStateDir, browser }) {
271
+ if (binPath) {
272
+ const hostPath = await createDirectLauncher(
273
+ path.resolve(binPath),
274
+ appStateDir,
275
+ browser,
276
+ "native-host-local-launcher",
277
+ );
278
+ return { hostPath, launchMode: "direct-bin" };
279
+ }
280
+ if (isNpxCachePath(selfBinPath)) {
281
+ const hostPath = await createNpxLauncher(appStateDir, browser);
282
+ return { hostPath, launchMode: "npx-launcher" };
283
+ }
284
+ const hostPath = await createDirectLauncher(
285
+ selfBinPath,
286
+ appStateDir,
287
+ browser,
288
+ "native-host-launcher",
289
+ );
290
+ return { hostPath, launchMode: "global-bin" };
291
+ }
292
+
255
293
  // Each browser gets its own launcher so the native host knows which browser's
256
294
  // config to read for proxy mode. Chrome keeps the original unsuffixed filename
257
295
  // so manifests written before multi-browser support still resolve.
@@ -259,35 +297,57 @@ function launcherFileName(base, browser) {
259
297
  return browser === DEFAULT_BROWSER ? base : `${base}-${browser}`;
260
298
  }
261
299
 
262
- async function createNpxLauncher(appStateDir, browser = DEFAULT_BROWSER) {
263
- if (process.platform === "win32") {
264
- throw new Error("NPX native host launcher installation is not implemented for Windows yet.");
300
+ /**
301
+ * A tiny shell shim that execs the installed Node CLI directly (no npx), so the
302
+ * host starts in milliseconds instead of paying npx's resolve/download cost on
303
+ * every Native Messaging connect. It prefers the Node.js used at install time
304
+ * and falls back to `node` on PATH, so a Node upgrade (which moves the versioned
305
+ * Homebrew/nvm path in process.execPath) does not strand the launcher. Chrome/
306
+ * Edge Native Messaging manifests cannot carry arguments, so `--native-host` and
307
+ * `--browser` are injected here.
308
+ */
309
+ async function createDirectLauncher(binPath, appStateDir, browser, launcherBase) {
310
+ if (process.platform === "win32" || !binPath.endsWith(".js")) {
311
+ return binPath;
265
312
  }
266
- const launcherPath = path.join(appStateDir, launcherFileName("native-host-launcher", browser));
313
+ const launcherPath = path.join(appStateDir, launcherFileName(launcherBase, browser));
267
314
  const script = `#!/usr/bin/env sh
268
- exec npx -y ${PACKAGE_NAME}@${PACKAGE_VERSION} --native-host --browser ${browser} "$@"
315
+ node_bin=${shellQuote(process.execPath)}
316
+ [ -x "$node_bin" ] || node_bin=node
317
+ exec "$node_bin" ${shellQuote(binPath)} --native-host --browser ${browser} "$@"
269
318
  `;
270
319
  await fs.writeFile(launcherPath, script, { encoding: "utf8", mode: 0o755 });
271
320
  await fs.chmod(launcherPath, 0o755);
272
321
  return launcherPath;
273
322
  }
274
323
 
275
- async function createLocalLauncher(binPath, appStateDir, browser = DEFAULT_BROWSER) {
324
+ async function createNpxLauncher(appStateDir, browser = DEFAULT_BROWSER) {
276
325
  if (process.platform === "win32") {
277
- return binPath;
278
- }
279
- if (!binPath.endsWith(".js")) {
280
- return binPath;
326
+ throw new Error("NPX native host launcher installation is not implemented for Windows yet.");
281
327
  }
282
- const launcherPath = path.join(appStateDir, launcherFileName("native-host-local-launcher", browser));
328
+ const launcherPath = path.join(appStateDir, launcherFileName("native-host-launcher", browser));
283
329
  const script = `#!/usr/bin/env sh
284
- exec ${shellQuote(process.execPath)} ${shellQuote(binPath)} --browser ${browser} "$@"
330
+ exec npx -y ${PACKAGE_NAME}@${PACKAGE_VERSION} --native-host --browser ${browser} "$@"
285
331
  `;
286
332
  await fs.writeFile(launcherPath, script, { encoding: "utf8", mode: 0o755 });
287
333
  await fs.chmod(launcherPath, 0o755);
288
334
  return launcherPath;
289
335
  }
290
336
 
337
+ /**
338
+ * Absolute path to this package's own CLI entry, resolved from the installed
339
+ * module location so a global (or otherwise stable) install yields a launcher
340
+ * target that survives package upgrades.
341
+ */
342
+ function resolveSelfBinPath() {
343
+ return fileURLToPath(new URL("../bin/codex-control-chrome-mcp.js", import.meta.url));
344
+ }
345
+
346
+ /** True when a path lives inside npm's `_npx` temp cache (subject to GC). */
347
+ function isNpxCachePath(filePath) {
348
+ return filePath.split(path.sep).includes("_npx");
349
+ }
350
+
291
351
  function shellQuote(value) {
292
352
  return `'${String(value).replaceAll("'", "'\\''")}'`;
293
353
  }
package/src/mcp-server.js CHANGED
@@ -229,15 +229,15 @@ function registerTools(server, runtime) {
229
229
  url: z.string().url(),
230
230
  },
231
231
  },
232
- async ({ tabId, url }) => {
233
- await runtime.request("attach", { tabId });
234
- await executeCdp(runtime, { tabId, method: "Page.enable", params: {} });
235
- return toolResult(await executeCdp(runtime, {
236
- tabId,
237
- method: "Page.navigate",
238
- params: { url },
239
- }));
240
- },
232
+ async ({ tabId, url }) =>
233
+ toolResult(await withReattach(runtime, tabId, async () => {
234
+ await executeCdp(runtime, { tabId, method: "Page.enable", params: {} });
235
+ return await executeCdp(runtime, {
236
+ tabId,
237
+ method: "Page.navigate",
238
+ params: { url },
239
+ });
240
+ })),
241
241
  );
242
242
 
243
243
  server.registerTool(
@@ -250,19 +250,19 @@ function registerTools(server, runtime) {
250
250
  awaitPromise: z.boolean().default(true),
251
251
  },
252
252
  },
253
- async ({ tabId, expression, awaitPromise }) => {
254
- await runtime.request("attach", { tabId });
255
- await executeCdp(runtime, { tabId, method: "Runtime.enable", params: {} });
256
- return toolResult(await executeCdp(runtime, {
257
- tabId,
258
- method: "Runtime.evaluate",
259
- params: {
260
- expression,
261
- awaitPromise,
262
- returnByValue: true,
263
- },
264
- }));
265
- },
253
+ async ({ tabId, expression, awaitPromise }) =>
254
+ toolResult(await withReattach(runtime, tabId, async () => {
255
+ await executeCdp(runtime, { tabId, method: "Runtime.enable", params: {} });
256
+ return await executeCdp(runtime, {
257
+ tabId,
258
+ method: "Runtime.evaluate",
259
+ params: {
260
+ expression,
261
+ awaitPromise,
262
+ returnByValue: true,
263
+ },
264
+ });
265
+ })),
266
266
  );
267
267
 
268
268
  server.registerTool(
@@ -274,16 +274,16 @@ function registerTools(server, runtime) {
274
274
  format: z.enum(["png", "jpeg", "webp"]).default("png"),
275
275
  },
276
276
  },
277
- async ({ tabId, format }) => {
278
- await runtime.request("attach", { tabId });
279
- await executeCdp(runtime, { tabId, method: "Page.enable", params: {} });
280
- return toolResult(await executeCdp(runtime, {
281
- tabId,
282
- method: "Page.captureScreenshot",
283
- params: { format, captureBeyondViewport: true },
284
- timeoutMs: 30_000,
285
- }));
286
- },
277
+ async ({ tabId, format }) =>
278
+ toolResult(await withReattach(runtime, tabId, async () => {
279
+ await executeCdp(runtime, { tabId, method: "Page.enable", params: {} });
280
+ return await executeCdp(runtime, {
281
+ tabId,
282
+ method: "Page.captureScreenshot",
283
+ params: { format, captureBeyondViewport: true },
284
+ timeoutMs: 30_000,
285
+ });
286
+ })),
287
287
  );
288
288
 
289
289
  server.registerTool(
@@ -319,6 +319,38 @@ function registerTools(server, runtime) {
319
319
  );
320
320
  }
321
321
 
322
+ /**
323
+ * Attach the debugger, run a CDP sequence, and self-heal a stale-attach state.
324
+ *
325
+ * The Codex Chrome extension tracks debugger attachments in memory. When Chrome
326
+ * tears an attachment down out-of-band (a competing debugger client, DevTools, a
327
+ * renderer/process swap), that flag can go stale: a fresh `attach` short-circuits
328
+ * and reports success without actually re-attaching, so the next CDP command
329
+ * fails with "Debugger is not attached". A `detach` clears the stale flag (it may
330
+ * itself throw "not attached" — expected, and ignored), after which `attach`
331
+ * re-attaches for real. We retry the sequence once on that specific error.
332
+ */
333
+ export async function withReattach(runtime, tabId, run) {
334
+ await runtime.request("attach", { tabId });
335
+ try {
336
+ return await run();
337
+ } catch (error) {
338
+ if (!isNotAttachedError(error)) throw error;
339
+ try {
340
+ await runtime.request("detach", { tabId });
341
+ } catch {
342
+ // Detach throws when the debugger is already gone; we only need its side
343
+ // effect of clearing the extension's stale attach flag.
344
+ }
345
+ await runtime.request("attach", { tabId });
346
+ return await run();
347
+ }
348
+ }
349
+
350
+ export function isNotAttachedError(error) {
351
+ return /not attached/i.test(error?.message ?? String(error ?? ""));
352
+ }
353
+
322
354
  async function executeCdp(runtime, { tabId, method, params = {}, timeoutMs }) {
323
355
  return await runtime.request(
324
356
  "executeCdp",