cdp-toolkit 1.0.0 → 1.1.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/CONTRACT.md +1 -1
- package/README.md +34 -0
- package/dist/cli.js +3156 -1290
- package/dist/index.js +1116 -927
- package/dist/mcp.js +4472 -2626
- package/package.json +12 -6
- package/src/backend.ts +86 -0
- package/src/bidi/client.ts +319 -0
- package/src/bidi/driver.ts +823 -0
- package/src/bidi/launch.ts +192 -0
- package/src/bidi/protocol.ts +782 -0
- package/src/bidi/snapshot.ts +419 -0
- package/src/bidi-tools.ts +193 -0
- package/src/capabilities.ts +43 -0
- package/src/cdp/driver.ts +660 -0
- package/src/cli.ts +72 -14
- package/src/driver.ts +424 -0
- package/src/firefox-tools.ts +14 -0
- package/src/index.ts +49 -41
- package/src/mcp.ts +59 -9
- package/src/shared-tools.ts +420 -0
- package/src/tools/dialogs.ts +0 -236
- package/src/tools/emulation.ts +0 -212
- package/src/tools/input.ts +0 -399
- package/src/tools/pages.ts +0 -159
- package/src/tools/screenshot.ts +0 -180
package/src/cli.ts
CHANGED
|
@@ -23,12 +23,16 @@
|
|
|
23
23
|
* - --list -> all tool names (one per line) to stdout, exit 0.
|
|
24
24
|
*/
|
|
25
25
|
import { TOOLS, TOOL_NAMES, type ToolName } from "./index.ts";
|
|
26
|
+
import { resolveBrowserKind, stripBrowserFlag, startFirefoxSession } from "./backend.ts";
|
|
27
|
+
import { toolAvailability } from "./capabilities.ts";
|
|
28
|
+
import { FIREFOX_TOOLS } from "./firefox-tools.ts";
|
|
26
29
|
|
|
27
|
-
const USAGE = `cdp-toolkit
|
|
30
|
+
const USAGE = `cdp-toolkit: raw single-target CDP, 29-tool chrome-devtools-mcp parity, plus a Firefox backend over WebDriver BiDi.
|
|
28
31
|
|
|
29
32
|
Usage:
|
|
30
|
-
bun run src/cli.ts <tool> [--target <sel>] [--json '<obj>'] [--<key> <value> ...]
|
|
31
|
-
bun run src/cli.ts --list
|
|
33
|
+
bun run src/cli.ts <tool> [--browser chrome|firefox] [--target <sel>] [--json '<obj>'] [--<key> <value> ...]
|
|
34
|
+
bun run src/cli.ts --list [--browser chrome|firefox]
|
|
35
|
+
bun run src/cli.ts --capabilities [--browser chrome|firefox]
|
|
32
36
|
|
|
33
37
|
Examples:
|
|
34
38
|
bun run src/cli.ts list_pages
|
|
@@ -36,9 +40,17 @@ Examples:
|
|
|
36
40
|
bun run src/cli.ts click --target index:0 --uid 42
|
|
37
41
|
bun run src/cli.ts evaluate_script --json '{"expression":"1+2"}'
|
|
38
42
|
bun run src/cli.ts take_screenshot --target url:example --fullPage true
|
|
43
|
+
bun run src/cli.ts --browser firefox take_snapshot
|
|
44
|
+
bun run src/cli.ts --capabilities --browser firefox
|
|
45
|
+
|
|
46
|
+
Backend selection: --browser chrome|firefox, else the CDP_BROWSER env var, else "chrome" (default,
|
|
47
|
+
zero behavior change for existing users). Firefox is LAUNCHED per invocation (it cannot be attached
|
|
48
|
+
to), used for exactly one tool call, then disposed before the process exits.
|
|
39
49
|
|
|
40
50
|
Target selector grammar: active | index:N | url:<substr> | title:<substr> | <targetId>
|
|
41
|
-
Run with --list to print every available
|
|
51
|
+
Run with --list to print every tool name available under the selected backend.
|
|
52
|
+
Run with --capabilities to see, per tool, whether it is available and (if not) which capability
|
|
53
|
+
the selected backend is missing.`;
|
|
42
54
|
|
|
43
55
|
/** Coerce a raw CLI string into boolean / number / string. */
|
|
44
56
|
function coerce(raw: string): boolean | number | string {
|
|
@@ -54,12 +66,13 @@ function coerce(raw: string): boolean | number | string {
|
|
|
54
66
|
interface ParsedArgs {
|
|
55
67
|
tool?: string;
|
|
56
68
|
list: boolean;
|
|
69
|
+
capabilities: boolean;
|
|
57
70
|
args: Record<string, unknown>;
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
/** Parse process argv (already sliced to drop the runtime + script). */
|
|
73
|
+
/** Parse process argv (already sliced to drop the runtime + script, and with --browser already stripped). */
|
|
61
74
|
function parseArgv(argv: string[]): ParsedArgs {
|
|
62
|
-
const out: ParsedArgs = { list: false, args: {} };
|
|
75
|
+
const out: ParsedArgs = { list: false, capabilities: false, args: {} };
|
|
63
76
|
// Collect flag pairs separately so --json can be merged BEFORE individual
|
|
64
77
|
// flags override it, regardless of token order.
|
|
65
78
|
let jsonObj: Record<string, unknown> | undefined;
|
|
@@ -71,6 +84,10 @@ function parseArgv(argv: string[]): ParsedArgs {
|
|
|
71
84
|
out.list = true;
|
|
72
85
|
continue;
|
|
73
86
|
}
|
|
87
|
+
if (token === "--capabilities") {
|
|
88
|
+
out.capabilities = true;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
74
91
|
if (token.startsWith("--")) {
|
|
75
92
|
const key = token.slice(2);
|
|
76
93
|
const next = argv[i + 1];
|
|
@@ -109,10 +126,23 @@ function isToolName(name: string): name is ToolName {
|
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
async function main(): Promise<number> {
|
|
112
|
-
const
|
|
129
|
+
const rawArgv = process.argv.slice(2);
|
|
130
|
+
const browser = resolveBrowserKind(rawArgv);
|
|
131
|
+
const parsed = parseArgv(stripBrowserFlag(rawArgv));
|
|
132
|
+
const availability = toolAvailability(browser);
|
|
133
|
+
const availableSet = new Set(availability.available);
|
|
134
|
+
|
|
135
|
+
if (parsed.capabilities) {
|
|
136
|
+
const lines = [`backend: ${browser}`, "", "available:"];
|
|
137
|
+
for (const name of availability.available) lines.push(` ${name}`);
|
|
138
|
+
lines.push("", "unavailable:");
|
|
139
|
+
for (const u of availability.unavailable) lines.push(` ${u.name} (needs: ${u.missing.join(", ")})`);
|
|
140
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
113
143
|
|
|
114
144
|
if (parsed.list) {
|
|
115
|
-
process.stdout.write(`${
|
|
145
|
+
process.stdout.write(`${availability.available.join("\n")}\n`);
|
|
116
146
|
return 0;
|
|
117
147
|
}
|
|
118
148
|
|
|
@@ -128,12 +158,40 @@ async function main(): Promise<number> {
|
|
|
128
158
|
return 1;
|
|
129
159
|
}
|
|
130
160
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
if (!availableSet.has(parsed.tool)) {
|
|
162
|
+
const gap = availability.unavailable.find((u) => u.name === parsed.tool);
|
|
163
|
+
process.stderr.write(
|
|
164
|
+
`${JSON.stringify({
|
|
165
|
+
error: `tool '${parsed.tool}' is not available under --browser ${browser}${gap ? ` (needs: ${gap.missing.join(", ")})` : ""}. Run --capabilities --browser ${browser} to see the full list.`,
|
|
166
|
+
})}\n`,
|
|
167
|
+
);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (browser === "chrome") {
|
|
172
|
+
const fn = TOOLS[parsed.tool];
|
|
173
|
+
// The registry's value type is the union of all tool signatures; each tool
|
|
174
|
+
// validates its own args at runtime, so we hand the parsed object through.
|
|
175
|
+
const result = await (fn as (args: unknown) => Promise<unknown>)(parsed.args);
|
|
176
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Firefox: one process per invocation. Launch, run exactly one tool call, always dispose,
|
|
181
|
+
// even if the tool call itself throws, so the spawned Firefox process never leaks.
|
|
182
|
+
const neutralFn = FIREFOX_TOOLS[parsed.tool];
|
|
183
|
+
if (!neutralFn) {
|
|
184
|
+
process.stderr.write(`${JSON.stringify({ error: `tool '${parsed.tool}' has no Firefox implementation wired` })}\n`);
|
|
185
|
+
return 1;
|
|
186
|
+
}
|
|
187
|
+
const session = await startFirefoxSession();
|
|
188
|
+
try {
|
|
189
|
+
const result = await neutralFn(session.driver, parsed.args as never);
|
|
190
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
191
|
+
return 0;
|
|
192
|
+
} finally {
|
|
193
|
+
await session.dispose();
|
|
194
|
+
}
|
|
137
195
|
}
|
|
138
196
|
|
|
139
197
|
main()
|
package/src/driver.ts
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-001: one browser-neutral Driver interface behind the 33 tools.
|
|
3
|
+
*
|
|
4
|
+
* Context. Every tool in src/tools/ is a stateless one-shot on withPage():
|
|
5
|
+
* open a socket, act, always close. That is only correct because a CDP `Uid`
|
|
6
|
+
* IS a backendDOMNodeId, a browser-global integer outliving the connection
|
|
7
|
+
* that made it, so take_snapshot in one process and click(uid) in the next
|
|
8
|
+
* interoperate. BiDi breaks both halves: a session is bound to ONE WebSocket
|
|
9
|
+
* (re-dialing gets `invalid session id`) and a `sharedId` is document-scoped,
|
|
10
|
+
* dying on any navigation with `no such node`. A literal port would break
|
|
11
|
+
* snapshot-then-click, the tool's core workflow.
|
|
12
|
+
*
|
|
13
|
+
* Decision.
|
|
14
|
+
* 1. `Uid` becomes an OPAQUE TAGGED STRING, `<scheme>:<payload>`. Tools never
|
|
15
|
+
* parse, build, or order uids; only the driver whose scheme tags a uid may
|
|
16
|
+
* resolve it. Byte-level codec: THE UID CODEC block below.
|
|
17
|
+
* 2. The CDP driver keeps its handle: `cdp:<backendDOMNodeId>`, and still
|
|
18
|
+
* accepts bare digits, so old snapshots and the MCP schema keep working.
|
|
19
|
+
* 3. The BiDi driver does NOT encode a `sharedId`. It STAMPS the page:
|
|
20
|
+
* snapshot writes `data-cdp-uid="<stamp>"` onto every emitted node and the
|
|
21
|
+
* uid is `bidi:<stamp>`, resolved by a locateNodes css lookup. Page state,
|
|
22
|
+
* not protocol state, so it survives a dropped socket, a fresh session,
|
|
23
|
+
* and a fresh CLI process, which is what the one-shot tools depend on.
|
|
24
|
+
* 4. Connection lifetime is declared, not implied: `lifetime` is "per-call"
|
|
25
|
+
* (CDP, release() closes) or "session" (BiDi, release() is a refcount
|
|
26
|
+
* decrement and the socket lives until dispose()). Tools call withDriver()
|
|
27
|
+
* and stay ignorant of which they got.
|
|
28
|
+
* 5. Anything on exactly one browser stays OUT of the interface and is named
|
|
29
|
+
* in `Capability`. tools/list filters on REQUIRED_CAPABILITIES, so
|
|
30
|
+
* tracing, heap, lighthouse, and CPU throttling are simply absent under
|
|
31
|
+
* Firefox rather than present and throwing.
|
|
32
|
+
*
|
|
33
|
+
* Rejected alternatives.
|
|
34
|
+
* A. `Uid = number` indexing a driver-side handle table. The table dies with
|
|
35
|
+
* the process, so the CLI (one process per call) could never resolve a uid
|
|
36
|
+
* it printed last call: works under MCP, fails under CLI, the worst split.
|
|
37
|
+
* B. Encode the `sharedId` (`bidi:<ctx>:<sharedId>`). It is meaningless
|
|
38
|
+
* outside the session that minted it and dies on every navigation. Both
|
|
39
|
+
* verified against Firefox 153.0.3.
|
|
40
|
+
* C. A long-lived module-scoped BiDi session as the WHOLE answer, no stamps.
|
|
41
|
+
* Insufficient, not wrong: it fixes the socket, and recorder.ts /
|
|
42
|
+
* network_mock.ts already prove the pattern, but navigation still voids
|
|
43
|
+
* every sharedId and the CLI gets a new session per process. Adopted as
|
|
44
|
+
* the transport (4) with stamps on top (3). Combination, not either/or.
|
|
45
|
+
* D. A computed unique css path instead of stamping, to avoid touching the
|
|
46
|
+
* DOM. Any sibling insertion silently resolves it to the WRONG node, while
|
|
47
|
+
* a stamp resolves to the right node or none. Loud miss over quiet mis-hit.
|
|
48
|
+
* E. One flat interface with `throw new Error("unsupported")` bodies under
|
|
49
|
+
* Firefox. Moves discovery from tools/list to runtime and makes an honest
|
|
50
|
+
* asymmetry look like a bug.
|
|
51
|
+
*
|
|
52
|
+
* Consequences. Stamping mutates the page (one attribute per snapshotted
|
|
53
|
+
* node), and a node the page re-renders from scratch loses its stamp, so uids
|
|
54
|
+
* expire on re-render rather than on navigation. Both are stated in
|
|
55
|
+
* `uidStability`. Do not "simplify" Uid back to a number: that is A, and it
|
|
56
|
+
* breaks the CLI.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import type { ToolName } from "./index.ts";
|
|
60
|
+
import type { TargetSelector } from "./types.ts";
|
|
61
|
+
|
|
62
|
+
/* ---------------------------------- uids ---------------------------------- */
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* An opaque element reference. NEVER parse, construct, or compare a DriverUid
|
|
66
|
+
* except by string equality. It is produced by Driver.snapshot / Driver.locate
|
|
67
|
+
* and consumed by Driver.resolve.
|
|
68
|
+
*/
|
|
69
|
+
export type DriverUid = string;
|
|
70
|
+
|
|
71
|
+
/* THE UID CODEC, binding on every driver. Two implementers reading only this
|
|
72
|
+
* block must produce interoperable encodings.
|
|
73
|
+
*
|
|
74
|
+
* GRAMMAR. `uid := scheme ":" payload`, split on the FIRST colon only.
|
|
75
|
+
* scheme matches /^[a-z][a-z0-9]{0,7}$/ and equals the driver's `scheme`
|
|
76
|
+
* field; payload is 1..503 chars, no control chars, MAY contain ":"; total
|
|
77
|
+
* length <= 512. A driver MUST reject a uid whose scheme is not its own with
|
|
78
|
+
* code "foreign-uid" and MUST NOT guess at another driver's scheme.
|
|
79
|
+
*
|
|
80
|
+
* SCHEME "cdp". payload := decimal backendDOMNodeId, no sign, no leading
|
|
81
|
+
* zeros, no whitespace. Encode `cdp:${backendNodeId}`; decode Number(payload),
|
|
82
|
+
* which must be a positive safe integer. LEGACY: a uid matching
|
|
83
|
+
* LEGACY_NUMERIC_UID with no scheme prefix MUST be accepted by the cdp driver
|
|
84
|
+
* as `cdp:<digits>`, and by no other driver.
|
|
85
|
+
*
|
|
86
|
+
* SCHEME "bidi". payload := exactly 12 lowercase hex chars, /^[0-9a-f]{12}$/,
|
|
87
|
+
* from a cryptographically-uniform source, never reused within a document.
|
|
88
|
+
* The driver writes it into the live DOM as UID_STAMP_ATTR on the referenced
|
|
89
|
+
* element in the SAME call that mints the uid. Resolution is
|
|
90
|
+
* browsingContext.locateNodes `{type:"css", value:'[data-cdp-uid="<payload>"]'}`
|
|
91
|
+
* in the uid's context. Zero matches MUST raise "stale-uid". More than one
|
|
92
|
+
* match MUST take the first node in document order and MUST NOT throw, since
|
|
93
|
+
* cloning a stamped subtree is legal page behavior.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
/** Attribute the bidi scheme stamps onto elements. Never change this string. */
|
|
97
|
+
export const UID_STAMP_ATTR = "data-cdp-uid" as const;
|
|
98
|
+
|
|
99
|
+
/** Matches the legacy bare-integer uid that the cdp driver must still accept. */
|
|
100
|
+
export const LEGACY_NUMERIC_UID = /^[0-9]+$/;
|
|
101
|
+
|
|
102
|
+
/** True when `u` carries `scheme` as its tag (or is a legacy bare integer for cdp). */
|
|
103
|
+
export function uidHasScheme(u: DriverUid, scheme: string): boolean {
|
|
104
|
+
return u.startsWith(`${scheme}:`) || (scheme === "cdp" && LEGACY_NUMERIC_UID.test(u));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* How long a uid stays resolvable. Declared per driver so a caller can decide
|
|
109
|
+
* when to re-snapshot instead of learning it from a failure.
|
|
110
|
+
* - "browser-node": valid until the underlying DOM node is destroyed, across
|
|
111
|
+
* connections and processes (cdp).
|
|
112
|
+
* - "document-stamp": valid until the document unloads OR the node is
|
|
113
|
+
* re-created by the page, across connections and processes (bidi).
|
|
114
|
+
*/
|
|
115
|
+
export type UidStability = "browser-node" | "document-stamp";
|
|
116
|
+
|
|
117
|
+
/* ------------------------------- capabilities ------------------------------- */
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Capabilities that are NOT universal. Anything a driver can always do has no
|
|
121
|
+
* token here. A driver declares the subset it supports; tools/list filters on
|
|
122
|
+
* REQUIRED_CAPABILITIES so an unsupported tool is absent, never present and
|
|
123
|
+
* throwing.
|
|
124
|
+
*/
|
|
125
|
+
export type Capability =
|
|
126
|
+
| "trace.performance" // Chrome tracing, behind every performance_* tool
|
|
127
|
+
| "heap.snapshot" // V8 heap snapshots
|
|
128
|
+
| "audit.lighthouse" // shelling out to lighthouse against a debug port
|
|
129
|
+
| "emulate.cpuThrottling" // CPU multiplier; absent from BiDi's emulation module
|
|
130
|
+
| "emulate.mediaFeatures" // media type + features, e.g. prefers-color-scheme
|
|
131
|
+
| "emulate.deviceMetrics" // past width/height: scale factor, mobile flag
|
|
132
|
+
| "emulate.networkConditions" // throttled or offline conditions
|
|
133
|
+
| "screenshot.fullPage" // one-shot full scrollable page, not just the viewport
|
|
134
|
+
| "screenshot.element" // capture clipped to a single element
|
|
135
|
+
| "network.intercept" // fulfill, fail, or continue a matched request
|
|
136
|
+
| "network.responseBodyPostHoc" // read a body for a request not armed in advance
|
|
137
|
+
| "snapshot.accessibilityTree" // native a11y dump, not a DOM-walk approximation
|
|
138
|
+
| "input.insertTextAtomic" // atomic value commit, not synthesized keystrokes
|
|
139
|
+
| "locate.text" // find a node by visible text substring
|
|
140
|
+
| "locate.xpath"; // find a node by xpath
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Tools whose availability depends on a capability. A tool absent from this
|
|
144
|
+
* map is universal and must be listed by every driver. Keyed by ToolName so
|
|
145
|
+
* the compiler catches a renamed tool.
|
|
146
|
+
*/
|
|
147
|
+
export const REQUIRED_CAPABILITIES: Partial<Record<ToolName, readonly Capability[]>> = {
|
|
148
|
+
performance_start_trace: ["trace.performance"],
|
|
149
|
+
performance_stop_trace: ["trace.performance"],
|
|
150
|
+
performance_analyze_insight: ["trace.performance"],
|
|
151
|
+
performance_trace: ["trace.performance"],
|
|
152
|
+
take_heapsnapshot: ["heap.snapshot"],
|
|
153
|
+
lighthouse_audit: ["audit.lighthouse"],
|
|
154
|
+
mock_request: ["network.intercept"],
|
|
155
|
+
list_mocks: ["network.intercept"],
|
|
156
|
+
clear_mocks: ["network.intercept"],
|
|
157
|
+
} as const;
|
|
158
|
+
|
|
159
|
+
/* --------------------------------- errors --------------------------------- */
|
|
160
|
+
|
|
161
|
+
/** Machine-readable failure kinds every driver maps its protocol errors onto. */
|
|
162
|
+
export type DriverErrorCode =
|
|
163
|
+
| "foreign-uid" // the uid's scheme belongs to another driver
|
|
164
|
+
| "stale-uid" // ours, but no longer resolves: node gone or document unloaded
|
|
165
|
+
| "no-such-element" // a locator matched nothing
|
|
166
|
+
| "no-such-target" // no page/context matched the TargetSelector
|
|
167
|
+
| "unsupported" // operation not supported; check Capability first
|
|
168
|
+
| "timeout" // the per-command deadline elapsed
|
|
169
|
+
| "disconnected" // socket closed, session ended, or browser exited
|
|
170
|
+
| "page-error"; // page-side exception: evaluate threw, navigation rejected
|
|
171
|
+
|
|
172
|
+
export interface DriverError extends Error {
|
|
173
|
+
readonly code: DriverErrorCode;
|
|
174
|
+
readonly data?: unknown;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function isDriverError(e: unknown): e is DriverError {
|
|
178
|
+
return e instanceof Error && typeof (e as { code?: unknown }).code === "string";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* ------------------------------ neutral shapes ------------------------------ */
|
|
182
|
+
|
|
183
|
+
/** A page/tab, neutral over CDP targets and BiDi browsing contexts. */
|
|
184
|
+
export interface PageInfo {
|
|
185
|
+
id: string;
|
|
186
|
+
url: string;
|
|
187
|
+
title: string;
|
|
188
|
+
/** CDP target type ("page", "iframe", "worker", ...), when the driver has one. Additive: BiDi leaves it unset. */
|
|
189
|
+
type?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** How a caller points at an element. Exactly one field is set. */
|
|
193
|
+
export type ElementLocator =
|
|
194
|
+
| { uid: DriverUid }
|
|
195
|
+
| { css: string }
|
|
196
|
+
| { xpath: string } // requires Capability "locate.xpath"
|
|
197
|
+
| { text: string }; // requires Capability "locate.text"
|
|
198
|
+
|
|
199
|
+
/** One line of the snapshot tree, before it is rendered to text. */
|
|
200
|
+
export interface SnapshotNode {
|
|
201
|
+
uid: DriverUid;
|
|
202
|
+
role: string;
|
|
203
|
+
name?: string;
|
|
204
|
+
depth: number;
|
|
205
|
+
/** value / checked / expanded / disabled / url / focused, already stringified. */
|
|
206
|
+
extras?: Record<string, string>;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface NavigateOptions {
|
|
210
|
+
url?: string;
|
|
211
|
+
reload?: boolean;
|
|
212
|
+
ignoreCache?: boolean;
|
|
213
|
+
waitUntil?: "load" | "domcontentloaded";
|
|
214
|
+
timeoutMs?: number;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface NavigateResult {
|
|
218
|
+
url: string;
|
|
219
|
+
/** CDP frameId or BiDi context id. Opaque. */
|
|
220
|
+
contextId: string;
|
|
221
|
+
reloaded?: boolean;
|
|
222
|
+
waitedFor: "load" | "domcontentloaded" | "committed" | "timeout";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface MouseButtonOptions {
|
|
226
|
+
button?: "left" | "right" | "middle";
|
|
227
|
+
clickCount?: number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface KeyPress {
|
|
231
|
+
/** "Enter", "Tab", "ArrowDown", or a single printable character. */
|
|
232
|
+
key: string;
|
|
233
|
+
/** "Control" | "Shift" | "Alt" | "Meta", case-insensitive. */
|
|
234
|
+
modifiers?: readonly string[];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface ScreenshotOptions {
|
|
238
|
+
format?: "png" | "jpeg";
|
|
239
|
+
quality?: number;
|
|
240
|
+
fullPage?: boolean; // requires "screenshot.fullPage"
|
|
241
|
+
clip?: ElementLocator; // requires "screenshot.element"
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Each optional field names the Capability it requires, if any. */
|
|
245
|
+
export interface EmulationOptions {
|
|
246
|
+
width?: number;
|
|
247
|
+
height?: number;
|
|
248
|
+
deviceScaleFactor?: number; // "emulate.deviceMetrics"
|
|
249
|
+
mobile?: boolean; // "emulate.deviceMetrics"
|
|
250
|
+
userAgent?: string;
|
|
251
|
+
cpuThrottlingRate?: number; // "emulate.cpuThrottling"
|
|
252
|
+
media?: string; // "emulate.mediaFeatures"
|
|
253
|
+
mediaFeatures?: ReadonlyArray<{ name: string; value: string }>; // "emulate.mediaFeatures"
|
|
254
|
+
/** Requires "emulate.networkConditions". connectionType is additive: CDP's optional
|
|
255
|
+
* Network.ConnectionType hint ("wifi", "cellular4g", ...); drivers without a matching
|
|
256
|
+
* concept simply ignore it. */
|
|
257
|
+
networkConditions?: { offline?: boolean; latency?: number; downloadThroughput?: number; uploadThroughput?: number; connectionType?: string };
|
|
258
|
+
clearOverrides?: boolean;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Neutral event names. Payload shapes are the driver's normalized records. */
|
|
262
|
+
export type DriverEvent =
|
|
263
|
+
| "navigated"
|
|
264
|
+
| "console"
|
|
265
|
+
| "network.request"
|
|
266
|
+
| "network.response"
|
|
267
|
+
| "dialog"
|
|
268
|
+
| "page.closed";
|
|
269
|
+
|
|
270
|
+
export interface DialogInfo {
|
|
271
|
+
type: string;
|
|
272
|
+
message: string;
|
|
273
|
+
url: string;
|
|
274
|
+
defaultPrompt?: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** One dialog the driver answered, echoing back how it was answered. */
|
|
278
|
+
export interface HandledDialogInfo extends DialogInfo {
|
|
279
|
+
accept: boolean;
|
|
280
|
+
promptText?: string;
|
|
281
|
+
handled: true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface InterceptRule {
|
|
285
|
+
/** Glob: `*` any run, `?` one char. Both drivers normalize to this. */
|
|
286
|
+
urlPattern: string;
|
|
287
|
+
method?: string;
|
|
288
|
+
action: "fulfill" | "fail" | "continue";
|
|
289
|
+
status?: number;
|
|
290
|
+
body?: string;
|
|
291
|
+
contentType?: string;
|
|
292
|
+
headers?: Record<string, string>;
|
|
293
|
+
errorReason?: string;
|
|
294
|
+
delayMs?: number;
|
|
295
|
+
failRate?: number;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* --------------------------------- drivers --------------------------------- */
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Connection ownership, declared rather than discovered.
|
|
302
|
+
* - "per-call": acquiring opens a transport and release() closes it. Nothing
|
|
303
|
+
* survives between tool calls. This is CDP today.
|
|
304
|
+
* - "session": the transport carries a session that CANNOT be re-dialed, so
|
|
305
|
+
* release() only decrements a refcount and the transport lives until
|
|
306
|
+
* dispose(). This is BiDi. A "session" driver MUST be memoized at module
|
|
307
|
+
* scope (see recorder.ts / network_mock.ts for the existing pattern) and
|
|
308
|
+
* MUST tolerate concurrent holders. It does NOT transparently re-establish
|
|
309
|
+
* itself if the transport dies: a BiDi session cannot be resumed on a
|
|
310
|
+
* second socket (re-dialing gets `invalid session id`), so a dead
|
|
311
|
+
* transport surfaces as an error on every subsequent call instead, and a
|
|
312
|
+
* caller that hits one must dispose() and acquire a fresh driver.
|
|
313
|
+
* Tools MUST NOT branch on this value. It exists so that withDriver's contract
|
|
314
|
+
* is unambiguous and so a reader knows why release() is not a close().
|
|
315
|
+
*/
|
|
316
|
+
export type LifetimeModel = "per-call" | "session";
|
|
317
|
+
|
|
318
|
+
/** Browser-scoped operations. One per browser process. */
|
|
319
|
+
export interface BrowserDriver {
|
|
320
|
+
/** Uid scheme tag owned by this driver, e.g. "cdp" or "bidi". */
|
|
321
|
+
readonly scheme: string;
|
|
322
|
+
readonly lifetime: LifetimeModel;
|
|
323
|
+
readonly capabilities: ReadonlySet<Capability>;
|
|
324
|
+
readonly uidStability: UidStability;
|
|
325
|
+
/** "accessibility-tree" when snapshots come from a native a11y dump. */
|
|
326
|
+
readonly snapshotFidelity: "accessibility-tree" | "dom-heuristic";
|
|
327
|
+
|
|
328
|
+
/** opts.all: include non-page targets (workers, iframes, ...) when the driver has that concept. */
|
|
329
|
+
listPages(opts?: { all?: boolean }): Promise<PageInfo[]>;
|
|
330
|
+
newPage(url?: string): Promise<PageInfo>;
|
|
331
|
+
/** success mirrors the underlying protocol's own success signal when it has one (CDP's
|
|
332
|
+
* Target.closeTarget); a driver without such a signal reports true once close resolves. */
|
|
333
|
+
closePage(id: string): Promise<{ success: boolean }>;
|
|
334
|
+
/** Bring a page to the front. Also writes the selected-page state file. */
|
|
335
|
+
activatePage(id: string): Promise<PageInfo>;
|
|
336
|
+
|
|
337
|
+
/** Acquire a page handle. The caller MUST release() it. Prefer withDriver. */
|
|
338
|
+
page(selector: TargetSelector): Promise<PageDriver>;
|
|
339
|
+
|
|
340
|
+
/** Tear down the transport: process exit (CLI) or shutdown (MCP server).
|
|
341
|
+
* Idempotent. MUST NOT be called by a tool. */
|
|
342
|
+
dispose(): Promise<void>;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Page-scoped operations. Every tool in src/tools/ maps onto this surface. */
|
|
346
|
+
export interface PageDriver {
|
|
347
|
+
readonly info: PageInfo;
|
|
348
|
+
readonly browser: BrowserDriver;
|
|
349
|
+
|
|
350
|
+
/* navigation */
|
|
351
|
+
navigate(opts: NavigateOptions): Promise<NavigateResult>;
|
|
352
|
+
/** Poll until `text` appears in the rendered body, or reject with "timeout". */
|
|
353
|
+
waitForText(text: string, timeoutMs?: number, pollMs?: number): Promise<{ found: true; elapsedMs: number }>;
|
|
354
|
+
|
|
355
|
+
/* script */
|
|
356
|
+
/** Evaluate an expression, or call it as a function when `args` is given. */
|
|
357
|
+
evaluate(expression: string, opts?: { args?: readonly unknown[]; awaitPromise?: boolean }): Promise<unknown>;
|
|
358
|
+
/** Call a function literal with an element as `this`, returning by value. */
|
|
359
|
+
callOnElement(loc: ElementLocator, functionDeclaration: string, args?: readonly unknown[]): Promise<unknown>;
|
|
360
|
+
|
|
361
|
+
/* elements */
|
|
362
|
+
/** Emit the snapshot tree. Mints (and, under "bidi", stamps) every uid. */
|
|
363
|
+
snapshot(opts?: { interactiveOnly?: boolean }): Promise<SnapshotNode[]>;
|
|
364
|
+
/** Mint a uid for the first node matching a non-uid locator. */
|
|
365
|
+
locate(loc: ElementLocator): Promise<DriverUid>;
|
|
366
|
+
/** Assert a uid still resolves. Rejects "stale-uid" / "foreign-uid". */
|
|
367
|
+
resolve(uid: DriverUid): Promise<void>;
|
|
368
|
+
|
|
369
|
+
/* input */
|
|
370
|
+
click(loc: ElementLocator, opts?: MouseButtonOptions): Promise<{ x: number; y: number }>;
|
|
371
|
+
hover(loc: ElementLocator): Promise<{ x: number; y: number }>;
|
|
372
|
+
drag(from: ElementLocator, to: ElementLocator): Promise<{ from: { x: number; y: number }; to: { x: number; y: number } }>;
|
|
373
|
+
/** Overwrite an element's value. One atomic commit under
|
|
374
|
+
* "input.insertTextAtomic", else synthesized keystrokes, so per-key handlers
|
|
375
|
+
* on the page WILL fire. Callers must not depend on either behavior. */
|
|
376
|
+
setValue(loc: ElementLocator, value: string): Promise<void>;
|
|
377
|
+
/** Append text at the focus point without clearing first. */
|
|
378
|
+
typeText(loc: ElementLocator, text: string): Promise<void>;
|
|
379
|
+
pressKey(press: KeyPress): Promise<void>;
|
|
380
|
+
/** Attach absolute file paths to a file input. */
|
|
381
|
+
setFiles(loc: ElementLocator, files: readonly string[]): Promise<void>;
|
|
382
|
+
|
|
383
|
+
/* rendering */
|
|
384
|
+
/** Returns raw image bytes. Artifact naming and writing stay in the tool. */
|
|
385
|
+
screenshot(opts?: ScreenshotOptions): Promise<{ data: Uint8Array; format: "png" | "jpeg" }>;
|
|
386
|
+
/** Apply overrides. Returns the field names actually applied. */
|
|
387
|
+
emulate(opts: EmulationOptions): Promise<{ applied: string[] }>;
|
|
388
|
+
|
|
389
|
+
/* events */
|
|
390
|
+
/** Subscribe to a neutral event. Returns an unsubscribe fn. */
|
|
391
|
+
on(event: DriverEvent, handler: (payload: Record<string, unknown>) => void): () => void;
|
|
392
|
+
/** Resolve on the first matching event, or reject with "timeout". */
|
|
393
|
+
waitForEvent(event: DriverEvent, predicate?: (p: Record<string, unknown>) => boolean, timeoutMs?: number): Promise<Record<string, unknown>>;
|
|
394
|
+
|
|
395
|
+
/* dialogs */
|
|
396
|
+
/** Answer the next user prompt. Combine with waitForEvent("dialog") to arm first.
|
|
397
|
+
* opts.timeoutMs: how long to wait for a dialog to open (default 15000).
|
|
398
|
+
* opts.autoMs: instead of waiting for exactly one dialog, answer every dialog that
|
|
399
|
+
* opens during this window (ms) and resolve with the list, never throwing on none. */
|
|
400
|
+
handleDialog(
|
|
401
|
+
accept: boolean,
|
|
402
|
+
promptText?: string,
|
|
403
|
+
opts?: { timeoutMs?: number; autoMs?: number },
|
|
404
|
+
): Promise<HandledDialogInfo | { handled: HandledDialogInfo[]; count: number }>;
|
|
405
|
+
|
|
406
|
+
/* network */
|
|
407
|
+
/** Begin retaining response bodies. REQUIRED before getResponseBody unless
|
|
408
|
+
* the driver declares "network.responseBodyPostHoc". Returns a stop fn. */
|
|
409
|
+
startBodyCapture(): Promise<() => Promise<void>>;
|
|
410
|
+
getResponseBody(requestId: string): Promise<{ body: string; base64Encoded: boolean }>;
|
|
411
|
+
/** Requires "network.intercept". Returns a fn that removes the rule. */
|
|
412
|
+
intercept(rules: readonly InterceptRule[]): Promise<() => Promise<void>>;
|
|
413
|
+
|
|
414
|
+
/** Give up this handle: closes the transport under "per-call", decrements a
|
|
415
|
+
* refcount under "session". Idempotent. Never assume it ends the session. */
|
|
416
|
+
release(): Promise<void>;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** The neutral replacement for withPage(): acquire, run, always release. The
|
|
420
|
+
* ONLY acquisition path a tool should use, correct under both LifetimeModels. */
|
|
421
|
+
export type WithDriver = <T>(selector: TargetSelector, fn: (page: PageDriver) => Promise<T>) => Promise<T>;
|
|
422
|
+
|
|
423
|
+
/** Names a driver can be selected by, e.g. via a CDP_BROWSER env var. */
|
|
424
|
+
export type DriverKind = "chrome" | "firefox";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The complete Firefox tool registry: the 20 tools shared with Chrome (src/shared-tools.ts) plus
|
|
3
|
+
* the 7 Firefox-only console/network/mock tools (src/bidi-tools.ts). This is what src/neutral.ts
|
|
4
|
+
* used to be; it now composes two files instead of reimplementing everything in one.
|
|
5
|
+
*/
|
|
6
|
+
import type { BrowserDriver } from "./driver.ts";
|
|
7
|
+
import type { ToolName } from "./index.ts";
|
|
8
|
+
import { SHARED_TOOLS } from "./shared-tools.ts";
|
|
9
|
+
import { BIDI_ONLY_TOOLS } from "./bidi-tools.ts";
|
|
10
|
+
|
|
11
|
+
export const FIREFOX_TOOLS: Partial<Record<ToolName, (driver: BrowserDriver, args: never) => Promise<unknown>>> = {
|
|
12
|
+
...SHARED_TOOLS,
|
|
13
|
+
...BIDI_ONLY_TOOLS,
|
|
14
|
+
};
|