cdp-toolkit 3.0.1 → 3.0.3
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 +18 -0
- package/README.md +2 -2
- package/dist/cli.js +62 -12
- package/dist/index.js +36 -3
- package/dist/mcp.js +37 -4
- package/package.json +1 -1
- package/src/cdp/driver.ts +51 -2
- package/src/cli.ts +27 -7
- package/src/install/browser.ts +9 -3
- package/src/install/wizard.ts +8 -8
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to cdp-toolkit are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [3.0.3] - 2026-10-29
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- `new_page` (and `claim_page` opening a fresh tab) wedged the tab in Vivaldi: blank content, an empty URL, a renderer that never answered again, and a 15 s `new_page` timeout under `claim:true`, until a person reloaded it. Vivaldi breaks any tab that something attaches to in its first few milliseconds, and the activity beacon attached straight after `Target.createTarget`. `newPage` now waits for the tab's first `Target.targetInfoChanged` (capped at 2 s) before returning. Verified live on Vivaldi 8.2 (Chromium 152) through `new_page {claim:true}`: on 3.0.2 two of two Fidelity tabs hung; with the fix three of three loaded in 230 to 390 ms with the beacon installed. (`src/cdp/driver.ts` `createSettledTarget`, `test/new-page-settle.test.ts`)
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `vivaldi` is now a supported `cdp install --browser` choice, wired like `arc`: binary detection (`VIVALDI_BIN`, `/Applications/Vivaldi.app/Contents/MacOS/Vivaldi`, `vivaldi` on PATH, `/usr/bin/vivaldi`), the `cdp-vivaldi` alias, the CLI choice list and help text, and the README. Vivaldi is Chromium, so it takes the same launch flags and the server env is the same as chrome and arc. (`src/install/browser.ts`, `src/install/wizard.ts`, `test/install-browser.test.ts`, `test/install-wizard.test.ts`)
|
|
19
|
+
|
|
20
|
+
## [3.0.2] - 2026-10-03
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- CLI: `get_network_request --requestId <id>` failed with "no network request matched" for every Fetch/XHR request. CDP mints those ids as `<pid>.<n>` (e.g. `91775.34`), and the CLI coerced any numeric-looking flag value to a number, so the lookup compared a number against the stored string, and `91775.10` was mangled to `91775.1`. Flag coercion now follows the tool's schema: a key typed as a string keeps its raw text, and every other key is coerced as before. Verified live on headless Chrome 154: all 6 captured requests resolve by `--requestId` from both the source and the built CLI; on 3.0.1 the 5 Fetch requests failed and only the hex-id Document request resolved. MCP callers were not affected, since their arguments arrive already typed. (`src/cli.ts`, `test/cli-coerce.test.ts`)
|
|
25
|
+
|
|
8
26
|
## [3.0.1] - 2026-09-29
|
|
9
27
|
|
|
10
28
|
### Fixed
|
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ One honest caveat the benchmark also reports: while a navigation to a dead endpo
|
|
|
76
76
|
npx -y --package cdp-toolkit cdp install # or `cdp install` from a clone / after a global install
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
It asks for a harness (`claude` | `codex` | `opencode`), a debug port, and a browser (`arc` | `chrome` | `firefox`), then writes an idempotent marker block into your `~/.zshrc`/`~/.bashrc`. The same flags drive it non-interactively: `--harness --browser --port --name --no-alias --yes`. The Firefox alias it writes includes `--marionette` (required for cdp-toolkit's orphaned-session auto-recovery — see [Firefox](#firefox-webdriver-bidi)). It is a CLI subcommand, **not** a 49th tool: `cdp --list` still shows 48.
|
|
79
|
+
It asks for a harness (`claude` | `codex` | `opencode`), a debug port, and a browser (`arc` | `chrome` | `firefox` | `vivaldi`), then writes an idempotent marker block into your `~/.zshrc`/`~/.bashrc`. The same flags drive it non-interactively: `--harness --browser --port --name --no-alias --yes`. The Firefox alias it writes includes `--marionette` (required for cdp-toolkit's orphaned-session auto-recovery — see [Firefox](#firefox-webdriver-bidi)). It is a CLI subcommand, **not** a 49th tool: `cdp --list` still shows 48.
|
|
80
80
|
|
|
81
81
|
Prefer to wire it by hand? The manual path is three steps:
|
|
82
82
|
|
|
@@ -218,7 +218,7 @@ cdp take_screenshot --target url:example --renderWidth 1920 --renderHeight 1080
|
|
|
218
218
|
cdp lighthouse_audit --url https://example.com --json '{"categories":["performance"]}'
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
**Argument parsing:** the first positional token is the tool name. `--json '<obj>'` merges a JSON object into the args (applied first). `--target <sel>` sets `args.target`. Repeated `--key value` pairs become `args.key
|
|
221
|
+
**Argument parsing:** the first positional token is the tool name. `--json '<obj>'` merges a JSON object into the args (applied first). `--target <sel>` sets `args.target`. Repeated `--key value` pairs become `args.key`. A key the tool's schema types as a string keeps its raw text (so `--requestId 91775.10` stays `"91775.10"`); any other key is coerced (`true`/`false` → boolean, numeric strings → number, else string). A bare `--flag` is `true`. Explicit flags override keys from `--json`. Output is `JSON.stringify(result, null, 2)` on stdout (exit 0); on any throw, `{"error":"<message>"}` goes to stderr and the process exits 1.
|
|
222
222
|
|
|
223
223
|
**`--help`/`-h`** are recognized anywhere in argv, ahead of every other flag, and are never treated as a tool argument or a tool name — as of 1.9.3, `<tool> --help` used to run the tool instead. With no tool named, `cdp --help` prints the usage above. With a tool named, `cdp <tool> --help` prints that tool's arguments (name, type, required/optional, description) read from its schema, and exits 0 having made no CDP connection, taken no lease, and written no file.
|
|
224
224
|
|
package/dist/cli.js
CHANGED
|
@@ -4869,6 +4869,41 @@ function noSuchTarget(e) {
|
|
|
4869
4869
|
throw e;
|
|
4870
4870
|
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
4871
4871
|
}
|
|
4872
|
+
var NEW_TARGET_SETTLE_MS = 2000;
|
|
4873
|
+
async function createSettledTarget(conn, url, settleMs = NEW_TARGET_SETTLE_MS) {
|
|
4874
|
+
const changed = new Set;
|
|
4875
|
+
let id = "";
|
|
4876
|
+
let wake;
|
|
4877
|
+
const off = conn.on("Target.targetInfoChanged", (params) => {
|
|
4878
|
+
const t = params.targetInfo?.targetId;
|
|
4879
|
+
if (!t)
|
|
4880
|
+
return;
|
|
4881
|
+
changed.add(t);
|
|
4882
|
+
if (t === id)
|
|
4883
|
+
wake?.();
|
|
4884
|
+
});
|
|
4885
|
+
try {
|
|
4886
|
+
await conn.send("Target.setDiscoverTargets", { discover: true }).catch(() => {
|
|
4887
|
+
return;
|
|
4888
|
+
});
|
|
4889
|
+
const { targetId } = await conn.send("Target.createTarget", { url });
|
|
4890
|
+
if (!targetId)
|
|
4891
|
+
throw driverError("page-error", "Target.createTarget returned no targetId");
|
|
4892
|
+
id = targetId;
|
|
4893
|
+
if (!changed.has(targetId)) {
|
|
4894
|
+
await new Promise((resolve) => {
|
|
4895
|
+
const timer = setTimeout(resolve, settleMs);
|
|
4896
|
+
wake = () => {
|
|
4897
|
+
clearTimeout(timer);
|
|
4898
|
+
resolve();
|
|
4899
|
+
};
|
|
4900
|
+
});
|
|
4901
|
+
}
|
|
4902
|
+
return targetId;
|
|
4903
|
+
} finally {
|
|
4904
|
+
off();
|
|
4905
|
+
}
|
|
4906
|
+
}
|
|
4872
4907
|
|
|
4873
4908
|
class CdpBrowserDriver {
|
|
4874
4909
|
scheme = "cdp";
|
|
@@ -4883,9 +4918,7 @@ class CdpBrowserDriver {
|
|
|
4883
4918
|
}
|
|
4884
4919
|
async newPage(url) {
|
|
4885
4920
|
return withBrowserConn(async (conn) => {
|
|
4886
|
-
const
|
|
4887
|
-
if (!targetId)
|
|
4888
|
-
throw driverError("page-error", "Target.createTarget returned no targetId");
|
|
4921
|
+
const targetId = await createSettledTarget(conn, url ?? "about:blank");
|
|
4889
4922
|
return { id: targetId, url: url ?? "about:blank", title: "" };
|
|
4890
4923
|
});
|
|
4891
4924
|
}
|
|
@@ -10760,6 +10793,12 @@ var CANDIDATE_BUILDERS = {
|
|
|
10760
10793
|
"chromium"
|
|
10761
10794
|
],
|
|
10762
10795
|
arc: () => [process.env.ARC_BIN, "/Applications/Arc.app/Contents/MacOS/Arc", "arc"],
|
|
10796
|
+
vivaldi: () => [
|
|
10797
|
+
process.env.VIVALDI_BIN,
|
|
10798
|
+
"/Applications/Vivaldi.app/Contents/MacOS/Vivaldi",
|
|
10799
|
+
"vivaldi",
|
|
10800
|
+
"/usr/bin/vivaldi"
|
|
10801
|
+
],
|
|
10763
10802
|
firefox: () => [
|
|
10764
10803
|
process.env.FIREFOX_BIN,
|
|
10765
10804
|
"/Applications/Firefox.app/Contents/MacOS/firefox",
|
|
@@ -11007,7 +11046,7 @@ function isOnPath(bin) {
|
|
|
11007
11046
|
// src/install/wizard.ts
|
|
11008
11047
|
var DEFAULT_NAME = "cdp-toolkit";
|
|
11009
11048
|
var HARNESSES = ["claude", "codex", "opencode"];
|
|
11010
|
-
var BROWSERS = ["arc", "chrome", "firefox"];
|
|
11049
|
+
var BROWSERS = ["arc", "chrome", "firefox", "vivaldi"];
|
|
11011
11050
|
function defaultPortFor(browser) {
|
|
11012
11051
|
return browser === "firefox" ? 9223 : 9222;
|
|
11013
11052
|
}
|
|
@@ -11020,10 +11059,10 @@ function parseHarness(v) {
|
|
|
11020
11059
|
}
|
|
11021
11060
|
function parseBrowserChoice(v) {
|
|
11022
11061
|
if (v === undefined)
|
|
11023
|
-
throw new Error("--browser requires a value: arc, chrome, or
|
|
11062
|
+
throw new Error("--browser requires a value: arc, chrome, firefox, or vivaldi");
|
|
11024
11063
|
if (BROWSERS.includes(v))
|
|
11025
11064
|
return v;
|
|
11026
|
-
throw new Error(`--browser must be one of arc|chrome|firefox (got '${v}')`);
|
|
11065
|
+
throw new Error(`--browser must be one of arc|chrome|firefox|vivaldi (got '${v}')`);
|
|
11027
11066
|
}
|
|
11028
11067
|
function parsePort(v) {
|
|
11029
11068
|
if (v === undefined)
|
|
@@ -11104,14 +11143,14 @@ function installHelpText() {
|
|
|
11104
11143
|
browser-launch alias (with the remote-debugging port open) to your shell rc.
|
|
11105
11144
|
|
|
11106
11145
|
Usage:
|
|
11107
|
-
cdp install [--harness <claude|codex|opencode>] [--browser <arc|chrome|firefox>]
|
|
11146
|
+
cdp install [--harness <claude|codex|opencode>] [--browser <arc|chrome|firefox|vivaldi>]
|
|
11108
11147
|
[--port <n>] [--name <server-name>] [--alias-name <name>]
|
|
11109
11148
|
[--no-alias] [--yes|-y] [--help|-h]
|
|
11110
11149
|
|
|
11111
11150
|
Options:
|
|
11112
11151
|
--harness <h> Coding harness to register the MCP server with: claude, codex, opencode.
|
|
11113
|
-
--browser <b> Browser the alias launches: arc, chrome, firefox.
|
|
11114
|
-
--port <n> Remote-debugging port (1-65535). Default: 9222 (chrome/arc), 9223 (firefox).
|
|
11152
|
+
--browser <b> Browser the alias launches: arc, chrome, firefox, vivaldi.
|
|
11153
|
+
--port <n> Remote-debugging port (1-65535). Default: 9222 (chrome/arc/vivaldi), 9223 (firefox).
|
|
11115
11154
|
--name <name> MCP server name to register. Default: ${DEFAULT_NAME}.
|
|
11116
11155
|
--alias-name <n> Shell alias name. Default: cdp-<browser> (e.g. cdp-firefox).
|
|
11117
11156
|
--no-alias Register the MCP server only; skip writing the shell alias.
|
|
@@ -11370,6 +11409,13 @@ function coerce(raw) {
|
|
|
11370
11409
|
}
|
|
11371
11410
|
return raw;
|
|
11372
11411
|
}
|
|
11412
|
+
function coerceForTool(tool, key, raw) {
|
|
11413
|
+
const spec = tool === undefined ? undefined : MANIFEST.find((t) => t.name === tool);
|
|
11414
|
+
const prop = spec?.inputSchema.properties?.[key];
|
|
11415
|
+
if (prop?.type === "string")
|
|
11416
|
+
return raw;
|
|
11417
|
+
return coerce(raw);
|
|
11418
|
+
}
|
|
11373
11419
|
function parseArgv(argv) {
|
|
11374
11420
|
const out = { list: false, capabilities: false, help: false, args: {} };
|
|
11375
11421
|
let jsonObj;
|
|
@@ -11407,7 +11453,7 @@ function parseArgv(argv) {
|
|
|
11407
11453
|
flagPairs.push(["target", rawValue]);
|
|
11408
11454
|
continue;
|
|
11409
11455
|
}
|
|
11410
|
-
flagPairs.push([key, hasValue ?
|
|
11456
|
+
flagPairs.push([key, hasValue ? rawValue : true]);
|
|
11411
11457
|
continue;
|
|
11412
11458
|
}
|
|
11413
11459
|
if (out.tool === undefined)
|
|
@@ -11415,8 +11461,9 @@ function parseArgv(argv) {
|
|
|
11415
11461
|
}
|
|
11416
11462
|
if (jsonObj)
|
|
11417
11463
|
Object.assign(out.args, jsonObj);
|
|
11418
|
-
for (const [k, v] of flagPairs)
|
|
11419
|
-
out.args[k] = v;
|
|
11464
|
+
for (const [k, v] of flagPairs) {
|
|
11465
|
+
out.args[k] = v === true || k === "target" ? v : coerceForTool(out.tool, k, v);
|
|
11466
|
+
}
|
|
11420
11467
|
return out;
|
|
11421
11468
|
}
|
|
11422
11469
|
function isToolName(name) {
|
|
@@ -11559,3 +11606,6 @@ if (isDirectRun()) {
|
|
|
11559
11606
|
process.exit(1);
|
|
11560
11607
|
});
|
|
11561
11608
|
}
|
|
11609
|
+
export {
|
|
11610
|
+
parseArgv
|
|
11611
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -2267,6 +2267,41 @@ function noSuchTarget(e) {
|
|
|
2267
2267
|
throw e;
|
|
2268
2268
|
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
2269
2269
|
}
|
|
2270
|
+
var NEW_TARGET_SETTLE_MS = 2000;
|
|
2271
|
+
async function createSettledTarget(conn, url, settleMs = NEW_TARGET_SETTLE_MS) {
|
|
2272
|
+
const changed = new Set;
|
|
2273
|
+
let id = "";
|
|
2274
|
+
let wake;
|
|
2275
|
+
const off = conn.on("Target.targetInfoChanged", (params) => {
|
|
2276
|
+
const t = params.targetInfo?.targetId;
|
|
2277
|
+
if (!t)
|
|
2278
|
+
return;
|
|
2279
|
+
changed.add(t);
|
|
2280
|
+
if (t === id)
|
|
2281
|
+
wake?.();
|
|
2282
|
+
});
|
|
2283
|
+
try {
|
|
2284
|
+
await conn.send("Target.setDiscoverTargets", { discover: true }).catch(() => {
|
|
2285
|
+
return;
|
|
2286
|
+
});
|
|
2287
|
+
const { targetId } = await conn.send("Target.createTarget", { url });
|
|
2288
|
+
if (!targetId)
|
|
2289
|
+
throw driverError("page-error", "Target.createTarget returned no targetId");
|
|
2290
|
+
id = targetId;
|
|
2291
|
+
if (!changed.has(targetId)) {
|
|
2292
|
+
await new Promise((resolve) => {
|
|
2293
|
+
const timer = setTimeout(resolve, settleMs);
|
|
2294
|
+
wake = () => {
|
|
2295
|
+
clearTimeout(timer);
|
|
2296
|
+
resolve();
|
|
2297
|
+
};
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
return targetId;
|
|
2301
|
+
} finally {
|
|
2302
|
+
off();
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2270
2305
|
|
|
2271
2306
|
class CdpBrowserDriver {
|
|
2272
2307
|
scheme = "cdp";
|
|
@@ -2281,9 +2316,7 @@ class CdpBrowserDriver {
|
|
|
2281
2316
|
}
|
|
2282
2317
|
async newPage(url) {
|
|
2283
2318
|
return withBrowserConn(async (conn) => {
|
|
2284
|
-
const
|
|
2285
|
-
if (!targetId)
|
|
2286
|
-
throw driverError("page-error", "Target.createTarget returned no targetId");
|
|
2319
|
+
const targetId = await createSettledTarget(conn, url ?? "about:blank");
|
|
2287
2320
|
return { id: targetId, url: url ?? "about:blank", title: "" };
|
|
2288
2321
|
});
|
|
2289
2322
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -4882,6 +4882,41 @@ function noSuchTarget(e) {
|
|
|
4882
4882
|
throw e;
|
|
4883
4883
|
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
4884
4884
|
}
|
|
4885
|
+
var NEW_TARGET_SETTLE_MS = 2000;
|
|
4886
|
+
async function createSettledTarget(conn, url, settleMs = NEW_TARGET_SETTLE_MS) {
|
|
4887
|
+
const changed = new Set;
|
|
4888
|
+
let id = "";
|
|
4889
|
+
let wake;
|
|
4890
|
+
const off = conn.on("Target.targetInfoChanged", (params) => {
|
|
4891
|
+
const t = params.targetInfo?.targetId;
|
|
4892
|
+
if (!t)
|
|
4893
|
+
return;
|
|
4894
|
+
changed.add(t);
|
|
4895
|
+
if (t === id)
|
|
4896
|
+
wake?.();
|
|
4897
|
+
});
|
|
4898
|
+
try {
|
|
4899
|
+
await conn.send("Target.setDiscoverTargets", { discover: true }).catch(() => {
|
|
4900
|
+
return;
|
|
4901
|
+
});
|
|
4902
|
+
const { targetId } = await conn.send("Target.createTarget", { url });
|
|
4903
|
+
if (!targetId)
|
|
4904
|
+
throw driverError("page-error", "Target.createTarget returned no targetId");
|
|
4905
|
+
id = targetId;
|
|
4906
|
+
if (!changed.has(targetId)) {
|
|
4907
|
+
await new Promise((resolve) => {
|
|
4908
|
+
const timer = setTimeout(resolve, settleMs);
|
|
4909
|
+
wake = () => {
|
|
4910
|
+
clearTimeout(timer);
|
|
4911
|
+
resolve();
|
|
4912
|
+
};
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
return targetId;
|
|
4916
|
+
} finally {
|
|
4917
|
+
off();
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4885
4920
|
|
|
4886
4921
|
class CdpBrowserDriver {
|
|
4887
4922
|
scheme = "cdp";
|
|
@@ -4896,9 +4931,7 @@ class CdpBrowserDriver {
|
|
|
4896
4931
|
}
|
|
4897
4932
|
async newPage(url) {
|
|
4898
4933
|
return withBrowserConn(async (conn) => {
|
|
4899
|
-
const
|
|
4900
|
-
if (!targetId)
|
|
4901
|
-
throw driverError("page-error", "Target.createTarget returned no targetId");
|
|
4934
|
+
const targetId = await createSettledTarget(conn, url ?? "about:blank");
|
|
4902
4935
|
return { id: targetId, url: url ?? "about:blank", title: "" };
|
|
4903
4936
|
});
|
|
4904
4937
|
}
|
|
@@ -11369,7 +11402,7 @@ var TOOL_DOCS = {
|
|
|
11369
11402
|
};
|
|
11370
11403
|
|
|
11371
11404
|
// src/version.ts
|
|
11372
|
-
var VERSION = "3.0.
|
|
11405
|
+
var VERSION = "3.0.3";
|
|
11373
11406
|
|
|
11374
11407
|
// src/mcp.ts
|
|
11375
11408
|
function isDirectRun() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdp-toolkit",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "A lightweight chrome-devtools-mcp alternative: an MCP server + CLI that drives the tabs you name, one target per call over one direct WebSocket, with a bounded timeout on every call, so a stuck page can't wedge your agent or force a /mcp restart. Chrome over CDP and Firefox over WebDriver BiDi, with tools a backend can't run absent from tools/list rather than throwing mid-task. 48 tools (only 5 core tools listed by default, plus search_tools/call_tool to reach the rest) including built-in network mocking, a cookie group, tab leases with human-activity detection, real HTML5 drag-and-drop, raw mouse/scroll dispatch, downloads, permission grants, and tab-to-video screen recording so several agents can drive one browser without stealing each other's tabs or a human's. No Puppeteer, no all-target fan-out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cdp/driver.ts
CHANGED
|
@@ -1691,6 +1691,56 @@ function noSuchTarget(e: unknown): never {
|
|
|
1691
1691
|
if (typeof (e as { code?: unknown } | null | undefined)?.code === "string") throw e;
|
|
1692
1692
|
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
1693
1693
|
}
|
|
1694
|
+
/** Upper bound on waiting for a new tab to settle; see createSettledTarget. */
|
|
1695
|
+
export const NEW_TARGET_SETTLE_MS = 2000;
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* Create a tab and return only once the browser has finished adopting it.
|
|
1699
|
+
*
|
|
1700
|
+
* Vivaldi wedges a tab that anything attaches to in the first few milliseconds
|
|
1701
|
+
* after Target.createTarget: the tab keeps an empty URL, its renderer never
|
|
1702
|
+
* answers again (Page.enable, Runtime.evaluate and runIfWaitingForDebugger all
|
|
1703
|
+
* time out), and only a manual reload recovers it. new_page{claim:true} hit this
|
|
1704
|
+
* every time, because the activity beacon attaches straight after creation.
|
|
1705
|
+
* Measured on Vivaldi 8.2 (Chromium 152): an attach at ~0ms wedged 5/5 tabs
|
|
1706
|
+
* (https and about:blank alike, page socket or flattened session alike); an
|
|
1707
|
+
* attach after the tab's first Target.targetInfoChanged loaded 5/5.
|
|
1708
|
+
*
|
|
1709
|
+
* So: discover targets, create, and wait for that first targetInfoChanged for
|
|
1710
|
+
* this id, capped at NEW_TARGET_SETTLE_MS. The listener is registered BEFORE
|
|
1711
|
+
* createTarget because the event can beat the command's response. A browser
|
|
1712
|
+
* that never sends the event costs at most the cap, never a failure.
|
|
1713
|
+
*/
|
|
1714
|
+
export async function createSettledTarget(
|
|
1715
|
+
conn: Pick<CdpConnection, "send" | "on">,
|
|
1716
|
+
url: string,
|
|
1717
|
+
settleMs = NEW_TARGET_SETTLE_MS,
|
|
1718
|
+
): Promise<string> {
|
|
1719
|
+
const changed = new Set<string>();
|
|
1720
|
+
let id = "";
|
|
1721
|
+
let wake: (() => void) | undefined;
|
|
1722
|
+
const off = conn.on("Target.targetInfoChanged", (params) => {
|
|
1723
|
+
const t = (params as { targetInfo?: { targetId?: string } }).targetInfo?.targetId;
|
|
1724
|
+
if (!t) return;
|
|
1725
|
+
changed.add(t);
|
|
1726
|
+
if (t === id) wake?.();
|
|
1727
|
+
});
|
|
1728
|
+
try {
|
|
1729
|
+
await conn.send("Target.setDiscoverTargets", { discover: true }).catch(() => undefined);
|
|
1730
|
+
const { targetId } = await conn.send<{ targetId: string }>("Target.createTarget", { url });
|
|
1731
|
+
if (!targetId) throw driverError("page-error", "Target.createTarget returned no targetId");
|
|
1732
|
+
id = targetId;
|
|
1733
|
+
if (!changed.has(targetId)) {
|
|
1734
|
+
await new Promise<void>((resolve) => {
|
|
1735
|
+
const timer = setTimeout(resolve, settleMs);
|
|
1736
|
+
wake = () => { clearTimeout(timer); resolve(); };
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
return targetId;
|
|
1740
|
+
} finally {
|
|
1741
|
+
off();
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1694
1744
|
class CdpBrowserDriver implements BrowserDriver {
|
|
1695
1745
|
readonly scheme = "cdp";
|
|
1696
1746
|
readonly lifetime: LifetimeModel = "per-call";
|
|
@@ -1706,8 +1756,7 @@ class CdpBrowserDriver implements BrowserDriver {
|
|
|
1706
1756
|
}
|
|
1707
1757
|
async newPage(url?: string): Promise<PageInfo> {
|
|
1708
1758
|
return withBrowserConn(async (conn) => {
|
|
1709
|
-
const
|
|
1710
|
-
if (!targetId) throw driverError("page-error", "Target.createTarget returned no targetId");
|
|
1759
|
+
const targetId = await createSettledTarget(conn, url ?? "about:blank");
|
|
1711
1760
|
return { id: targetId, url: url ?? "about:blank", title: "" };
|
|
1712
1761
|
});
|
|
1713
1762
|
}
|
package/src/cli.ts
CHANGED
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
* - First positional token is the tool name (a key of TOOLS).
|
|
10
10
|
* - `--json '<obj>'` parses a JSON object and merges it into the args object.
|
|
11
11
|
* - `--target <sel>` sets `args.target` (a TargetSelector string).
|
|
12
|
-
* - Any other `--key value` pair becomes `args.key
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* - Any other `--key value` pair becomes `args.key`. When the tool's MANIFEST
|
|
13
|
+
* inputSchema declares the key `type: "string"`, the raw string is kept
|
|
14
|
+
* as-is (so a CDP requestId like "91775.10" is not turned into the number
|
|
15
|
+
* 91775.1). Otherwise the value is coerced: "true"/"false" -> boolean, a
|
|
16
|
+
* numeric string -> number, else the raw string. A bare `--flag` with no following value (or followed by another
|
|
15
17
|
* --flag) is treated as boolean true.
|
|
16
18
|
* - Explicit `--key` pairs OVERRIDE keys merged from `--json` (last writer
|
|
17
19
|
* wins; --json is applied first, then the individual flags).
|
|
@@ -108,6 +110,20 @@ function coerce(raw: string): boolean | number | string {
|
|
|
108
110
|
return raw;
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Coerce one `--key value` using the tool's declared schema type. A key the
|
|
115
|
+
* schema types as "string" keeps its raw text verbatim; coercing it would turn
|
|
116
|
+
* an id that merely LOOKS numeric ("91775.10", "007") into a number that no
|
|
117
|
+
* longer matches, or into a different string entirely. Every other key (or a
|
|
118
|
+
* tool/key the manifest doesn't know) falls back to coerce().
|
|
119
|
+
*/
|
|
120
|
+
function coerceForTool(tool: string | undefined, key: string, raw: string): boolean | number | string {
|
|
121
|
+
const spec = tool === undefined ? undefined : MANIFEST.find((t) => t.name === tool);
|
|
122
|
+
const prop = spec?.inputSchema.properties?.[key] as { type?: unknown } | undefined;
|
|
123
|
+
if (prop?.type === "string") return raw;
|
|
124
|
+
return coerce(raw);
|
|
125
|
+
}
|
|
126
|
+
|
|
111
127
|
interface ParsedArgs {
|
|
112
128
|
tool?: string;
|
|
113
129
|
list: boolean;
|
|
@@ -117,12 +133,14 @@ interface ParsedArgs {
|
|
|
117
133
|
}
|
|
118
134
|
|
|
119
135
|
/** Parse process argv (already sliced to drop the runtime + script, and with --browser already stripped). */
|
|
120
|
-
function parseArgv(argv: string[]): ParsedArgs {
|
|
136
|
+
export function parseArgv(argv: string[]): ParsedArgs {
|
|
121
137
|
const out: ParsedArgs = { list: false, capabilities: false, help: false, args: {} };
|
|
122
138
|
// Collect flag pairs separately so --json can be merged BEFORE individual
|
|
123
139
|
// flags override it, regardless of token order.
|
|
124
140
|
let jsonObj: Record<string, unknown> | undefined;
|
|
125
|
-
|
|
141
|
+
// Raw values are kept until the tool name is known (it may follow the flags),
|
|
142
|
+
// so coercion can consult that tool's schema.
|
|
143
|
+
const flagPairs: Array<[string, string | true]> = [];
|
|
126
144
|
|
|
127
145
|
for (let i = 0; i < argv.length; i++) {
|
|
128
146
|
const token = argv[i]!;
|
|
@@ -164,7 +182,7 @@ function parseArgv(argv: string[]): ParsedArgs {
|
|
|
164
182
|
flagPairs.push(["target", rawValue]);
|
|
165
183
|
continue;
|
|
166
184
|
}
|
|
167
|
-
flagPairs.push([key, hasValue ?
|
|
185
|
+
flagPairs.push([key, hasValue ? rawValue : true]);
|
|
168
186
|
continue;
|
|
169
187
|
}
|
|
170
188
|
// First non-flag positional is the tool name; ignore any extras.
|
|
@@ -173,7 +191,9 @@ function parseArgv(argv: string[]): ParsedArgs {
|
|
|
173
191
|
|
|
174
192
|
// --json first, then explicit flags win.
|
|
175
193
|
if (jsonObj) Object.assign(out.args, jsonObj);
|
|
176
|
-
for (const [k, v] of flagPairs)
|
|
194
|
+
for (const [k, v] of flagPairs) {
|
|
195
|
+
out.args[k] = v === true || k === "target" ? v : coerceForTool(out.tool, k, v);
|
|
196
|
+
}
|
|
177
197
|
return out;
|
|
178
198
|
}
|
|
179
199
|
|
package/src/install/browser.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { delimiter, dirname, join } from "node:path";
|
|
12
12
|
|
|
13
|
-
export type BrowserChoice = "arc" | "chrome" | "firefox";
|
|
13
|
+
export type BrowserChoice = "arc" | "chrome" | "firefox" | "vivaldi";
|
|
14
14
|
|
|
15
15
|
export interface BrowserInfo {
|
|
16
16
|
choice: BrowserChoice;
|
|
@@ -40,7 +40,7 @@ export interface RcAppendResult {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Per-choice candidate lists, built fresh on every call so env var overrides (CHROME_BIN /
|
|
43
|
-
* ARC_BIN / FIREFOX_BIN) are read live rather than captured once at module load — important for
|
|
43
|
+
* ARC_BIN / VIVALDI_BIN / FIREFOX_BIN) are read live rather than captured once at module load — important for
|
|
44
44
|
* tests that set/restore these env vars around a single call.
|
|
45
45
|
*
|
|
46
46
|
* macOS-first, Linux fallback. Order mirrors the spec: env override, then the macOS .app bundle
|
|
@@ -56,6 +56,12 @@ const CANDIDATE_BUILDERS: Record<BrowserChoice, () => Array<string | undefined>>
|
|
|
56
56
|
"chromium",
|
|
57
57
|
],
|
|
58
58
|
arc: () => [process.env.ARC_BIN, "/Applications/Arc.app/Contents/MacOS/Arc", "arc"],
|
|
59
|
+
vivaldi: () => [
|
|
60
|
+
process.env.VIVALDI_BIN,
|
|
61
|
+
"/Applications/Vivaldi.app/Contents/MacOS/Vivaldi",
|
|
62
|
+
"vivaldi",
|
|
63
|
+
"/usr/bin/vivaldi",
|
|
64
|
+
],
|
|
59
65
|
firefox: () => [
|
|
60
66
|
process.env.FIREFOX_BIN,
|
|
61
67
|
"/Applications/Firefox.app/Contents/MacOS/firefox",
|
|
@@ -113,7 +119,7 @@ export function defaultAliasName(choice: BrowserChoice): string {
|
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
/**
|
|
116
|
-
* The launch flags only (no binary). Chrome and
|
|
122
|
+
* The launch flags only (no binary). Chrome, Arc and Vivaldi are all Chromium-based and accept
|
|
117
123
|
* Chrome's flags, so they share a shape: --remote-debugging-port plus a DEDICATED
|
|
118
124
|
* --user-data-dir. The dedicated profile is required, not cosmetic — Chrome silently ignores
|
|
119
125
|
* --remote-debugging-port when it hands off to an already-running instance on the default
|
package/src/install/wizard.ts
CHANGED
|
@@ -63,7 +63,7 @@ export interface InstallerIO {
|
|
|
63
63
|
|
|
64
64
|
const DEFAULT_NAME = "cdp-toolkit";
|
|
65
65
|
const HARNESSES: readonly HarnessChoice[] = ["claude", "codex", "opencode"];
|
|
66
|
-
const BROWSERS: readonly BrowserChoice[] = ["arc", "chrome", "firefox"];
|
|
66
|
+
const BROWSERS: readonly BrowserChoice[] = ["arc", "chrome", "firefox", "vivaldi"];
|
|
67
67
|
|
|
68
68
|
/* --------------------------------- pure: arg parsing --------------------------------- */
|
|
69
69
|
|
|
@@ -79,9 +79,9 @@ function parseHarness(v: string | undefined): HarnessChoice {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function parseBrowserChoice(v: string | undefined): BrowserChoice {
|
|
82
|
-
if (v === undefined) throw new Error("--browser requires a value: arc, chrome, or
|
|
82
|
+
if (v === undefined) throw new Error("--browser requires a value: arc, chrome, firefox, or vivaldi");
|
|
83
83
|
if ((BROWSERS as readonly string[]).includes(v)) return v as BrowserChoice;
|
|
84
|
-
throw new Error(`--browser must be one of arc|chrome|firefox (got '${v}')`);
|
|
84
|
+
throw new Error(`--browser must be one of arc|chrome|firefox|vivaldi (got '${v}')`);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
function parsePort(v: string | undefined): number {
|
|
@@ -165,8 +165,8 @@ export function parseInstallArgs(argv: string[]): InstallArgs {
|
|
|
165
165
|
* PURE. Build the MCP stdio server config for a given browser + debug port.
|
|
166
166
|
*
|
|
167
167
|
* The server always runs `npx -y cdp-toolkit`. Only the env differs by browser, and even then
|
|
168
|
-
* chrome and
|
|
169
|
-
* whatever is listening on CDP_BASE — the arc-vs-chrome distinction lives entirely in the shell
|
|
168
|
+
* chrome, arc and vivaldi are IDENTICAL: both are driven over CDP, and the driver just talks CDP to
|
|
169
|
+
* whatever is listening on CDP_BASE — the arc-vs-chrome-vs-vivaldi distinction lives entirely in the shell
|
|
170
170
|
* ALIAS (which .app it launches), not in the server env. Firefox is the odd one out: it is driven
|
|
171
171
|
* over WebDriver BiDi, selected by CDP_BROWSER=firefox with the port in CDP_FIREFOX_ENDPOINT.
|
|
172
172
|
*/
|
|
@@ -185,14 +185,14 @@ export function installHelpText(): string {
|
|
|
185
185
|
browser-launch alias (with the remote-debugging port open) to your shell rc.
|
|
186
186
|
|
|
187
187
|
Usage:
|
|
188
|
-
cdp install [--harness <claude|codex|opencode>] [--browser <arc|chrome|firefox>]
|
|
188
|
+
cdp install [--harness <claude|codex|opencode>] [--browser <arc|chrome|firefox|vivaldi>]
|
|
189
189
|
[--port <n>] [--name <server-name>] [--alias-name <name>]
|
|
190
190
|
[--no-alias] [--yes|-y] [--help|-h]
|
|
191
191
|
|
|
192
192
|
Options:
|
|
193
193
|
--harness <h> Coding harness to register the MCP server with: claude, codex, opencode.
|
|
194
|
-
--browser <b> Browser the alias launches: arc, chrome, firefox.
|
|
195
|
-
--port <n> Remote-debugging port (1-65535). Default: 9222 (chrome/arc), 9223 (firefox).
|
|
194
|
+
--browser <b> Browser the alias launches: arc, chrome, firefox, vivaldi.
|
|
195
|
+
--port <n> Remote-debugging port (1-65535). Default: 9222 (chrome/arc/vivaldi), 9223 (firefox).
|
|
196
196
|
--name <name> MCP server name to register. Default: ${DEFAULT_NAME}.
|
|
197
197
|
--alias-name <n> Shell alias name. Default: cdp-<browser> (e.g. cdp-firefox).
|
|
198
198
|
--no-alias Register the MCP server only; skip writing the shell alias.
|
package/src/version.ts
CHANGED