@crouton-kit/capture 1.3.6 → 1.3.8

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 (43) hide show
  1. package/bin/capture +3269 -1281
  2. package/package.json +4 -1
  3. package/src/capture.ts +30 -0
  4. package/src/cdp/args.ts +113 -10
  5. package/src/cdp/bridge/client.ts +49 -0
  6. package/src/cdp/bridge/protocol.ts +137 -0
  7. package/src/cdp/bridge/server.ts +241 -0
  8. package/src/cdp/bridge/spawn.ts +127 -0
  9. package/src/cdp/client.ts +7 -1
  10. package/src/cdp/commands/bridge-serve.ts +43 -0
  11. package/src/cdp/commands/cdp.ts +203 -0
  12. package/src/cdp/commands/measure/census.ts +31 -0
  13. package/src/cdp/commands/measure/check.ts +35 -0
  14. package/src/cdp/commands/measure/diff.ts +32 -0
  15. package/src/cdp/commands/measure/explain.ts +31 -0
  16. package/src/cdp/commands/measure/index.ts +114 -0
  17. package/src/cdp/commands/measure/map-focus.ts +24 -0
  18. package/src/cdp/commands/measure/map-layers.ts +25 -0
  19. package/src/cdp/commands/measure/map-scroll.ts +24 -0
  20. package/src/cdp/commands/measure/snap.ts +36 -0
  21. package/src/cdp/commands/measure/sweep.ts +30 -0
  22. package/src/cdp/commands/motion/index.ts +60 -0
  23. package/src/cdp/commands/motion/jank.ts +22 -0
  24. package/src/cdp/commands/motion/mask.ts +23 -0
  25. package/src/cdp/commands/motion/rec.ts +33 -0
  26. package/src/cdp/commands/motion/response.ts +26 -0
  27. package/src/cdp/commands/motion/timeline.ts +27 -0
  28. package/src/cdp/coordinates.ts +158 -0
  29. package/src/cdp/dispatch.ts +8 -0
  30. package/src/cdp/domains.ts +75 -0
  31. package/src/cdp/recorder-bridge.ts +521 -0
  32. package/src/cdp/source-map.ts +309 -0
  33. package/src/cdp/targets.ts +8 -1
  34. package/src/cdp/timing.ts +82 -0
  35. package/src/cdp/types.ts +71 -0
  36. package/src/cdp/usage.ts +9 -0
  37. package/src/output/artifact.ts +520 -0
  38. package/src/output/diff.ts +111 -0
  39. package/src/output/render.ts +593 -0
  40. package/src/output/selector.ts +185 -0
  41. package/src/session/artifacts.ts +229 -0
  42. package/src/session/commands.ts +183 -20
  43. package/src/session-context.ts +50 -8
@@ -0,0 +1,309 @@
1
+ /**
2
+ * Source provenance helper: resolves a generated CSS/JS location (as reported by
3
+ * `CSS.getMatchedStylesForNode`'s stylesheet ranges, or any other generated-source location)
4
+ * to an authored file/line/column via the standard source-map-v3 `mappings` VLQ format, when
5
+ * the generated source carries a `sourceMappingURL`. Degrades to the generated source URL when
6
+ * no source map is present, fails to fetch, or fails to parse — this module never throws for
7
+ * "no source map available"; it throws only for a malformed CDP response.
8
+ *
9
+ * No `source-map` npm dependency is added (this unit does not own `package.json`); the VLQ
10
+ * decoder below is a self-contained implementation of the standard algorithm.
11
+ *
12
+ * Selector/specificity fields that round out full provenance (used by `styles.json`) are the
13
+ * caller's (U08 styles collector's) responsibility — this module resolves source *location*
14
+ * only.
15
+ */
16
+
17
+ import type { CDPClient } from './client.js';
18
+
19
+ /** Where a generated location came from and which position in it to resolve. */
20
+ export interface GeneratedSourceRef {
21
+ /** A CDP stylesheet id — when present, the generated text is read via `CSS.getStyleSheetText`. */
22
+ styleSheetId?: string;
23
+ /**
24
+ * Absolute URL of the generated source. Used as the fetch target when `styleSheetId` is
25
+ * absent, as the base URL for resolving a relative `sourceMappingURL`, and as the reported
26
+ * generated-source URL either way.
27
+ */
28
+ sourceURL?: string;
29
+ /** 0-indexed line in the generated source. */
30
+ line: number;
31
+ /** 0-indexed column in the generated source. */
32
+ column: number;
33
+ }
34
+
35
+ export interface GeneratedLocation {
36
+ kind: 'generated';
37
+ sourceURL: string;
38
+ line: number;
39
+ column: number;
40
+ }
41
+
42
+ export interface AuthoredLocation {
43
+ kind: 'authored';
44
+ generated: { sourceURL: string; line: number; column: number };
45
+ /** Authored source file path/URL as recorded in the source map's `sources` (joined with `sourceRoot` when present). */
46
+ file: string;
47
+ /** 1-indexed authored line. */
48
+ line: number;
49
+ /** 0-indexed authored column. */
50
+ column: number;
51
+ /** Inlined authored source text, when the source map carries `sourcesContent` for this source. */
52
+ sourceContent?: string;
53
+ }
54
+
55
+ export type ResolvedSourceLocation = AuthoredLocation | GeneratedLocation;
56
+
57
+ /**
58
+ * Resolves `ref` to an authored file/line/column when a `sourceMappingURL` is present and
59
+ * resolvable, otherwise returns the generated source location unchanged.
60
+ */
61
+ export async function resolveAuthoredSourceLocation(
62
+ client: CDPClient,
63
+ ref: GeneratedSourceRef,
64
+ ): Promise<ResolvedSourceLocation> {
65
+ const { text, sourceURL } = await fetchGeneratedText(client, ref);
66
+ const generated: GeneratedLocation = { kind: 'generated', sourceURL, line: ref.line, column: ref.column };
67
+
68
+ if (!text) return generated;
69
+
70
+ const mappingURL = extractSourceMappingURL(text);
71
+ if (!mappingURL) return generated;
72
+
73
+ const rawMap = await loadSourceMap(mappingURL, sourceURL);
74
+ if (!rawMap) return generated;
75
+
76
+ const authored = mapGeneratedPosition(rawMap, ref.line, ref.column);
77
+ if (!authored) return generated;
78
+
79
+ return {
80
+ kind: 'authored',
81
+ generated: { sourceURL, line: ref.line, column: ref.column },
82
+ file: authored.file,
83
+ line: authored.line,
84
+ column: authored.column,
85
+ sourceContent: authored.sourceContent,
86
+ };
87
+ }
88
+
89
+ async function fetchGeneratedText(
90
+ client: CDPClient,
91
+ ref: GeneratedSourceRef,
92
+ ): Promise<{ text: string | null; sourceURL: string }> {
93
+ if (ref.styleSheetId) {
94
+ try {
95
+ const result = (await client.send('CSS.getStyleSheetText', {
96
+ styleSheetId: ref.styleSheetId,
97
+ })) as { text: string };
98
+ return { text: result.text, sourceURL: ref.sourceURL ?? ref.styleSheetId };
99
+ } catch {
100
+ // Stylesheet text unavailable (e.g. a stale styleSheetId) — fall through to a direct fetch.
101
+ }
102
+ }
103
+
104
+ if (ref.sourceURL) {
105
+ try {
106
+ const text = await fetchText(ref.sourceURL);
107
+ return { text, sourceURL: ref.sourceURL };
108
+ } catch {
109
+ return { text: null, sourceURL: ref.sourceURL };
110
+ }
111
+ }
112
+
113
+ return { text: null, sourceURL: '' };
114
+ }
115
+
116
+ const SOURCE_MAPPING_URL_RE = /\/\*[#@]\s*sourceMappingURL=([^\s*]+)\s*\*\/|\/\/[#@]\s*sourceMappingURL=([^\s]+)/g;
117
+
118
+ /** Finds the `sourceMappingURL` comment in generated CSS/JS text. When more than one is present, the last wins (spec convention). */
119
+ export function extractSourceMappingURL(text: string): string | null {
120
+ SOURCE_MAPPING_URL_RE.lastIndex = 0;
121
+ let match: RegExpExecArray | null;
122
+ let last: string | null = null;
123
+ while ((match = SOURCE_MAPPING_URL_RE.exec(text)) !== null) {
124
+ last = match[1] ?? match[2] ?? last;
125
+ }
126
+ return last;
127
+ }
128
+
129
+ export interface RawSourceMap {
130
+ version: number;
131
+ sources: string[];
132
+ sourcesContent?: (string | null)[];
133
+ names?: string[];
134
+ mappings: string;
135
+ sourceRoot?: string;
136
+ file?: string;
137
+ }
138
+
139
+ async function loadSourceMap(mappingURL: string, baseURL: string): Promise<RawSourceMap | null> {
140
+ if (mappingURL.startsWith('data:')) {
141
+ return parseDataURISourceMap(mappingURL);
142
+ }
143
+
144
+ try {
145
+ const resolved = baseURL ? new URL(mappingURL, baseURL).toString() : mappingURL;
146
+ const text = await fetchText(resolved);
147
+ return JSON.parse(text) as RawSourceMap;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function parseDataURISourceMap(dataURI: string): RawSourceMap | null {
154
+ const commaIndex = dataURI.indexOf(',');
155
+ if (commaIndex === -1) return null;
156
+ const meta = dataURI.slice('data:'.length, commaIndex);
157
+ const payload = dataURI.slice(commaIndex + 1);
158
+ try {
159
+ const json = meta.includes(';base64') ? Buffer.from(payload, 'base64').toString('utf8') : decodeURIComponent(payload);
160
+ return JSON.parse(json) as RawSourceMap;
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ async function fetchText(url: string): Promise<string> {
167
+ const response = await fetch(url);
168
+ if (!response.ok) {
169
+ throw new Error(`Fetching ${url} returned HTTP ${response.status}`);
170
+ }
171
+ return response.text();
172
+ }
173
+
174
+ export interface AuthoredPosition {
175
+ file: string;
176
+ /** 1-indexed authored line. */
177
+ line: number;
178
+ /** 0-indexed authored column. */
179
+ column: number;
180
+ sourceContent?: string;
181
+ }
182
+
183
+ /**
184
+ * Maps a 0-indexed (generatedLine, generatedColumn) position through a parsed source-map-v3
185
+ * `mappings` string to the nearest preceding authored position on that generated line. Pure —
186
+ * no I/O, no CDP — so it is directly testable against a fixture map.
187
+ */
188
+ export function mapGeneratedPosition(
189
+ rawMap: RawSourceMap,
190
+ generatedLine: number,
191
+ generatedColumn: number,
192
+ ): AuthoredPosition | null {
193
+ const entries = decodeMappings(rawMap.mappings).filter(
194
+ (entry) => entry.generatedLine === generatedLine && entry.sourceIndex !== undefined,
195
+ );
196
+ if (entries.length === 0) return null;
197
+
198
+ let best: MappingEntry | undefined;
199
+ for (const entry of entries) {
200
+ if (entry.generatedColumn <= generatedColumn && (!best || entry.generatedColumn > best.generatedColumn)) {
201
+ best = entry;
202
+ }
203
+ }
204
+ best = best ?? entries[0];
205
+
206
+ const sourceIndex = best.sourceIndex as number;
207
+ const sourcePath = rawMap.sources[sourceIndex];
208
+ if (sourcePath === undefined) return null;
209
+
210
+ const file = rawMap.sourceRoot ? joinSourceRoot(rawMap.sourceRoot, sourcePath) : sourcePath;
211
+ const sourceContent = rawMap.sourcesContent?.[sourceIndex] ?? undefined;
212
+
213
+ return {
214
+ file,
215
+ line: (best.sourceLine ?? 0) + 1,
216
+ column: best.sourceColumn ?? 0,
217
+ sourceContent,
218
+ };
219
+ }
220
+
221
+ function joinSourceRoot(root: string, source: string): string {
222
+ if (source.startsWith('/') || /^[a-z]+:\/\//i.test(source)) return source;
223
+ return root.endsWith('/') ? `${root}${source}` : `${root}/${source}`;
224
+ }
225
+
226
+ interface MappingEntry {
227
+ generatedLine: number;
228
+ generatedColumn: number;
229
+ sourceIndex?: number;
230
+ sourceLine?: number;
231
+ sourceColumn?: number;
232
+ nameIndex?: number;
233
+ }
234
+
235
+ const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
236
+ const BASE64_LOOKUP = new Map<string, number>(Array.from(BASE64_CHARS).map((char, index) => [char, index]));
237
+
238
+ const VLQ_BASE_SHIFT = 5;
239
+ const VLQ_BASE = 1 << VLQ_BASE_SHIFT;
240
+ const VLQ_BASE_MASK = VLQ_BASE - 1;
241
+ const VLQ_CONTINUATION_BIT = VLQ_BASE;
242
+
243
+ /** Decodes every VLQ-encoded number concatenated in one comma-delimited mapping segment. */
244
+ function decodeVLQSegment(segment: string): number[] {
245
+ const values: number[] = [];
246
+ let shift = 0;
247
+ let result = 0;
248
+
249
+ for (const char of segment) {
250
+ const digit = BASE64_LOOKUP.get(char);
251
+ if (digit === undefined) {
252
+ throw new Error(`Invalid base64 VLQ character in source map mappings: ${JSON.stringify(char)}`);
253
+ }
254
+ const continuation = digit & VLQ_CONTINUATION_BIT;
255
+ const bits = digit & VLQ_BASE_MASK;
256
+ result += bits << shift;
257
+ if (continuation) {
258
+ shift += VLQ_BASE_SHIFT;
259
+ } else {
260
+ const negate = result & 1;
261
+ result >>= 1;
262
+ values.push(negate ? -result : result);
263
+ result = 0;
264
+ shift = 0;
265
+ }
266
+ }
267
+
268
+ return values;
269
+ }
270
+
271
+ /** Decodes a full source-map-v3 `mappings` string into per-generated-position entries with sources/names deltas resolved. */
272
+ function decodeMappings(mappings: string): MappingEntry[] {
273
+ const entries: MappingEntry[] = [];
274
+ const lines = mappings.split(';');
275
+
276
+ let sourceIndex = 0;
277
+ let sourceLine = 0;
278
+ let sourceColumn = 0;
279
+ let nameIndex = 0;
280
+
281
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
282
+ const lineText = lines[lineIndex];
283
+ if (lineText === '') continue;
284
+
285
+ let generatedColumn = 0;
286
+ for (const segment of lineText.split(',')) {
287
+ if (segment === '') continue;
288
+ const fields = decodeVLQSegment(segment);
289
+ generatedColumn += fields[0];
290
+
291
+ const entry: MappingEntry = { generatedLine: lineIndex, generatedColumn };
292
+ if (fields.length > 1) {
293
+ sourceIndex += fields[1];
294
+ sourceLine += fields[2];
295
+ sourceColumn += fields[3];
296
+ entry.sourceIndex = sourceIndex;
297
+ entry.sourceLine = sourceLine;
298
+ entry.sourceColumn = sourceColumn;
299
+ if (fields.length > 4) {
300
+ nameIndex += fields[4];
301
+ entry.nameIndex = nameIndex;
302
+ }
303
+ }
304
+ entries.push(entry);
305
+ }
306
+ }
307
+
308
+ return entries;
309
+ }
@@ -2,7 +2,14 @@ import { CDPClient } from './client.js';
2
2
  import { detectCdpPortsAsync } from './detect.js';
3
3
  import { type CDPTarget } from './types.js';
4
4
 
5
- async function getBrowserClient(
5
+ /**
6
+ * Connects to the BROWSER-level CDP websocket (not a page/tab target).
7
+ * Browser-scoped domains (`Browser.grantPermissions`, `ServiceWorker.*`,
8
+ * `Target.*`) only take effect on this connection — and only for as long as
9
+ * it stays open, which is why `capture cdp --browser` routes through the
10
+ * session bridge (`src/cdp/bridge/`) instead of opening a fresh one per call.
11
+ */
12
+ export async function getBrowserClient(
6
13
  port: number,
7
14
  ): Promise<{ client: CDPClient; browserWsUrl: string }> {
8
15
  const versionResp = await fetch(`http://localhost:${port}/json/version`);
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Shared timing helpers for `measure`/`motion`. See the design's "Recorder timing model":
3
+ * the authoritative clock for every reported timestamp is `performance.now()`, relative to
4
+ * the recorder-armed marker (`t=0`). Screencast frames carry a wall-clock `metadata.timestamp`
5
+ * and Tracing events carry a monotonic `ts` in their own domains; the recorder bridge (U13/U14)
6
+ * converts both into the `performance.now()` domain by baseline-offset subtraction using the
7
+ * baseline this module reads.
8
+ */
9
+
10
+ import type { CDPClient } from './client.js';
11
+
12
+ export interface BracketedTiming<T> {
13
+ result: T;
14
+ /** `performance.now()` immediately before `fn` ran. */
15
+ startPerformanceNow: number;
16
+ /** `performance.now()` immediately after `fn` resolved. */
17
+ endPerformanceNow: number;
18
+ }
19
+
20
+ /**
21
+ * Runs `fn`, bracketed by two `performance.now()` reads taken in the page's document execution
22
+ * context. Used to attribute a discrete action (e.g. an `Input.dispatch*` call) to a window in
23
+ * the recorder's authoritative `performance.now()` clock domain — this is the mechanism behind
24
+ * the recorder-client adapter's internal `performance.mark(input_name)` bracketing (U14).
25
+ */
26
+ export async function withDocumentPerformanceNow<T>(
27
+ client: CDPClient,
28
+ fn: () => Promise<T>,
29
+ ): Promise<BracketedTiming<T>> {
30
+ const startPerformanceNow = await readPerformanceNow(client);
31
+ const result = await fn();
32
+ const endPerformanceNow = await readPerformanceNow(client);
33
+ return { result, startPerformanceNow, endPerformanceNow };
34
+ }
35
+
36
+ async function readPerformanceNow(client: CDPClient): Promise<number> {
37
+ const evaluation = (await client.send('Runtime.evaluate', {
38
+ expression: 'performance.now()',
39
+ returnByValue: true,
40
+ })) as { result: { value?: unknown }; exceptionDetails?: unknown };
41
+
42
+ if (evaluation.exceptionDetails) {
43
+ throw new Error(`Failed to read performance.now(): ${JSON.stringify(evaluation.exceptionDetails)}`);
44
+ }
45
+ if (typeof evaluation.result.value !== 'number') {
46
+ throw new Error('performance.now() did not return a number');
47
+ }
48
+ return evaluation.result.value;
49
+ }
50
+
51
+ export interface TraceClockBaseline {
52
+ /** `performance.now()` (document-relative ms) at the moment the baseline was captured. */
53
+ performanceNowMs: number;
54
+ /** `Date.now()` (wall-clock epoch ms) at the same moment. */
55
+ wallClockMs: number;
56
+ }
57
+
58
+ /**
59
+ * Reads one synchronized (`performance.now()`, wall-clock) pair from the page in a single
60
+ * round trip. The recorder bridge captures this once at `--start` as the `capture-rec-start`
61
+ * baseline stored in `markers.json`; screencast-frame `metadata.timestamp` (wall-clock seconds)
62
+ * and Tracing `ts` (wall-clock-anchored microseconds) are then converted into the
63
+ * `performance.now()` domain by subtracting this baseline's wall-clock offset.
64
+ */
65
+ export async function readTraceClockBaseline(client: CDPClient): Promise<TraceClockBaseline> {
66
+ const evaluation = (await client.send('Runtime.evaluate', {
67
+ expression: '({ performanceNowMs: performance.now(), wallClockMs: Date.now() })',
68
+ returnByValue: true,
69
+ })) as {
70
+ result: { value?: { performanceNowMs?: unknown; wallClockMs?: unknown } };
71
+ exceptionDetails?: unknown;
72
+ };
73
+
74
+ if (evaluation.exceptionDetails) {
75
+ throw new Error(`Failed to read trace clock baseline: ${JSON.stringify(evaluation.exceptionDetails)}`);
76
+ }
77
+ const value = evaluation.result.value;
78
+ if (!value || typeof value.performanceNowMs !== 'number' || typeof value.wallClockMs !== 'number') {
79
+ throw new Error('Trace clock baseline evaluation did not return the expected {performanceNowMs, wallClockMs} shape');
80
+ }
81
+ return { performanceNowMs: value.performanceNowMs, wallClockMs: value.wallClockMs };
82
+ }
package/src/cdp/types.ts CHANGED
@@ -34,4 +34,75 @@ export interface ParsedArgs {
34
34
  filterStatus?: string;
35
35
  filterMethod?: string;
36
36
  limit?: number;
37
+ browser?: boolean;
38
+ params?: string;
39
+ waitEvent?: string;
40
+ timeoutMs?: number;
41
+ socket?: string;
42
+
43
+ // -- measure/motion branch flags (settled in the capture measure+motion
44
+ // build plan's U02) -- every leaf under `capture measure`/`capture motion`
45
+ // parses these from the shared global parser instead of re-parsing argv
46
+ // itself.
47
+ /** `snap --freeze-animations` — pause CSS/WAAPI animation before capture. */
48
+ freezeAnimations?: boolean;
49
+ /** `snap --settle-timeout <ms>` — override the default 5000ms settle wait. */
50
+ settleTimeout?: number;
51
+ /** `snap --capture-unsettled` — write full substrate despite non-settlement. */
52
+ captureUnsettled?: boolean;
53
+ /** `snap|diff|mask --pixels` — include raster crop/diff facts. */
54
+ pixels?: boolean;
55
+ /** `snap --state <state[:selector]>` — repeatable; one entry per occurrence. */
56
+ state?: string[];
57
+ /** `check --for <list>` — comma-separated check names or categories. */
58
+ for?: string;
59
+ /** `diff --before <snap>` */
60
+ before?: string;
61
+ /** `diff --after <snap>` */
62
+ after?: string;
63
+ /** `diff --full` — expand to the complete state-matrix/per-element diff. */
64
+ full?: boolean;
65
+ /** `check|diff --gate` — exit 2 on findings/changes instead of 0. */
66
+ gate?: boolean;
67
+ /** `census --snap <id>` — repeatable; one entry per occurrence. */
68
+ snap?: string[];
69
+ /** `census --url <url>` — repeatable accumulator alongside the existing
70
+ * single-value `url` field above (which stays last-wins for every
71
+ * existing command's target-matching semantics). */
72
+ urls?: string[];
73
+ /** `census --set-file <path>` */
74
+ setFile?: string;
75
+ /** `census|sweep --axis <axis>` */
76
+ axis?: string;
77
+ /** `sweep --from <val>` — raw string; axis units vary (px/dpr/zoom). */
78
+ from?: string;
79
+ /** `sweep --to <val>` — raw string; axis units vary (px/dpr/zoom). */
80
+ to?: string;
81
+ /** `sweep --viewport-height <val>` — raw string, distinct from `--height`. */
82
+ viewportHeight?: string;
83
+ /** `motion rec --stop --rec-id <id>` — explicit recording id override. */
84
+ recId?: string;
85
+ /** `check|snap|census --viewport WxH` — repeatable accumulator alongside
86
+ * the existing single-value `viewport` field above (last-wins, unchanged). */
87
+ viewports?: string[];
88
+ /** `explain|map focus|... --selector <sel>` */
89
+ selector?: string;
90
+ /** `explain --size` — include size/layout provenance detail. */
91
+ size?: boolean;
92
+ /** `explain --text` — include text/line-box detail. */
93
+ text?: boolean;
94
+ /** `explain --form` — include form/caret/autofill detail. */
95
+ form?: boolean;
96
+ /** `motion rec --start` — arm the composed recorder. */
97
+ start?: boolean;
98
+ /** `motion rec --stop` — finalize the composed recorder. */
99
+ stop?: boolean;
100
+ /** `motion rec --do <action>` — one-shot scripted action. */
101
+ do?: string;
102
+ /** `motion timeline --element <sel>` */
103
+ element?: string;
104
+ /** `motion timeline --prop <name>` */
105
+ prop?: string;
106
+ /** `motion response --action <action>` */
107
+ action?: string;
37
108
  }
package/src/cdp/usage.ts CHANGED
@@ -21,6 +21,9 @@ Commands:
21
21
  lib search "<q>" Fuzzy-search lib functions [--limit N]
22
22
  lib show <name> Lib + function summaries
23
23
  lib read <name> [fn…] Full input/output schemas (+ .ts source path)
24
+ cdp <Domain.method> Send a raw CDP command capture doesn't wrap [--browser] [--params json] [--wait-event name]
25
+ measure <leaf> Enriched snapshot substrate + read-only queries — see \`capture measure --help\`
26
+ motion <leaf> Recorder lifecycle + read-only queries over a recording — see \`capture motion --help\`
24
27
 
25
28
  Options:
26
29
  --port <port> Override CDP port
@@ -37,6 +40,10 @@ Options:
37
40
  --filter-status <s> Filter HAR by status: code, prefix (4), or range (400-499)
38
41
  --filter-method <m> Filter HAR by HTTP method (har read)
39
42
  --limit <N> Return only first N matching HAR entries (har read)
43
+ --browser Target the browser-level connection (cdp)
44
+ --params '<json>' JSON params for the method (cdp)
45
+ --wait-event <name> Wait for a CDP event and return it (cdp)
46
+ --timeout <ms> Wait timeout, default 10000 (cdp)
40
47
  --out <path> Output path (screenshot)
41
48
  --viewport <preset> Viewport preset: desktop-wide|desktop|tablet|mobile (default: desktop)
42
49
  --height <px> Override viewport height (screenshot)
@@ -67,4 +74,6 @@ Examples:
67
74
  capture lib search "search emails"
68
75
  capture lib read gmail searchEmails
69
76
  capture exec "import {searchEmails} from 'libs/gmail'; const ctx = await getContext(); return await searchEmails({ ...ctx, query: 'invoice' })"
77
+ capture cdp Browser.grantPermissions --browser --params '{"origin":"http://localhost:3000","permissions":["notifications"]}'
78
+ capture cdp --help Raw CDP escape hatch (Browser.*, ServiceWorker.*, ...) + how session --hold keeps grants alive
70
79
  `;