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 +15 -0
- package/README.md +15 -13
- package/docs/architecture.md +14 -9
- package/docs/install.md +17 -7
- package/package.json +1 -1
- package/src/installer.js +76 -16
- package/src/mcp-server.js +64 -32
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
|
-
|
|
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
|
-
|
|
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": "
|
|
61
|
-
"args": ["
|
|
61
|
+
"command": "codex-control-chrome-mcp",
|
|
62
|
+
"args": ["--stdio"],
|
|
62
63
|
"startup_timeout_sec": 30
|
|
63
64
|
}
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
### Without a global install
|
|
73
77
|
|
|
74
|
-
|
|
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
|
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
-
->
|
|
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
|
-
##
|
|
192
|
+
## Runtime And Launch Model
|
|
193
193
|
|
|
194
|
-
The
|
|
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": "
|
|
202
|
-
"args": ["
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
```
|
|
52
|
-
|
|
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
|
-
|
|
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": "
|
|
100
|
-
"args": ["
|
|
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
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 =
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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(
|
|
313
|
+
const launcherPath = path.join(appStateDir, launcherFileName(launcherBase, browser));
|
|
267
314
|
const script = `#!/usr/bin/env sh
|
|
268
|
-
|
|
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
|
|
324
|
+
async function createNpxLauncher(appStateDir, browser = DEFAULT_BROWSER) {
|
|
276
325
|
if (process.platform === "win32") {
|
|
277
|
-
|
|
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-
|
|
328
|
+
const launcherPath = path.join(appStateDir, launcherFileName("native-host-launcher", browser));
|
|
283
329
|
const script = `#!/usr/bin/env sh
|
|
284
|
-
exec ${
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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",
|