cdp-mcp 0.2.1 → 0.4.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.
Files changed (96) hide show
  1. package/README.md +28 -303
  2. package/bin.js +23 -0
  3. package/contract.d.ts +1 -0
  4. package/contract.js +2 -0
  5. package/index.d.ts +1 -0
  6. package/index.js +2 -0
  7. package/package.json +31 -73
  8. package/dist/contract.d.ts +0 -11
  9. package/dist/contract.js +0 -11
  10. package/dist/contract.js.map +0 -1
  11. package/dist/index.d.ts +0 -18
  12. package/dist/index.js +0 -402
  13. package/dist/index.js.map +0 -1
  14. package/dist/locator.d.ts +0 -108
  15. package/dist/locator.js +0 -176
  16. package/dist/locator.js.map +0 -1
  17. package/dist/server.d.ts +0 -2
  18. package/dist/server.js +0 -41
  19. package/dist/server.js.map +0 -1
  20. package/dist/session/browser.d.ts +0 -29
  21. package/dist/session/browser.js +0 -409
  22. package/dist/session/browser.js.map +0 -1
  23. package/dist/session/buffers.d.ts +0 -48
  24. package/dist/session/buffers.js +0 -43
  25. package/dist/session/buffers.js.map +0 -1
  26. package/dist/session/pause.d.ts +0 -21
  27. package/dist/session/pause.js +0 -99
  28. package/dist/session/pause.js.map +0 -1
  29. package/dist/session/state.d.ts +0 -53
  30. package/dist/session/state.js +0 -93
  31. package/dist/session/state.js.map +0 -1
  32. package/dist/sourcemap/loader.d.ts +0 -4
  33. package/dist/sourcemap/loader.js +0 -138
  34. package/dist/sourcemap/loader.js.map +0 -1
  35. package/dist/sourcemap/normalize.d.ts +0 -2
  36. package/dist/sourcemap/normalize.js +0 -59
  37. package/dist/sourcemap/normalize.js.map +0 -1
  38. package/dist/sourcemap/store.d.ts +0 -57
  39. package/dist/sourcemap/store.js +0 -185
  40. package/dist/sourcemap/store.js.map +0 -1
  41. package/dist/tools/_locator_runtime.d.ts +0 -31
  42. package/dist/tools/_locator_runtime.js +0 -243
  43. package/dist/tools/_locator_runtime.js.map +0 -1
  44. package/dist/tools/_register.d.ts +0 -2
  45. package/dist/tools/_register.js +0 -30
  46. package/dist/tools/_register.js.map +0 -1
  47. package/dist/tools/breakpoints.d.ts +0 -4
  48. package/dist/tools/breakpoints.js +0 -164
  49. package/dist/tools/breakpoints.js.map +0 -1
  50. package/dist/tools/console.d.ts +0 -2
  51. package/dist/tools/console.js +0 -48
  52. package/dist/tools/console.js.map +0 -1
  53. package/dist/tools/dom.d.ts +0 -2
  54. package/dist/tools/dom.js +0 -309
  55. package/dist/tools/dom.js.map +0 -1
  56. package/dist/tools/execution.d.ts +0 -29
  57. package/dist/tools/execution.js +0 -89
  58. package/dist/tools/execution.js.map +0 -1
  59. package/dist/tools/forms.d.ts +0 -8
  60. package/dist/tools/forms.js +0 -256
  61. package/dist/tools/forms.js.map +0 -1
  62. package/dist/tools/inspect.d.ts +0 -2
  63. package/dist/tools/inspect.js +0 -178
  64. package/dist/tools/inspect.js.map +0 -1
  65. package/dist/tools/nav.d.ts +0 -2
  66. package/dist/tools/nav.js +0 -136
  67. package/dist/tools/nav.js.map +0 -1
  68. package/dist/tools/network.d.ts +0 -2
  69. package/dist/tools/network.js +0 -137
  70. package/dist/tools/network.js.map +0 -1
  71. package/dist/tools/session.d.ts +0 -2
  72. package/dist/tools/session.js +0 -76
  73. package/dist/tools/session.js.map +0 -1
  74. package/dist/tools/source.d.ts +0 -2
  75. package/dist/tools/source.js +0 -63
  76. package/dist/tools/source.js.map +0 -1
  77. package/dist/tools/storage.d.ts +0 -2
  78. package/dist/tools/storage.js +0 -296
  79. package/dist/tools/storage.js.map +0 -1
  80. package/dist/util/browser-resolve.d.ts +0 -19
  81. package/dist/util/browser-resolve.js +0 -263
  82. package/dist/util/browser-resolve.js.map +0 -1
  83. package/dist/util/errors.d.ts +0 -7
  84. package/dist/util/errors.js +0 -12
  85. package/dist/util/errors.js.map +0 -1
  86. package/dist/util/format.d.ts +0 -20
  87. package/dist/util/format.js +0 -65
  88. package/dist/util/format.js.map +0 -1
  89. package/dist/util/log.d.ts +0 -6
  90. package/dist/util/log.js +0 -34
  91. package/dist/util/log.js.map +0 -1
  92. package/docs/chromium-sandboxing.md +0 -197
  93. package/docs/known-chromium-gaps.md +0 -138
  94. package/docs/launchd-service.md +0 -217
  95. package/docs/local-l3-e2e-setup.md +0 -199
  96. package/docs/systemd-service.md +0 -233
package/dist/locator.js DELETED
@@ -1,176 +0,0 @@
1
- /**
2
- * Canonical `LocatorSpec` contract for cdp-mcp.
3
- *
4
- * This module is the single source of truth for the structured element-locator
5
- * shape that `locate`, `wait_for`, and the form-driving tools accept. It is
6
- * deliberately **side-effect free** and depends only on `zod`, so external
7
- * consumers can import it (via `cdp-mcp/contract`) to produce and validate specs
8
- * without pulling in the CLI/server. Tool code re-imports these symbols rather
9
- * than redefining them, so the published contract can never silently drift from
10
- * what the tools actually accept.
11
- */
12
- import { z } from "zod";
13
- /**
14
- * Error thrown by {@link normalizeLocator} / {@link parseLocator} for an invalid
15
- * spec. `code` mirrors the cdp-mcp tool error codes so tool handlers can re-wrap it
16
- * structurally: `"missing_arg"` when a spec is under-specified for its strategy,
17
- * `"invalid_locator"` for an unsupported strategy.
18
- */
19
- export class LocatorError extends Error {
20
- code;
21
- constructor(message, code = "missing_arg") {
22
- super(message);
23
- this.name = "LocatorError";
24
- this.code = code;
25
- }
26
- }
27
- /** The locator strategies cdp-mcp understands. `css` is the default when a selector is given. */
28
- export const locatorBySchema = z.enum([
29
- "css",
30
- "text",
31
- "role",
32
- "test_id",
33
- "testId",
34
- "label",
35
- "placeholder",
36
- "name",
37
- ]);
38
- /**
39
- * The raw Zod shape for a LocatorSpec. Spread into tool input schemas
40
- * (`{ ...locatorShape, ... }`) so the field docs stay identical everywhere.
41
- */
42
- export const locatorShape = {
43
- by: locatorBySchema.optional().describe("Locator strategy. Omit when passing selector/css for a CSS lookup."),
44
- selector: z.string().optional().describe("CSS selector. Equivalent to by=css."),
45
- css: z.string().optional().describe("CSS selector. Equivalent to selector."),
46
- text: z.string().optional().describe("Text to match for by=text."),
47
- role: z.string().optional().describe("ARIA/implicit role for by=role, e.g. button, link, textbox."),
48
- name: z.string().optional().describe("Accessible name, field name, or fallback value depending on the locator strategy."),
49
- test_id: z.string().optional().describe("Value for data-testid, data-test-id, or data-test."),
50
- // Both snake_case (`test_id`) and camelCase (`testId`) are accepted so callers
51
- // can use whichever matches their convention; `normalizeLocator` /
52
- // `serializeLocator` fold them to the canonical `test_id`. Likewise `name` is a
53
- // cross-strategy fallback that gets folded into the strategy-specific field.
54
- testId: z.string().optional().describe("CamelCase alias for test_id."),
55
- label: z.string().optional().describe("Label text for by=label."),
56
- placeholder: z.string().optional().describe("Placeholder text for by=placeholder."),
57
- exact: z.boolean().optional().describe("Default false: substring match for text/name-like fields."),
58
- };
59
- /** A standalone `ZodObject` for a LocatorSpec — external consumers can `.parse()`/`.safeParse()`. */
60
- export const locatorSchema = z.object(locatorShape);
61
- /**
62
- * Validate and canonicalize a spec: infer `by` from selector/css, enforce the
63
- * required field for the chosen strategy, and fold the `name` fallback into the
64
- * strategy-specific field. Throws {@link LocatorError} on an under-specified spec.
65
- */
66
- export function normalizeLocator(input) {
67
- const by = input.by ?? (input.selector || input.css ? "css" : undefined);
68
- if (!by)
69
- throw new LocatorError("by is required unless selector/css is supplied");
70
- switch (by) {
71
- case "css": {
72
- const selector = input.selector ?? input.css;
73
- if (!selector)
74
- throw new LocatorError("selector or css is required for by=css");
75
- return { ...input, by, selector };
76
- }
77
- case "role":
78
- if (!input.role)
79
- throw new LocatorError("role is required for by=role");
80
- return { ...input, by };
81
- case "text":
82
- if (!input.text && !input.name)
83
- throw new LocatorError("text or name is required for by=text");
84
- return { ...input, by, text: input.text ?? input.name };
85
- case "test_id":
86
- case "testId": {
87
- const testId = input.test_id ?? input.testId ?? input.name;
88
- if (!testId)
89
- throw new LocatorError(`test_id, testId, or name is required for by=${by}`);
90
- return { ...input, by, test_id: testId };
91
- }
92
- case "label": {
93
- const label = input.label ?? input.name;
94
- if (!label)
95
- throw new LocatorError("label or name is required for by=label");
96
- return { ...input, by, label };
97
- }
98
- case "placeholder": {
99
- const placeholder = input.placeholder ?? input.name;
100
- if (!placeholder)
101
- throw new LocatorError("placeholder or name is required for by=placeholder");
102
- return { ...input, by, placeholder };
103
- }
104
- case "name":
105
- if (!input.name)
106
- throw new LocatorError("name is required for by=name");
107
- return { ...input, by };
108
- default: {
109
- // Compile-time exhaustiveness: if `locatorBySchema` gains a strategy and a
110
- // case here is missed, `by` is no longer `never` and this fails to build
111
- // (pairs with `noFallthroughCasesInSwitch`).
112
- const _exhaustive = by;
113
- throw new LocatorError(`unsupported locator strategy: ${String(_exhaustive)}`, "invalid_locator");
114
- }
115
- }
116
- }
117
- /** Validate an unknown value as a LocatorSpec (shape + strategy requirements). */
118
- export function parseLocator(input) {
119
- return normalizeLocator(locatorSchema.parse(input));
120
- }
121
- /**
122
- * Serialize a LocatorSpec to a stable, normalized JSON string. Equivalent specs
123
- * serialize identically regardless of which alias the caller used — e.g.
124
- * `{ css: ".x" }` and `{ selector: ".x" }` both yield `{"by":"css","selector":".x"}`
125
- * — so the output is safe to use as a cache key or for equality checks.
126
- */
127
- export function serializeLocator(spec) {
128
- return JSON.stringify(canonicalLocator(spec));
129
- }
130
- /**
131
- * Reduce a spec to only its canonical fields, in a fixed key order, dropping
132
- * alias inputs (`css`, `testId`, and the cross-strategy `name` fallback). This is
133
- * what makes {@link serializeLocator} stable across equivalent inputs — the raw
134
- * `normalizeLocator` result still carries whichever aliases the caller passed.
135
- */
136
- function canonicalLocator(spec) {
137
- const n = normalizeLocator(spec);
138
- const out = {};
139
- switch (n.by) {
140
- case "css":
141
- out.by = "css";
142
- out.selector = n.selector;
143
- break;
144
- case "role":
145
- out.by = "role";
146
- out.role = n.role;
147
- if (n.name !== undefined)
148
- out.name = n.name;
149
- break;
150
- case "text":
151
- out.by = "text";
152
- out.text = n.text;
153
- break;
154
- case "test_id":
155
- case "testId":
156
- out.by = "test_id";
157
- out.test_id = n.test_id;
158
- break;
159
- case "label":
160
- out.by = "label";
161
- out.label = n.label;
162
- break;
163
- case "placeholder":
164
- out.by = "placeholder";
165
- out.placeholder = n.placeholder;
166
- break;
167
- case "name":
168
- out.by = "name";
169
- out.name = n.name;
170
- break;
171
- }
172
- if (n.exact !== undefined)
173
- out.exact = n.exact;
174
- return out;
175
- }
176
- //# sourceMappingURL=locator.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"locator.js","sourceRoot":"","sources":["../src/locator.ts"],"names":[],"mappings":"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"}
package/dist/server.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function buildServer(): McpServer;
package/dist/server.js DELETED
@@ -1,41 +0,0 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { registerSessionTools } from "./tools/session.js";
3
- import { registerNavTools } from "./tools/nav.js";
4
- import { registerSourceTools } from "./tools/source.js";
5
- import { registerBreakpointTools } from "./tools/breakpoints.js";
6
- import { registerExecutionTools } from "./tools/execution.js";
7
- import { registerInspectTools } from "./tools/inspect.js";
8
- import { registerConsoleTools } from "./tools/console.js";
9
- import { registerNetworkTools } from "./tools/network.js";
10
- import { registerDomTools } from "./tools/dom.js";
11
- import { registerFormTools } from "./tools/forms.js";
12
- import { registerStorageTools } from "./tools/storage.js";
13
- export function buildServer() {
14
- const server = new McpServer({
15
- name: "cdp-mcp",
16
- version: "0.1.2",
17
- });
18
- registerSessionTools(server);
19
- registerNavTools(server);
20
- registerSourceTools(server);
21
- registerBreakpointTools(server);
22
- registerExecutionTools(server);
23
- registerInspectTools(server);
24
- registerConsoleTools(server);
25
- registerNetworkTools(server);
26
- registerDomTools(server);
27
- registerFormTools(server);
28
- registerStorageTools(server);
29
- // The SDK advertises `tools: { listChanged: true }` as soon as any tool is
30
- // registered, but never emits the matching notification on its own. Some
31
- // clients (e.g. GitHub Copilot CLI over SSE) gate their first `tools/list`
32
- // call on receiving a `notifications/tools/list_changed` and otherwise wait
33
- // forever — registering zero tools. Emit it once, right after the client's
34
- // `initialized` (so the transport is connected and the client is ready).
35
- // Harmless for clients that already fetch eagerly. See issue #1.
36
- server.server.oninitialized = () => {
37
- server.sendToolListChanged();
38
- };
39
- return server;
40
- }
41
- //# sourceMappingURL=server.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,MAAM,UAAU,WAAW;IACzB,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACzB,mBAAmB,CAAC,MAAM,CAAC,CAAC;IAC5B,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAChC,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAC/B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAC7B,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACzB,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,oBAAoB,CAAC,MAAM,CAAC,CAAC;IAE7B,2EAA2E;IAC3E,yEAAyE;IACzE,2EAA2E;IAC3E,4EAA4E;IAC5E,2EAA2E;IAC3E,yEAAyE;IACzE,iEAAiE;IACjE,MAAM,CAAC,MAAM,CAAC,aAAa,GAAG,GAAG,EAAE;QACjC,MAAM,CAAC,mBAAmB,EAAE,CAAC;IAC/B,CAAC,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -1,29 +0,0 @@
1
- export interface LaunchArgs {
2
- url?: string;
3
- headless?: boolean;
4
- userDataDir?: string;
5
- args?: string[];
6
- chromePath?: string;
7
- sandbox?: boolean;
8
- }
9
- export interface AttachArgs {
10
- port?: number;
11
- host?: string;
12
- targetFilter?: {
13
- type?: string;
14
- urlIncludes?: string;
15
- };
16
- }
17
- export declare function launchChrome(opts?: LaunchArgs): Promise<{
18
- targetId: string;
19
- url: string;
20
- }>;
21
- export declare function attachChrome(opts?: AttachArgs): Promise<{
22
- targetId: string;
23
- url: string;
24
- }>;
25
- export declare function closeSession(): Promise<void>;
26
- export declare function switchTarget(targetId: string): Promise<{
27
- targetId: string;
28
- url: string;
29
- }>;
@@ -1,409 +0,0 @@
1
- import { mkdirSync } from "node:fs";
2
- import CDP from "chrome-remote-interface";
3
- import { launch } from "chrome-launcher";
4
- import { sessionState, ROOT_SESSION_KEY } from "./state.js";
5
- import { attachScriptListener } from "../sourcemap/loader.js";
6
- import { mapCdpToOriginal } from "../sourcemap/store.js";
7
- import { alreadySession } from "../util/errors.js";
8
- import { previewRemoteObject } from "../util/format.js";
9
- import { log } from "../util/log.js";
10
- import { snapUserDataDir } from "../util/browser-resolve.js";
11
- const DEFAULT_PORT = 9222;
12
- export async function launchChrome(opts = {}) {
13
- if (sessionState.client)
14
- throw alreadySession();
15
- // chrome-launcher manages --remote-debugging-port itself: it picks an
16
- // unused port, adds the flag, and polls it. Passing our own
17
- // --remote-debugging-port=0 in chromeFlags appears AFTER chrome-launcher's,
18
- // and since Chrome honors the last occurrence, Chrome picks an ephemeral
19
- // port written to DevToolsActivePort while chrome-launcher polls its own
20
- // (stale) port → ECONNREFUSED on every connect. Don't pass it; let
21
- // chrome-launcher own port selection. `runningChrome.port` then reflects
22
- // the actual port Chrome is listening on. (Codex blocker review on PR #11.)
23
- // Sandbox decision: an explicit `sandbox` arg from the caller always wins.
24
- // When the caller omits it, fall back to the CDP_SANDBOX env (default off);
25
- // "true" or "1" enable it (matching the eval runner's EVAL_SANDBOX parsing).
26
- // This lets a host with a working sandbox path opt a whole run into
27
- // sandbox-on (e.g. the L4 eval runner via EVAL_SANDBOX → CDP_SANDBOX)
28
- // without prompt-injecting every launch_chrome call. Unset env → false →
29
- // the --no-sandbox automation default (unchanged). Explicit `sandbox: false`
30
- // still forces --no-sandbox even if the env is set.
31
- const sandboxEnv = process.env.CDP_SANDBOX;
32
- const useSandbox = opts.sandbox ?? (sandboxEnv === "true" || sandboxEnv === "1");
33
- const userArgs = opts.args ?? [];
34
- const userAlreadyDisabled = userArgs.includes("--no-sandbox");
35
- // A caller can request the sandbox AND still pass --no-sandbox in args; the
36
- // userArgs spread re-adds it last, so Chromium ends up unsandboxed despite the
37
- // request. Warn rather than silently dropping the sandbox.
38
- if (useSandbox && userAlreadyDisabled) {
39
- log.warn("launch_chrome: sandbox requested but --no-sandbox is in args; the flag wins and the sandbox stays OFF");
40
- }
41
- // Snap-confinement auto-profile. When the effective Chrome path (explicit
42
- // chromePath, or CHROME_PATH env that chrome-launcher will pick up) is
43
- // under /snap/ AND the caller didn't already specify userDataDir, derive
44
- // the snap-confined profile path so chrome-launcher doesn't hand snap-
45
- // Chromium a /tmp/... profile that snap confinement rejects (debug port
46
- // never opens; chrome-launcher's startup-port poll ECONNREFUSEs). Mirrors
47
- // the L3 globalSetup logic in test/e2e/setup/global.ts so the L4 eval
48
- // harness (which steers chrome-launcher via CHROME_PATH) and direct
49
- // launch_chrome callers inherit the same workaround without making the
50
- // agent responsible for it. (Codex review on PR #24.)
51
- const effectiveChromePath = opts.chromePath ?? process.env.CHROME_PATH;
52
- const autoUserDataDir = !opts.userDataDir && effectiveChromePath?.startsWith("/snap/")
53
- ? snapUserDataDir(effectiveChromePath)
54
- : undefined;
55
- if (autoUserDataDir) {
56
- mkdirSync(autoUserDataDir, { recursive: true });
57
- }
58
- const launchOpts = {
59
- startingUrl: opts.url ?? "about:blank",
60
- chromeFlags: [
61
- ...(opts.headless ? ["--headless=new"] : []),
62
- ...(!useSandbox && !userAlreadyDisabled ? ["--no-sandbox"] : []),
63
- ...userArgs,
64
- ],
65
- ...(opts.userDataDir
66
- ? { userDataDir: opts.userDataDir }
67
- : autoUserDataDir
68
- ? { userDataDir: autoUserDataDir }
69
- : {}),
70
- ...(opts.chromePath ? { chromePath: opts.chromePath } : {}),
71
- };
72
- const chrome = await launch(launchOpts);
73
- sessionState.chrome = chrome;
74
- sessionState.chromePort = chrome.port;
75
- log.info("launched chrome", { port: chrome.port, pid: chrome.pid, sandbox: useSandbox });
76
- // Pick the first page target.
77
- const targets = await waitForFirstPage(chrome.port);
78
- const target = targets[0];
79
- await connectToTarget(chrome.port, target.id);
80
- return { targetId: target.id, url: target.url };
81
- }
82
- export async function attachChrome(opts = {}) {
83
- if (sessionState.client)
84
- throw alreadySession();
85
- const port = opts.port ?? DEFAULT_PORT;
86
- sessionState.chromePort = port;
87
- sessionState.attached = true;
88
- const targets = await CDP.List({ port, host: opts.host });
89
- const wantType = opts.targetFilter?.type;
90
- const wantUrl = opts.targetFilter?.urlIncludes;
91
- const filtered = targets.filter((t) => {
92
- // When a type filter is supplied, it is authoritative. Otherwise default
93
- // to "page" targets (the common debugging case).
94
- if (wantType) {
95
- if (t.type !== wantType)
96
- return false;
97
- }
98
- else if (t.type !== "page") {
99
- return false;
100
- }
101
- if (wantUrl && !t.url.includes(wantUrl))
102
- return false;
103
- return true;
104
- });
105
- if (filtered.length === 0) {
106
- throw new Error(`No matching targets on the running Chrome (filter type=${wantType ?? "page"}, urlIncludes=${wantUrl ?? "*"})`);
107
- }
108
- const target = filtered[0];
109
- await connectToTarget(port, target.id, opts.host);
110
- log.info("attached to chrome", { port, targetId: target.id, url: target.url });
111
- return { targetId: target.id, url: target.url };
112
- }
113
- async function waitForFirstPage(port) {
114
- const deadline = Date.now() + 5000;
115
- while (Date.now() < deadline) {
116
- try {
117
- const list = await CDP.List({ port });
118
- const pages = list.filter((t) => t.type === "page");
119
- if (pages.length > 0)
120
- return pages;
121
- }
122
- catch {
123
- // Chrome may not be ready yet
124
- }
125
- await new Promise((r) => setTimeout(r, 100));
126
- }
127
- throw new Error("Chrome did not expose any page targets within 5s");
128
- }
129
- async function connectToTarget(port, targetId, host) {
130
- const client = await CDP({ port, host, target: targetId });
131
- sessionState.client = client;
132
- sessionState.currentTargetId = targetId;
133
- // Wire root-target handlers and Target.attachedToTarget BEFORE setAutoAttach
134
- // — Chrome immediately enumerates pre-existing eligible children (workers,
135
- // OOPIFs, service workers) inline with the setAutoAttach response. If the
136
- // listener is registered after, those attachedToTarget events are dropped.
137
- wireDomainHandlers(client, undefined);
138
- const onAttached = (params) => {
139
- void onChildAttached(client, params);
140
- };
141
- const onDetached = (params) => {
142
- detachSession(client, params.sessionId);
143
- };
144
- client.on("Target.attachedToTarget", onAttached);
145
- client.on("Target.detachedFromTarget", onDetached);
146
- client.on("disconnect", () => log.warn("CDP disconnect"));
147
- await enableDomains(client, undefined);
148
- try {
149
- await client.Target.setAutoAttach({
150
- autoAttach: true,
151
- waitForDebuggerOnStart: false,
152
- flatten: true,
153
- });
154
- }
155
- catch (e) {
156
- log.warn("Target.setAutoAttach failed", { error: String(e) });
157
- }
158
- }
159
- async function onChildAttached(client, params) {
160
- const sessionId = params.sessionId;
161
- log.debug("child target attached", { sessionId, type: params.targetInfo.type, url: params.targetInfo.url });
162
- try {
163
- wireDomainHandlers(client, sessionId);
164
- await enableDomains(client, sessionId);
165
- await client.Target.setAutoAttach({ autoAttach: true, waitForDebuggerOnStart: false, flatten: true }, sessionId);
166
- // Inherit pause-on-exceptions setting so child sessions honor it from
167
- // birth, not only if the user re-issues the tool after they attach.
168
- if (sessionState.pauseOnExceptions !== "none") {
169
- try {
170
- await client.Debugger.setPauseOnExceptions({ state: sessionState.pauseOnExceptions }, sessionId);
171
- }
172
- catch (e) {
173
- log.warn("failed to apply pauseOnExceptions to child", { sessionId, error: String(e) });
174
- }
175
- }
176
- }
177
- catch (e) {
178
- log.warn("failed to enable child target", { sessionId, error: String(e) });
179
- }
180
- }
181
- function detachSession(client, sessionId) {
182
- log.debug("child target detached", { sessionId });
183
- // Remove every event handler we registered for this sub-session and drop
184
- // its scripts from the store — those scriptIds are now invalid.
185
- const handlers = sessionState.sessionHandlers.get(sessionId);
186
- if (handlers) {
187
- for (const { event, handler } of handlers) {
188
- client.removeListener(event, handler);
189
- }
190
- sessionState.sessionHandlers.delete(sessionId);
191
- }
192
- // Drop scripts owned by this session so stale scriptIds don't survive.
193
- for (const sc of sessionState.scripts.all()) {
194
- if (sc.sessionId === sessionId)
195
- sessionState.scripts.remove(sc.scriptId, sc.sessionId);
196
- }
197
- }
198
- async function enableDomains(client, sessionId) {
199
- // The full debugger surface needs these. Some are no-ops on workers but harmless.
200
- const swallow = (p) => p.then(() => { }, () => { });
201
- await swallow(client.Runtime.enable(sessionId));
202
- await swallow(client.Debugger.enable({}, sessionId));
203
- await swallow(client.Page.enable(sessionId));
204
- await swallow(client.DOM.enable({}, sessionId));
205
- await swallow(client.Network.enable({}, sessionId));
206
- }
207
- function wireDomainHandlers(client, sessionId) {
208
- // The strict gate: only process events from THIS session. The original
209
- // implementation used `if (sessionId && eventSessionId !== sessionId)`,
210
- // which is vacuously false when sessionId is undefined (the root) —
211
- // making the root handler process every child session's events too.
212
- const own = (eventSessionId) => eventSessionId === sessionId;
213
- const registered = [];
214
- const reg = (event, handler) => {
215
- client.on(event, handler);
216
- registered.push({ event, handler });
217
- };
218
- // Source-map / Debugger.scriptParsed
219
- const scriptHandler = attachScriptListener(client, sessionState.scripts, sessionId);
220
- registered.push({ event: "Debugger.scriptParsed", handler: scriptHandler });
221
- const onPaused = (params, eventSessionId) => {
222
- if (!own(eventSessionId))
223
- return;
224
- sessionState.pause.onPaused({
225
- reason: params.reason,
226
- data: params.data,
227
- hitBreakpoints: params.hitBreakpoints,
228
- callFrames: params.callFrames,
229
- asyncStackTrace: params.asyncStackTrace,
230
- sessionId: eventSessionId,
231
- pausedAt: Date.now(),
232
- });
233
- log.debug("paused", { reason: params.reason, hit: params.hitBreakpoints });
234
- };
235
- reg("Debugger.paused", onPaused);
236
- const onResumed = (_p, eventSessionId) => {
237
- if (!own(eventSessionId))
238
- return;
239
- sessionState.pause.onResumed();
240
- };
241
- reg("Debugger.resumed", onResumed);
242
- const onConsoleApi = (params, eventSessionId) => {
243
- if (!own(eventSessionId))
244
- return;
245
- pushConsoleFromApi(sessionState, params, eventSessionId);
246
- };
247
- reg("Runtime.consoleAPICalled", onConsoleApi);
248
- const onException = (params, eventSessionId) => {
249
- if (!own(eventSessionId))
250
- return;
251
- pushConsoleFromException(sessionState, params, eventSessionId);
252
- };
253
- reg("Runtime.exceptionThrown", onException);
254
- // Predicate that matches an entry by (requestId, sessionId). CDP requestIds
255
- // are scoped per Network agent — two iframes can both emit requestId="123" —
256
- // so the predicate must include sessionId to avoid cross-session collisions
257
- // in RingBuffer.update.
258
- const matchEntry = (requestId) => (e) => e.requestId === requestId && e.sessionId === sessionId;
259
- const onRequest = (params, eventSessionId) => {
260
- if (!own(eventSessionId))
261
- return;
262
- sessionState.network.push({
263
- requestId: params.requestId,
264
- ts: Date.now(),
265
- url: params.request.url,
266
- method: params.request.method,
267
- resourceType: params.type,
268
- ...(sessionId ? { sessionId } : {}),
269
- });
270
- };
271
- reg("Network.requestWillBeSent", onRequest);
272
- const onResponse = (params, eventSessionId) => {
273
- if (!own(eventSessionId))
274
- return;
275
- sessionState.network.update(matchEntry(params.requestId), {
276
- status: params.response.status,
277
- statusText: params.response.statusText,
278
- mimeType: params.response.mimeType,
279
- fromCache: params.response.fromDiskCache || params.response.fromPrefetchCache,
280
- });
281
- };
282
- reg("Network.responseReceived", onResponse);
283
- const onLoadingFinished = (params, eventSessionId) => {
284
- if (!own(eventSessionId))
285
- return;
286
- // Use the entry's own ts (set at requestWillBeSent) to compute duration.
287
- const existing = sessionState.network.query({ filter: matchEntry(params.requestId), limit: 1 }).pop();
288
- sessionState.network.update(matchEntry(params.requestId), {
289
- ...(existing ? { durationMs: Date.now() - existing.ts } : {}),
290
- finished: true,
291
- });
292
- };
293
- reg("Network.loadingFinished", onLoadingFinished);
294
- const onLoadingFailed = (params, eventSessionId) => {
295
- if (!own(eventSessionId))
296
- return;
297
- // Symmetric with onLoadingFinished: time-to-failure (DNS error, connect
298
- // refused, RST, abort, …) is useful for latency/anomaly analysis. Without
299
- // this, duration_ms is `number | undefined` purely as a function of
300
- // success vs. failure in the same call to get_network_requests.
301
- const existing = sessionState.network.query({ filter: matchEntry(params.requestId), limit: 1 }).pop();
302
- sessionState.network.update(matchEntry(params.requestId), {
303
- ...(existing ? { durationMs: Date.now() - existing.ts } : {}),
304
- failureReason: params.errorText,
305
- finished: true,
306
- });
307
- };
308
- reg("Network.loadingFailed", onLoadingFailed);
309
- sessionState.sessionHandlers.set(sessionId ?? ROOT_SESSION_KEY, registered);
310
- }
311
- function pushConsoleFromApi(s, params, sessionId) {
312
- // Two formatting modes:
313
- // - String args render as raw text (no surrounding quotes, no \n→\\n
314
- // escapes) — matches how DevTools / Node / every log shipper renders
315
- // console.log("Server started"). previewRemoteObject would
316
- // JSON.stringify them, which is right for REPL/evaluate output but
317
- // wrong for console buffering.
318
- // - Everything else (numbers, objects, arrays, functions, …) goes
319
- // through previewRemoteObject so {foo:1} keeps its shape.
320
- const renderArg = (a) => a.type === "string" ? (a.value ?? "") : previewRemoteObject(a);
321
- const text = params.args.length === 0
322
- ? "(no args)"
323
- : params.args.map(renderArg).join(" ");
324
- const top = params.stackTrace?.callFrames?.[0];
325
- const mapped = top ? mapCdpToOriginal(s.scripts, {
326
- scriptId: top.scriptId,
327
- lineNumber: top.lineNumber,
328
- columnNumber: top.columnNumber,
329
- }, sessionId) : null;
330
- s.console.push({
331
- ts: Date.now(),
332
- level: mapApiLevel(params.type),
333
- text,
334
- source: "console-api",
335
- ...(top?.url ? { url: top.url } : {}),
336
- ...(top ? { lineNumber: top.lineNumber, columnNumber: top.columnNumber } : {}),
337
- ...(mapped ? { mappedFile: mapped.file, mappedLine: mapped.line, mappedColumn: mapped.column } : {}),
338
- ...(params.stackTrace ? { stack: params.stackTrace } : {}),
339
- });
340
- }
341
- function pushConsoleFromException(s, params, sessionId) {
342
- const det = params.exceptionDetails;
343
- const top = det.stackTrace?.callFrames?.[0];
344
- const mapped = top ? mapCdpToOriginal(s.scripts, {
345
- scriptId: top.scriptId,
346
- lineNumber: top.lineNumber,
347
- columnNumber: top.columnNumber,
348
- }, sessionId) : null;
349
- const text = (det.exception?.description ?? det.exception?.value ?? det.text) + "";
350
- s.console.push({
351
- ts: Date.now(),
352
- level: "error",
353
- text,
354
- source: "runtime-exception",
355
- ...(top?.url ? { url: top.url } : { url: det.url }),
356
- ...(top ? { lineNumber: top.lineNumber, columnNumber: top.columnNumber } : { lineNumber: det.lineNumber, columnNumber: det.columnNumber }),
357
- ...(mapped ? { mappedFile: mapped.file, mappedLine: mapped.line, mappedColumn: mapped.column } : {}),
358
- ...(det.stackTrace ? { stack: det.stackTrace } : {}),
359
- });
360
- }
361
- function mapApiLevel(t) {
362
- switch (t) {
363
- case "warning":
364
- return "warn";
365
- case "error":
366
- return "error";
367
- case "debug":
368
- return "debug";
369
- case "info":
370
- return "info";
371
- case "trace":
372
- return "trace";
373
- case "log":
374
- default:
375
- return "log";
376
- }
377
- }
378
- export async function closeSession() {
379
- await sessionState.close();
380
- }
381
- // Switch to a different target on the same browser without tearing down the
382
- // chrome process. Used by select_target.
383
- export async function switchTarget(targetId) {
384
- if (!sessionState.client)
385
- throw new Error("No active session");
386
- const port = sessionState.chromePort;
387
- const attached = sessionState.attached;
388
- const chrome = sessionState.chrome;
389
- try {
390
- await sessionState.client.close().catch(() => { });
391
- }
392
- catch {
393
- /* ignore */
394
- }
395
- sessionState.client = null;
396
- sessionState.currentTargetId = null;
397
- sessionState.pause.reset();
398
- sessionState.scripts.clear();
399
- sessionState.breakpoints.clear();
400
- sessionState.sessionHandlers.clear();
401
- sessionState.chromePort = port;
402
- sessionState.attached = attached;
403
- sessionState.chrome = chrome;
404
- await connectToTarget(port, targetId);
405
- const list = await CDP.List({ port });
406
- const t = list.find((x) => x.id === targetId);
407
- return { targetId, url: t?.url ?? "" };
408
- }
409
- //# sourceMappingURL=browser.js.map