codex-control-chrome-mcp 1.2.0 → 1.3.0

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,23 @@ This project follows semantic versioning.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.3.0] - 2026-07-03
10
+
11
+ ### Changed
12
+
13
+ - 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.
14
+ - 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).
15
+
16
+ ### Fixed
17
+
18
+ - `package-lock.json` recorded a stale `1.1.0` project version; it now tracks the package version.
19
+
20
+ ## [1.2.1] - 2026-07-03
21
+
22
+ ### Fixed
23
+
24
+ - Native host failed to start on 1.2.0. The install launcher runs the host with `--browser <name>` before the extension origin that Chrome appends, so the origin was no longer `args[0]`. The CLI dispatcher only recognized native-host mode when the origin was `args[0]`, so the launcher's argv fell through to `Unknown command: --browser` and the host exited on every connect. The visible symptoms were the Codex extension connection flapping between connected/disconnected and `status` never listing a bridge socket (`sockets: []`). The dispatcher now detects the origin by shape anywhere in the args, and a regression test covers the launcher argv.
25
+
9
26
  ## [1.2.0] - 2026-06-29
10
27
 
11
28
  ### Added
package/README.md CHANGED
@@ -15,17 +15,6 @@ This is an independent community project. It is not affiliated with OpenAI, Code
15
15
  - Chrome Extension: [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg)
16
16
  - Skill: [`skills/codex-control-chrome-mcp/SKILL.md`](./skills/codex-control-chrome-mcp/SKILL.md)
17
17
 
18
- ## Status
19
-
20
- The current implementation provides:
21
-
22
- - official `@modelcontextprotocol/sdk` stdio server
23
- - Chrome and Microsoft Edge Native Messaging host mode
24
- - install/uninstall commands with manifest backup
25
- - JSON-RPC bridge socket under `/tmp/codex-browser-use`
26
- - Chrome tab, navigation, screenshot, CDP, and event MCP tools
27
- - bundled skill documentation for Agent tool ordering
28
-
29
18
  ## Platform Support
30
19
 
31
20
  | Platform | Status | Notes |
@@ -51,40 +40,42 @@ Commands accept `--browser <chrome\|edge\|all>`. Without the flag, `install-nati
51
40
 
52
41
  Install and enable the [Codex Chrome Extension](https://chromewebstore.google.com/detail/hehggadaopoacecdllhhajmbjkdcmajg) in the Chrome or Edge profile you want to automate.
53
42
 
54
- Install the native host (auto-detects the browsers present):
43
+ Install the package globally, then register the native host (auto-detects the browsers present):
55
44
 
56
45
  ```bash
57
- 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
58
48
  ```
59
49
 
60
- 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:
61
51
 
62
52
  ```bash
63
- npx -y codex-control-chrome-mcp@latest install-native-host --browser edge
53
+ codex-control-chrome-mcp install-native-host --browser edge
64
54
  ```
65
55
 
66
- Configure your Agent to use the MCP stdio server:
56
+ Configure your Agent to use the MCP stdio server through the installed binary:
67
57
 
68
58
  ```json
69
59
  {
70
60
  "type": "stdio",
71
- "command": "npx",
72
- "args": ["-y", "codex-control-chrome-mcp@latest", "--stdio"],
61
+ "command": "codex-control-chrome-mcp",
62
+ "args": ["--stdio"],
73
63
  "startup_timeout_sec": 30
74
64
  }
75
65
  ```
76
66
 
77
- 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:
78
70
 
79
71
  ```bash
80
- 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
81
74
  ```
82
75
 
83
- Uninstall and restore the previous Codex native host manifest:
76
+ ### Without a global install
84
77
 
85
- ```bash
86
- npx -y codex-control-chrome-mcp@latest uninstall-native-host
87
- ```
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.
88
79
 
89
80
  ## Agent Skill
90
81
 
@@ -161,16 +152,12 @@ node ./bin/codex-control-chrome-mcp.js --native-host
161
152
 
162
153
  ## Troubleshooting
163
154
 
164
- `status` reports, per browser (under `install.chrome` / `install.edge`), a `registered` flag and a `classification` of the live manifest. When `registered` is `false` and `classification.kind` is `official-openai`, a Codex update has reclaimed that browser's manifest (see below). `classification` identifies OpenAI's genuine host by stable signals — the `com.openai.codexextension` name, the official extension origin, the bundle path, the binary format, and (on macOS) the OpenAI code signature — not by filename.
165
-
166
155
  ### A Codex update stopped the bridge
167
156
 
168
157
  Codex App/extension updates re-register their own `com.openai.codexextension` native-messaging manifest, which overwrites this project's proxy registration (the bundled host binary has even been renamed across releases). The symptom is `status` showing `sockets: []`, `registered: false`, and a manifest `path` back inside the Codex bundle.
169
158
 
170
159
  The MCP stdio server now **self-heals**: on startup it re-asserts the manifest when it detects it was reverted, so you usually do not need to re-run `install-native-host`. Because Chrome only reads the manifest when the extension reconnects, **reload the Codex Chrome Extension (or restart Chrome) once** after a Codex update to bring the bridge back. Set `CODEX_CONTROL_CHROME_NO_AUTO_REGISTER=1` to disable the auto re-register.
171
160
 
172
- If `status` shows no bridge sockets, reload Chrome or the Codex Chrome Extension after installing the native host.
173
-
174
161
  If Codex App integration stops working, uninstall this native host to restore the backed-up manifest:
175
162
 
176
163
  ```bash
@@ -179,6 +166,8 @@ npx -y codex-control-chrome-mcp@latest uninstall-native-host
179
166
 
180
167
  If tab or CDP tools fail, use the bundled skill workflow: list tabs again, claim only current tab IDs, attach before raw CDP calls, and read CDP events after enabling the relevant domain.
181
168
 
169
+ See [Install And Uninstall](./docs/install.md#troubleshooting) for the full per-browser checklist (the `registered` flag and host `classification`).
170
+
182
171
  ## License
183
172
 
184
173
  MIT
@@ -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,34 +189,35 @@ 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
 
206
- Chrome Native Messaging manifests cannot store command arguments. Therefore `install-native-host` creates a stable launcher script:
207
-
208
- ```sh
209
- npx -y codex-control-chrome-mcp@latest install-native-host
210
- ```
206
+ Chrome Native Messaging manifests cannot store command arguments. Therefore `install-native-host` writes a stable launcher script at:
211
207
 
212
208
  ```text
213
209
  ~/.codex-control-chrome-mcp/native-host-launcher
214
210
  ```
215
211
 
216
- That launcher runs:
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:
217
213
 
218
214
  ```sh
219
- npx -y codex-control-chrome-mcp@1.0.0 --native-host "$@"
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 "$@"
220
219
  ```
221
220
 
222
- 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
@@ -8,13 +8,6 @@ Package:
8
8
 
9
9
  ## Development Status
10
10
 
11
- The current implementation provides:
12
-
13
- - MCP stdio mode: `npx -y codex-control-chrome-mcp@latest --stdio`
14
- - manifest install: `npx -y codex-control-chrome-mcp@latest install-native-host`
15
- - manifest uninstall: `npx -y codex-control-chrome-mcp@latest uninstall-native-host`
16
- - bridge/socket status: `npx -y codex-control-chrome-mcp@latest status`
17
-
18
11
  Do not run install on a machine where Codex Chrome integration is important unless you are ready to test proxy mode or restore from backup.
19
12
 
20
13
  This is an independent community project. It is not affiliated with OpenAI, Codex, Google, or Chrome.
@@ -40,8 +33,11 @@ Commands accept `--browser <chrome|edge|all>`. Without it, `install-native-host`
40
33
 
41
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.
42
35
 
36
+ Install the package globally, then register the native host:
37
+
43
38
  ```bash
44
- 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
45
41
  ```
46
42
 
47
43
  Without `--browser`, this installs for every supported browser whose profile directory exists. Pass `--browser <chrome|edge|all>` to target one explicitly.
@@ -53,12 +49,17 @@ This writes a stable per-browser launcher (Edge uses the `-edge` suffix):
53
49
  ~/.codex-control-chrome-mcp/native-host-launcher-edge
54
50
  ```
55
51
 
56
- 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:
57
53
 
58
- ```bash
59
- 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 "$@"
60
59
  ```
61
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
+
62
63
  For each selected browser the installer:
63
64
 
64
65
  1. Reads the existing `com.openai.codexextension.json` in that browser's directory.
@@ -98,17 +99,19 @@ Example manifest paths on Linux:
98
99
 
99
100
  ## MCP Client Config
100
101
 
101
- Use stdio mode from other Agent tools:
102
+ With the package installed globally, point the client at the installed binary:
102
103
 
103
104
  ```json
104
105
  {
105
106
  "type": "stdio",
106
- "command": "npx",
107
- "args": ["-y", "codex-control-chrome-mcp@latest", "--stdio"],
107
+ "command": "codex-control-chrome-mcp",
108
+ "args": ["--stdio"],
108
109
  "startup_timeout_sec": 30
109
110
  }
110
111
  ```
111
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
+
112
115
  If the Agent supports skills, install or reference the bundled skill folder from this package or repository:
113
116
 
114
117
  ```text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-control-chrome-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
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/cli.js CHANGED
@@ -13,21 +13,19 @@ import { probeLengthPrefixedSocket } from "./probe.js";
13
13
 
14
14
  export async function runCli(argv) {
15
15
  const args = argv.slice(2);
16
- const command = args[0];
16
+ const invocation = classifyInvocation(args);
17
17
 
18
- if (args.includes("--stdio") || command === "stdio") {
18
+ if (invocation.mode === "mcp") {
19
19
  await runMcpServer();
20
20
  return;
21
21
  }
22
22
 
23
- if (args.includes("--native-host") || looksLikeChromeOrigin(command)) {
24
- // Chrome/Edge append the extension origin as a positional arg, so find it
25
- // by shape rather than position (the `--browser` flag shifts the indices).
26
- const origin = args.find(looksLikeChromeOrigin) ?? argv[3];
27
- await runNativeHost({ origin, browser: normalizeBrowser(readOption(args, "--browser")) });
23
+ if (invocation.mode === "native-host") {
24
+ await runNativeHost({ origin: invocation.origin ?? argv[3], browser: invocation.browser });
28
25
  return;
29
26
  }
30
27
 
28
+ const command = invocation.command;
31
29
  switch (command) {
32
30
  case "install-native-host": {
33
31
  const binPath = readOption(args, "--bin");
@@ -78,6 +76,31 @@ export async function runCli(argv) {
78
76
  }
79
77
  }
80
78
 
79
+ /**
80
+ * Decide how an invocation should be handled, purely from the CLI args.
81
+ *
82
+ * Chrome/Edge launch the native host by appending the extension origin as a
83
+ * positional arg, and the install launcher prepends `--browser <name>`. That
84
+ * prefix shifts the origin out of `args[0]`, so both the mode check and the
85
+ * origin lookup must scan by shape rather than position. (A 1.2.0 regression
86
+ * only checked `args[0]` here, so the launcher's `--browser`-prefixed argv fell
87
+ * through to `Unknown command: --browser` and the native host exited on every
88
+ * connect.)
89
+ */
90
+ export function classifyInvocation(args) {
91
+ if (args.includes("--stdio") || args[0] === "stdio") {
92
+ return { mode: "mcp" };
93
+ }
94
+ if (args.includes("--native-host") || args.some(looksLikeChromeOrigin)) {
95
+ return {
96
+ mode: "native-host",
97
+ origin: args.find(looksLikeChromeOrigin),
98
+ browser: normalizeBrowser(readOption(args, "--browser")),
99
+ };
100
+ }
101
+ return { mode: "command", command: args[0] };
102
+ }
103
+
81
104
  function looksLikeChromeOrigin(value) {
82
105
  return typeof value === "string" && value.startsWith("chrome-extension://");
83
106
  }
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
  }