npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

chrome-cdp-manager

v2.0.0

Published

Set up and drive a Chrome DevTools Protocol (CDP) instance on macOS, Windows, or Linux through a dedicated launcher (consistent Dock/taskbar/app-menu icon). Works with any Chromium-based browser — Chrome, Edge, Brave, Chromium, Vivaldi, Opera, Arc.

Readme

chrome-cdp-manager

Set up and drive a Chrome DevTools Protocol (CDP) instance on macOS, Windows, or Linux through a dedicated launcher, so launches always go through the same entry point and the Dock/taskbar/app-menu icon stays consistent. Works with any Chromium-based browser — Chrome, Edge, Brave, Chromium, Vivaldi, Opera, Arc (Arc is macOS/Windows only).

  • macOS: builds a real .app bundle (/Applications/Agent.app) with a proper Info.plist and the browser's own icon — headed and headless launches go through open -n so both run under the same Dock icon (easy to spot and quit). Setup also creates a small "Google Chrome" wrapper app in ~/Applications that opens your normal browser profile via open -n, so a regular browser window is always one click away even while the CDP instance runs.
  • Windows: creates a Start Menu .lnk shortcut with the CDP flags baked in and a custom icon — headed launches go through the shortcut for a consistent taskbar entry.
  • Linux: installs a bash launcher under ~/.local/share/chrome-cdp-manager/ plus a freedesktop .desktop entry in ~/.local/share/applications/ so the app menu can show a consistent icon.
  • The raw CDP client uses Node's built-in fetch and WebSocket; the package never downloads or bundles a browser. playwright-core powers the optional Playwright CLI wrapper, while the high-level bridge requires your own compatible playwright installation.

Requires Node.js ≥ 22 and a Chromium-based browser installed. On macOS, Apple Silicon (the launcher uses arch -arm64).

Non-Chromium browsers (Firefox, Safari) are not supported — they don't speak the same CDP this tool drives.

Usage

The package is published as chrome-cdp-manager; the command it installs is chrome-cdp.

# Run without installing — invoke via the package name
npx chrome-cdp-manager setup

# Or install once, then use the `chrome-cdp` command
npm install -g chrome-cdp-manager
# Create / repair the launcher and save defaults (port 9222, profile ~/.chrome_cdp_profile)
chrome-cdp setup

# Use a specific browser
chrome-cdp setup --browser edge

# See which browsers are installed
chrome-cdp browsers

# Launch Agent and open a page
chrome-cdp open https://example.com

# Same, headless (no window/Dock/taskbar)
chrome-cdp open https://example.com --headless

# Route all traffic through a proxy (defaults to socks5://127.0.0.1:1080)
chrome-cdp open https://example.com --proxy
chrome-cdp open https://example.com --proxy socks5://127.0.0.1:9050
chrome-cdp open https://example.com --proxy http://10.0.0.1:8080

# Fetch a page's rendered HTML over CDP (headless by default)
chrome-cdp html example.com -o page.html

# Open your NORMAL browser (default profile) — works even while Agent runs
chrome-cdp chrome
chrome-cdp chrome https://example.com

# Inspect state (shows the live port, and the configured one if they differ)
chrome-cdp status

# Quit the running instance
chrome-cdp stop

Tip: with npx, run it by package name — npx chrome-cdp-manager <command> — since npx chrome-cdp would try to fetch an unrelated chrome-cdp package.

Commands

| Command | Description | | ------------------ | ----------------------------------------------------------------- | | setup | Create or repair the launcher (icon + CDP flags) | | open [url] | Launch via the launcher, optionally open a URL; leaves it running | | html <url> | Navigate and print/save the page's serialized HTML | | chrome [url] | Open your normal (non-CDP) browser, even while Agent runs | | status | Show launcher presence and CDP connection state | | stop | Ask the running Agent instance to quit | | browsers | List supported browsers and which are installed |

Headless sessions vs. your normal browser (macOS)

Headed and headless sessions launch through open -n /Applications/Agent.app, so both run under the Agent Dock icon — one place to find the CDP instance and quit it (right-click → Quit, or chrome-cdp stop).

A CDP session launched from the full browser binary still registers with macOS LaunchServices as that browser. While it runs, clicking your normal browser's own Dock icon (or launching it via Spotlight/Finder) may silently do nothing: macOS sees the browser as "already running". The profiles never conflict — only the app identity does. Ways out:

  1. The "Google Chrome" wrapper appchrome-cdp setup creates ~/Applications/Google Chrome.app (named and iconed after your configured browser). It opens your normal browser profile with open -n, which forces a new instance, so it works even while the CDP instance runs. Drag it to the Dock and keep it next to Agent. Both instances run the same signed browser binary, so both can read the "Chrome Safe Storage" key from the macOS Keychain (saved passwords/cookies decrypt normally in each).
  2. chrome-cdp chrome — the same thing from the terminal. Manual equivalent: open -n -a "Google Chrome".
  3. chrome-cdp stop — quit the CDP instance first. The idle watchdog also auto-quits a forgotten headless instance (--idle-timeout, default 0.5 min).

--headless-shell (a separate chrome-headless-shell binary for headless launches) is still supported on Windows/Linux, but is no longer used on macOS — headless goes through the Agent bundle so it keeps the Dock icon.

If you ever ran sudo chrome-cdp setup: older versions could leave the bundle and config root-owned, breaking later launches with "Permission denied". Fix once with sudo chown -R "$USER" /Applications/Agent.app ~/.config/chrome-cdp-manager. Current versions hand ownership back automatically after a sudo setup.

Programmatic API

Beyond the CLI, the package exposes a typed ES-module API (Node ≥ 22). It ships generated declarations for both the core and Playwright entry points.

import { launch, CdpClient } from "chrome-cdp-manager";

// Ensure the Agent instance is running (launches it if needed) and get its endpoint.
const { endpoint, cdpPort, config, launched } = await launch({ headless: false });

// Drive it with the built-in zero-dependency raw-CDP client...
// (use `cdpPort`, not your requested port — see "Busy CDP port" above)
const cdp = await CdpClient.connect(cdpPort);

Also exported: stopBrowser, getBrowserStatus, probeCdp, openUrl, getPageHtml, closeBrowser, the config helpers loadConfig / resolveConfig / DEFAULTS, the proxy helpers parseProxy / detectProxy (+ the DEFAULT_PROXY* constants), touchActivity, and the EndpointCollisionError / ProfileBusyError classes.

Breaking in 2.0 — internal helpers that were previously re-exported are no longer part of the public API: getLauncher, ensureBrowserRunning, waitForCdp, inspectCdpProcess, detectModeFromCommandLine, commandLineMatchesManager, readProcessCommandLine, findListeningPid, validateConfig, computeDefaults, resolveHeadlessShell, usesHeadlessShell, headlessShellArgs, and the port helpers resolveAvailableCdpPort, canBindPort, portFromWebSocketUrl, readDevToolsActivePort, CDP_PORT_SCAN_LIMIT. Reach the same behaviour through launch(), stopBrowser(), and getBrowserStatus().

import { getBrowserStatus } from "chrome-cdp-manager";

const status = await getBrowserStatus();
if (status.running) {
  console.log(status.headless); // true | false
  console.log(status.owner);    // "manager" | "foreign"
  console.log(status.pid, status.modeSource);
}

getBrowserStatus() does not launch or stop the browser. Mode prefers the live process argv (--headless / headless-shell); owner is "manager" only when the endpoint matches this profile (session, DevToolsActivePort/pid proof, or argv --user-data-dir + --remote-debugging-port).

Playwright bridge (optional)

If you want Playwright's high-level page API, opt into the separate entry point. playwright is an optional peer dependency — no browser binary is installed by this package.

import { connect } from "chrome-cdp-manager/playwright";

await using session = await connect({ headless: false, match: u => u.includes("bing.com") });
await session.page.goto("https://www.bing.com");
// `await using` detaches the CDP channel on scope exit; the browser keeps running.

connect() resolves config, ensures the launcher + a running browser, connects Playwright over CDP, and returns { browser, context, page, config }. In zero-install (npx) setups where Playwright can't be resolved automatically, inject it: connect({ chromium }).

Surviving sleep / dropped connections

The CDP channel is a websocket from your process to Chrome, and it does not survive a host sleep/suspend — macOS tears the socket down, so on wake every page/context/browser handle throws Target page, context or browser has been closed, even though Chrome itself is still running.

connect() handles this for you: by default (autoReconnect: true) it re-launches Chrome if needed and re-attaches with backoff, refreshing the live handles behind the session.browser / session.context / session.page getters. The one call that was in flight when the socket dropped still rejects (Playwright can't replay it) — so wrap each unit of work in session.run(), which catches a connection-closed error, reconnects, and retries:

await using session = await connect({ match: u => u.includes("bing.com") });

for (const q of queries) {
  // Derive locators INSIDE run(): each attempt gets a fresh, live page.
  await session.run(async (page) => {
    const box = page.locator('textarea[name="q"]').first();
    await box.fill(q);
    await page.keyboard.press("Enter");
    await page.waitForURL(u => u.searchParams.get("q") === q);
  });
}

The golden rule: derive locators inside run(), never capture them outside — a locator (or page) captured before a reconnect points at a dead handle.

Lower-level hooks if you'd rather drive the recovery yourself: session.page (getter, always the live page), session.whenConnected() (await an in-flight reconnect), session.reconnect() (force one), and onReconnect(session) (fires after each re-attach). The exported isConnectionClosedError(err) tells a dropped socket apart from a genuine automation failure.

Note on the idle watchdog. A headless instance has an auto-quit watchdog that closes it once idle. The watchdog can't see Playwright traffic (it only watches CDP target changes), so a live connect() session bumps an activity heartbeat (every 15s, and on every run()) to keep an actively-driven browser from being closed out from under you. If it is closed anyway — a long idle gap between run() calls, or a sleep past the idle budget — run() simply re-launches it on the next call. To opt out of auto-quit entirely, launch with config: { idleTimeoutMinutes: 0 }.

Playwright CLI (playwright-cli)

The package also installs a playwright-cli command that runs Playwright's MCP CLI client against the Agent instance managed here. It's a thin wrapper that, before handing off, guarantees:

  1. A headed Agent browser is reachable over CDP. If one is already running on the port, it just attaches to it (no relaunch); otherwise it launches a fresh headed instance.

  2. The environment points Playwright at that browser instead of spawning its own:

    PLAYWRIGHT_MCP_CDP_ENDPOINT=http://localhost:9222   # from config (default 9222)
    PLAYWRIGHT_MCP_ISOLATED=false

Everything after playwright-cli is forwarded verbatim to the Playwright CLI:

# Drive the attached browser
playwright-cli goto https://example.com
playwright-cli click "Sign in"

# Help / version forward straight through (no browser launch)
playwright-cli --help

The Playwright CLI ships via playwright-core (a dependency of this package, so it works out of the box — no browser download). The endpoint port follows your saved config (~/.config/chrome-cdp-manager/config.json).

Common options

| Option | Description | | ----------------------- | ------------------------------------------------------------- | | --port <port> | CDP port (default 9222) | | -p, --profile <dir> | Browser user-data-dir (default ~/.chrome_cdp_profile) | | -b, --browser <name> | Browser: chrome, edge, brave, chromium, vivaldi, opera, arc | | --path <path> | Explicit browser executable (overrides --browser) | | --target <path> | Launcher location (.app / .lnk / Linux script) | | --proxy [server] | Route traffic through a proxy; omit the value for socks5://127.0.0.1:1080. Accepts a port (1080), host:port, or scheme://host:port (socks5, socks4, http, https) | | --no-proxy | Disable any configured proxy for this run | | -t, --timeout <secs> | Startup / load timeout (open, html) | | --idle-timeout <min> | Auto-quit an idle headless instance after N minutes; 0 disables (default 0.5, i.e. 30 seconds). Persist with setup; override per-run on open / html |

-c, --chrome and --bundle remain as aliases for --path and --target.

Browser, port, profile and launcher path are baked into the launcher and persisted (~/.config/chrome-cdp-manager/config.json) at setup time so every command agrees on the same environment. Re-run setup --force after changing them.

Busy CDP port

If the configured port (default 9222) is already taken — a foreign CDP browser, a proxy, anything — the launch moves to the next free port above it (up to 100 ports) instead of failing or attaching to something it does not own:

$ chrome-cdp open https://example.com
Port 9222 busy; launched on CDP port 9223 instead.
DevTools: http://127.0.0.1:9223/json

The switch is per launch: neither the saved config nor the launcher is rewritten, so the next cold start tries 9222 again. Later commands still find the instance — status, html and stop look at the configured port, the session sidecar, and Chrome's own DevToolsActivePort file. Programmatic callers read the live port from the launch result:

const { cdpPort, requestedPort, portSwitched, endpoint } = await launch();
// portSwitched === true  → this call launched past a busy requestedPort
// cdpPort !== requestedPort with portSwitched === false
//                        → an instance was already running over there

getBrowserStatus() mirrors this with config.cdpPort (live), requestedPort and portChanged. Pass portScanLimit to launch() to widen or narrow the search.

The free-port check and the browser's own bind are not atomic. If something grabs the port in between, the browser starts without a debugging port and the launch fails with an error saying exactly that — close it and retry.

Proxy

--proxy passes Chrome's --proxy-server flag at launch so traffic routes through it. The default is a local SOCKS5 proxy on port 1080 (the common ssh -D / shadowsocks default). The proxy is a per-launch flag — it is not baked into the launcher, so a normal (non-proxied) launch is never affected by it. Before launching, the proxy port is probed and a warning is printed if nothing is listening; status reports the configured proxy and whether it's reachable. Persist a default proxy with chrome-cdp setup --proxy …, or opt out of a persisted proxy for a single run with --no-proxy.

Idle auto-shutdown

A headless instance has no window, so it's easy to forget one running in the background where it competes with your normal Chrome. When a headless instance is launched, a small detached watchdog quits it once it has been idle — no new tabs, navigations, or CLI commands — for --idle-timeout minutes (0.5 by default, i.e. 30 seconds; set 1, 10, fractions like 0.5, etc., or 0 to disable). Headed instances are never auto-quit — you manage those directly. Persist a default with chrome-cdp setup --idle-timeout 10; status shows the effective timeout and whether a watchdog is currently active.

How the icon stays consistent

macOS — the bundle's executable is a small bash launcher:

exec arch -arm64 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --remote-debugging-port=9222 \
  --user-data-dir="$HOME/.chrome_cdp_profile" \
  "$@"

Headed and headless launches go through open -n /Applications/Agent.app, so LaunchServices runs the browser under the Agent bundle identity — a consistent Dock icon every time instead of a generic/duplicated entry, and one icon to quit whichever mode is running.

Windows — a Start Menu .lnk shortcut (Agent.lnk) is created with the browser as its target, the CDP flags as its arguments, and the browser's icon. Because the dedicated --user-data-dir gives the browser its own AppUserModelID, the Agent window gets its own pinnable taskbar entry, separate from your everyday browser windows.

Linux — a bash launcher at ~/.local/share/chrome-cdp-manager/Agent pins the same CDP flags and profile (including the headless relaunch guard). A companion ~/.local/share/applications/chrome-cdp.desktop entry makes it show up in the app menu with the browser's icon when available.

Development

The source, CLI, scripts, and tests are written in strict TypeScript 7.0. The published package contains compiled ESM JavaScript and declaration files under dist/.

npm ci
npm run check          # strict type-check, including tests and build scripts
npm test               # unit tests
npm run test:coverage  # CI coverage thresholds
npm run build          # compile ESM + declarations
npm run pack:check     # validate the npm package contents

CI runs on Ubuntu and Windows with Node.js 22.