cssprobe-cli 0.3.0 → 0.3.1

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/AGENTS.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  `cssprobe-cli` — Runtime CSS probe for layout/scroll/overflow/color/font inspection with session-based browser management. Entrypoint: `bin/cssprobe-cli.js` → `require('../dist/index')`.
6
6
  Build: `npm run build` (= `tsc` + `esbuild` collector IIFE bundle + `esbuild` daemon entry + `tsx scripts/generate-help.ts`).
7
- Test: `npm test` (= `tsx --test tests/*.test.ts` — Node.js built-in test runner, 58 tests).
7
+ Test: `npm test` (= `tsx --test tests/*.test.ts` — Node.js built-in test runner, 108 tests).
8
8
  Dependencies: `playwright-core`, `zod`. Dev: `typescript`, `@types/node`, `tsx`, `esbuild`.
9
9
 
10
10
  ## Architecture
@@ -21,19 +21,23 @@ src/
21
21
  ├── daemon/
22
22
  │ ├── session.ts # Session management (single session, Unix socket)
23
23
  │ ├── daemon.ts # Daemon process: browser lifecycle + command execution
24
+ │ ├── flowRunner.ts # Flow script loader/runner (run command): dynamic import, API injection, console capture
24
25
  │ └── daemonEntry.ts # Daemon entry point (spawned as child process)
25
26
  ├── engine/
26
27
  │ ├── types.ts # Shared types: Snapshot, TreeNode, Finding, Confidence
27
28
  │ ├── collector.ts # Browser-side: DOM traversal, computed styles, declared values
28
29
  │ ├── analyzer.ts # Node-side: pure functions, confidence model, pattern detection
30
+ │ ├── evalSerializer.ts # Self-contained serializer for eval results (runs in-page via JSHandle.evaluate)
29
31
  │ └── renderer.ts # Markdown / JSON rendering
30
32
  ├── browser/
31
- │ └── launcher.ts # Playwright wrapper: launch, navigate, inject collector
33
+ │ ├── launcher.ts # Playwright wrapper: launch, navigate, inject collector
34
+ │ └── interactions.ts # Interaction primitives: click/hover/type/wait-for (shared by daemon + run API)
32
35
  ├── config/
33
36
  │ ├── config.ts # Config loader (CLI → env → ~/.cssprobe-clirc profiles)
34
37
  │ └── helpGenerator.ts # Build-time: Zod schemas → dist/help.json
35
38
  └── utils/
36
- └── socketConnection.ts # Socket communication (adapted from playwright-cli)
39
+ ├── socketConnection.ts # Socket communication (adapted from playwright-cli)
40
+ └── screenshot.ts # Screenshot data-URL → file saving (shared by CLI + flow runner)
37
41
  ```
38
42
 
39
43
  ## Key Patterns
@@ -73,11 +77,32 @@ src/
73
77
  - `resize <width> <height>` — Resize browser viewport
74
78
  - `eval <expression>` — Evaluate JavaScript (browser context)
75
79
  - Returns raw string values (not JSON-quoted) for string results
80
+ - DOM nodes / circular structures are auto-serialized (descriptors, `[Circular]`, `[Function: name]`, depth cap 6)
76
81
  - `playwright <call>` — Execute Playwright API call (Node.js context)
77
82
  - Returns `{ ok: true, value: null, note: ... }` when the call returns no value; throws a clear error message on failure
78
83
  - `screenshot` — Take a screenshot
79
84
  - `--out <path>` — Output file path (default: `~/.cssprobe-cli/screenshots/<timestamp>.png`)
80
85
 
86
+ ### Interaction
87
+ - `click <selector>` — Real mouse click (auto-scroll + actionability wait, works with custom React components)
88
+ - `--nth <n>` — 1-based match index (default: first match; multiple matches without `--nth` fail — strict)
89
+ - `--force` — skip actionability checks
90
+ - `--timeout <ms>` — action timeout (default 30000)
91
+ - `hover <selector>` — Real mouse hover (`--nth` / `--force` / `--timeout` as above)
92
+ - `type <selector> <text>` — Type text
93
+ - Default: `fill()` (replaces the value); `--sequential` for real key events (custom editors), `--clear` clears first
94
+ - `--press <key>` — press a key after typing (e.g. `Enter`)
95
+ - `--delay <ms>` — keystroke delay for `--sequential`
96
+ - `wait-for` — Wait for a condition (three mutually exclusive modes)
97
+ - `wait-for <selector>` — selector state: `--state visible|hidden|attached|detached` (default visible)
98
+ - `wait-for --js "<predicate>"` — wait until a JS predicate is truthy
99
+ - `wait-for --network-idle` — wait until the page is network-idle
100
+ - `--timeout <ms>` — default 30000; timeout errors name the target
101
+ - `run <file>` — Run a multi-step flow script inside the daemon
102
+ - Script default-exports an async function receiving the API object: `page`, `click`, `hover`, `type`, `waitFor`, `inspect`, `screenshot`, `log` (CJS also supported)
103
+ - `--timeout <ms>` — whole-script guard (default 300000, 0 = unlimited); failure exits 1 and keeps the session alive
104
+ - `screenshot` relative paths resolve against the script's directory; logs collected and returned
105
+
81
106
  ### State
82
107
  - `state-import [file]` — Import cookies from Netscape format
83
108
  - `--out <file>` — Output state file path (default: `~/.cssprobe-cli/states/imported.json`)
@@ -151,6 +176,6 @@ cssprobe-cli close
151
176
  | Output | Single markdown string | Markdown (default) / JSON (--json) |
152
177
  | Config | `window.__DOM_REPORT_CFG` global + eval two-step | CLI flags → env → rc file, one command |
153
178
  | ROOT_SELECTOR | Manual or eval probe | `--auto-root` auto-detection |
154
- | Tests | None | 45 unit tests (command, analyzer, output, renderer) |
179
+ | Tests | None | 45+ unit tests (command, analyzer, output, renderer) |
155
180
  | Build | Regex stripping CJS module.exports | esbuild IIFE + CJS bundle, clean build |
156
181
  | Session | None (blocking commands) | Session-based daemon architecture |
package/README.md CHANGED
@@ -182,10 +182,24 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
182
182
  | Command | Description |
183
183
  |---------|-------------|
184
184
  | `resize <width> <height>` | Resize browser viewport |
185
- | `eval <expression>` | Evaluate JavaScript (browser context) |
185
+ | `eval <expression>` | Evaluate JavaScript (browser context); DOM nodes are auto-serialized |
186
186
  | `playwright <call>` | Execute Playwright API (Node.js context) |
187
187
  | `screenshot` | Take screenshot (saves to ~/.cssprobe-cli/screenshots/, --out to override) |
188
188
 
189
+ ### Interaction
190
+
191
+ | Command | Description |
192
+ |---------|-------------|
193
+ | `click <selector>` | Real mouse click (auto-scroll + actionability wait; works with custom React components) |
194
+ | `hover <selector>` | Real mouse hover |
195
+ | `type <selector> <text>` | Fill text (replaces value); `--press Enter` to submit; `--sequential` for real key events |
196
+ | `wait-for <selector>` | Wait for selector state (`--state visible\|hidden\|attached\|detached`, `--timeout ms`) |
197
+ | `wait-for --js "<predicate>"` | Wait until a JS predicate is truthy |
198
+ | `wait-for --network-idle` | Wait until the page is network-idle |
199
+ | `run <file>` | Run a multi-step flow script |
200
+
201
+ `click` / `hover` support `--nth N` (1-based, default first match), `--force` (skip actionability checks) and `--timeout ms`. Without `--nth`, multiple matches fail (strict) to prevent wrong-target clicks.
202
+
189
203
  ### State
190
204
 
191
205
  | Command | Description |
@@ -206,6 +220,51 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
206
220
 
207
221
  ---
208
222
 
223
+ ## Multi-step Flows
224
+
225
+ The interaction commands drive a page step by step; `run` turns a sequence into a versionable script:
226
+
227
+ ```bash
228
+ cssprobe-cli open https://example.com/editor
229
+ cssprobe-cli wait-for .editor-canvas
230
+ cssprobe-cli click ".layout-switch" --nth 1
231
+ cssprobe-cli type ".block-title input" "Hello" --press Enter
232
+ cssprobe-cli inspect .block-grid --json
233
+ cssprobe-cli close
234
+ ```
235
+
236
+ For repeatable flows, write a script and run it with one command. The script runs inside the daemon session (full Playwright `page` access):
237
+
238
+ ```js
239
+ // flow.js
240
+ export default async ({ page, click, waitFor, type, inspect, screenshot, log }) => {
241
+ await page.goto('https://example.com/editor', { waitUntil: 'domcontentloaded' });
242
+ await waitFor('.editor-canvas', { state: 'visible', timeout: 10000 });
243
+
244
+ await click('.layout-switch', { nth: 1 });
245
+ await waitFor({ js: "() => document.body.dataset.layout === 'two-col'" });
246
+
247
+ await type('.block-title input', 'Hello', { press: 'Enter', sequential: true, clear: true });
248
+ await waitFor({ networkIdle: true });
249
+
250
+ const { findings } = await inspect('.block-grid', { format: 'json' });
251
+ log('findings:', findings.length);
252
+ await screenshot({ path: './after-layout-switch.png' });
253
+
254
+ return { findings: findings.length };
255
+ };
256
+ ```
257
+
258
+ ```bash
259
+ cssprobe-cli run ./flow.js # --timeout ms for the whole script (default 300000, 0 = unlimited)
260
+ ```
261
+
262
+ Scripts must `default-export` an async function receiving the API object: `page`, `click`, `hover`, `type`, `waitFor`, `inspect`, `screenshot`, `log`. `screenshot` relative paths resolve against the script's directory; logs (`log()` and `console.*`) are collected and returned with the result. On failure the command exits `1`, prints the collected logs, and keeps the session alive.
263
+
264
+ > Scripts are executed by the local daemon with full Node privileges — only run trusted scripts.
265
+
266
+ ---
267
+
209
268
  ## Confidence Model
210
269
 
211
270
  Every finding carries a confidence level:
@@ -0,0 +1,48 @@
1
+ import type { Page } from 'playwright-core';
2
+ export interface ElementDescriptor {
3
+ tag: string;
4
+ classes: string[];
5
+ text: string;
6
+ }
7
+ export interface TargetResult {
8
+ matches: number;
9
+ nth: number;
10
+ descriptor: ElementDescriptor | null;
11
+ }
12
+ export type WaitState = 'visible' | 'hidden' | 'attached' | 'detached';
13
+ export type WaitTarget = {
14
+ kind: 'selector';
15
+ selector: string;
16
+ state: WaitState;
17
+ } | {
18
+ kind: 'js';
19
+ expression: string;
20
+ } | {
21
+ kind: 'networkIdle';
22
+ };
23
+ export declare function clickElement(page: Page, selector: string, opts?: {
24
+ nth?: number;
25
+ force?: boolean;
26
+ timeout?: number;
27
+ }): Promise<TargetResult>;
28
+ export declare function hoverElement(page: Page, selector: string, opts?: {
29
+ nth?: number;
30
+ force?: boolean;
31
+ timeout?: number;
32
+ }): Promise<TargetResult>;
33
+ export interface TypeOptions {
34
+ press?: string;
35
+ sequential?: boolean;
36
+ clear?: boolean;
37
+ delay?: number;
38
+ nth?: number;
39
+ timeout?: number;
40
+ }
41
+ export declare function typeInto(page: Page, selector: string, text: string, opts?: TypeOptions): Promise<void>;
42
+ export declare function describeWaitTarget(target: WaitTarget): string;
43
+ export declare function resolveWaitTarget(selector: string | undefined, opts: {
44
+ js?: string;
45
+ networkIdle?: boolean;
46
+ state?: string;
47
+ }): WaitTarget;
48
+ export declare function waitForCondition(page: Page, target: WaitTarget, timeout: number): Promise<number>;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ // Interaction primitives shared by daemon commands and the run-script API.
3
+ // Strict mode is preserved when no nth is given: multiple matches fail the action.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.clickElement = clickElement;
6
+ exports.hoverElement = hoverElement;
7
+ exports.typeInto = typeInto;
8
+ exports.describeWaitTarget = describeWaitTarget;
9
+ exports.resolveWaitTarget = resolveWaitTarget;
10
+ exports.waitForCondition = waitForCondition;
11
+ const DEFAULT_TIMEOUT = 30_000;
12
+ function resolveLocator(page, selector, nth) {
13
+ const base = page.locator(selector);
14
+ return nth ? base.nth(nth - 1) : base;
15
+ }
16
+ async function describeTarget(locator) {
17
+ return await locator
18
+ .evaluate(el => ({
19
+ tag: el.tagName.toLowerCase(),
20
+ classes: Array.from(el.classList),
21
+ text: (el.textContent ?? '').trim().slice(0, 60),
22
+ }))
23
+ .catch(() => null);
24
+ }
25
+ async function interactTarget(page, selector, opts, action) {
26
+ const base = page.locator(selector);
27
+ const locator = resolveLocator(page, selector, opts.nth);
28
+ const matches = await base.count();
29
+ const descriptor = await describeTarget(locator);
30
+ await action(locator, { force: opts.force, timeout: opts.timeout ?? DEFAULT_TIMEOUT });
31
+ return { matches, nth: opts.nth ?? 1, descriptor };
32
+ }
33
+ async function clickElement(page, selector, opts = {}) {
34
+ return interactTarget(page, selector, opts, (locator, o) => locator.click({ force: o.force, timeout: o.timeout }));
35
+ }
36
+ async function hoverElement(page, selector, opts = {}) {
37
+ return interactTarget(page, selector, opts, (locator, o) => locator.hover({ force: o.force, timeout: o.timeout }));
38
+ }
39
+ async function typeInto(page, selector, text, opts = {}) {
40
+ const locator = resolveLocator(page, selector, opts.nth);
41
+ const timeout = opts.timeout ?? DEFAULT_TIMEOUT;
42
+ if (opts.sequential) {
43
+ if (opts.clear)
44
+ await locator.fill('', { timeout });
45
+ await locator.pressSequentially(text, { delay: opts.delay ?? 0, timeout });
46
+ }
47
+ else {
48
+ await locator.fill(text, { timeout });
49
+ }
50
+ if (opts.press)
51
+ await locator.press(opts.press, { timeout });
52
+ }
53
+ function describeWaitTarget(target) {
54
+ switch (target.kind) {
55
+ case 'selector':
56
+ return `${target.selector} (state: ${target.state})`;
57
+ case 'js':
58
+ return `JS predicate: ${target.expression.slice(0, 80)}`;
59
+ case 'networkIdle':
60
+ return 'network idle';
61
+ }
62
+ }
63
+ function resolveWaitTarget(selector, opts) {
64
+ const modeCount = [selector, opts.js, opts.networkIdle].filter(Boolean).length;
65
+ if (modeCount !== 1)
66
+ throw new Error('wait-for requires exactly one of: <selector>, --js "<expression>", --network-idle');
67
+ if (selector)
68
+ return { kind: 'selector', selector, state: (opts.state ?? 'visible') };
69
+ if (opts.js)
70
+ return { kind: 'js', expression: opts.js };
71
+ return { kind: 'networkIdle' };
72
+ }
73
+ async function waitForCondition(page, target, timeout) {
74
+ const started = Date.now();
75
+ switch (target.kind) {
76
+ case 'selector':
77
+ await page.waitForSelector(target.selector, { state: target.state, timeout });
78
+ break;
79
+ case 'js':
80
+ await page.waitForFunction(target.expression, null, { timeout });
81
+ break;
82
+ case 'networkIdle':
83
+ await page.waitForLoadState('networkidle', { timeout });
84
+ break;
85
+ }
86
+ return Date.now() - started;
87
+ }
88
+ //# sourceMappingURL=interactions.js.map
@@ -157,6 +157,75 @@ const playwrightCmd = (0, command_1.declareCommand)({
157
157
  call: z.string().describe('Playwright API call, e.g. "page.setViewportSize({width: 1280, height: 720})"'),
158
158
  }),
159
159
  });
160
+ // ── interaction ──
161
+ const click = (0, command_1.declareCommand)({
162
+ name: 'click',
163
+ category: 'core',
164
+ description: 'Click an element with real mouse events (works with custom React components)',
165
+ args: z.object({
166
+ selector: z.string().describe('CSS selector'),
167
+ }),
168
+ options: z.object({
169
+ nth: numberArg.optional().describe('1-based match index (default: first match)'),
170
+ force: z.boolean().optional().describe('skip actionability checks'),
171
+ timeout: numberArg.optional().describe('action timeout in ms (default 30000)'),
172
+ }),
173
+ });
174
+ const hover = (0, command_1.declareCommand)({
175
+ name: 'hover',
176
+ category: 'core',
177
+ description: 'Hover an element with real mouse events',
178
+ args: z.object({
179
+ selector: z.string().describe('CSS selector'),
180
+ }),
181
+ options: z.object({
182
+ nth: numberArg.optional().describe('1-based match index (default: first match)'),
183
+ force: z.boolean().optional().describe('skip actionability checks'),
184
+ timeout: numberArg.optional().describe('action timeout in ms (default 30000)'),
185
+ }),
186
+ });
187
+ const typeCmd = (0, command_1.declareCommand)({
188
+ name: 'type',
189
+ category: 'core',
190
+ description: 'Type text into an element (fill by default; --sequential for real key events)',
191
+ args: z.object({
192
+ selector: z.string().describe('CSS selector'),
193
+ text: z.string().describe('text to type'),
194
+ }),
195
+ options: z.object({
196
+ press: z.string().optional().describe('key to press after typing (e.g. Enter, Tab, Control+A)'),
197
+ sequential: z.boolean().optional().describe('type with real key events (for custom editors)'),
198
+ clear: z.boolean().optional().describe('clear existing content before sequential typing'),
199
+ delay: numberArg.optional().describe('delay between keystrokes in ms (--sequential, default 0)'),
200
+ nth: numberArg.optional().describe('1-based match index (default: first match)'),
201
+ timeout: numberArg.optional().describe('action timeout in ms (default 30000)'),
202
+ }),
203
+ });
204
+ const waitForCmd = (0, command_1.declareCommand)({
205
+ name: 'wait-for',
206
+ category: 'core',
207
+ description: 'Wait for a condition: selector state, JS predicate, or network idle',
208
+ args: z.object({
209
+ selector: z.string().optional().describe('CSS selector to wait for'),
210
+ }),
211
+ options: z.object({
212
+ state: z.enum(['visible', 'hidden', 'attached', 'detached']).optional().describe('selector state to wait for (default visible)'),
213
+ js: z.string().optional().describe('JS predicate, e.g. "() => document.querySelector(\'.x\')?.offsetHeight > 100"'),
214
+ 'network-idle': z.boolean().optional().describe('wait until the page is network-idle'),
215
+ timeout: numberArg.optional().describe('timeout in ms (default 30000)'),
216
+ }),
217
+ });
218
+ const runCmd = (0, command_1.declareCommand)({
219
+ name: 'run',
220
+ category: 'core',
221
+ description: 'Run a multi-step flow script (default-exports an async function receiving the API object)',
222
+ args: z.object({
223
+ file: z.string().describe('flow script path (.js / .mjs / .cjs)'),
224
+ }),
225
+ options: z.object({
226
+ timeout: numberArg.optional().describe('whole-script timeout in ms (default 300000, 0 = unlimited)'),
227
+ }),
228
+ });
160
229
  // ── state management ──
161
230
  const stateImport = (0, command_1.declareCommand)({
162
231
  name: 'state-import',
@@ -279,6 +348,8 @@ const commandsArray = [
279
348
  inspect, tree, layout, findings, injectCss,
280
349
  // browser
281
350
  resize, evalCmd, playwrightCmd, screenshot,
351
+ // interaction
352
+ click, hover, typeCmd, waitForCmd, runCmd,
282
353
  // state
283
354
  stateImport, stateSave,
284
355
  // config
@@ -70,8 +70,15 @@ async function program() {
70
70
  case 'eval':
71
71
  case 'resize':
72
72
  case 'playwright':
73
+ case 'click':
74
+ case 'hover':
75
+ case 'type':
76
+ case 'wait-for':
73
77
  await handleSessionCommand(commandName, command, rawArgs, output);
74
78
  return;
79
+ case 'run':
80
+ await handleRun(command, rawArgs, output);
81
+ return;
75
82
  }
76
83
  output.error(`Unknown command: ${commandName}. Run 'cssprobe-cli --help' for usage.`);
77
84
  }
@@ -197,6 +204,16 @@ async function handleStatus(output) {
197
204
  output.error(e instanceof Error ? e.message : String(e));
198
205
  }
199
206
  }
207
+ async function handleRun(command, args, output) {
208
+ const fileArg = args._[1];
209
+ if (!fileArg)
210
+ output.error('Usage: cssprobe-cli run <file>');
211
+ const file = path_1.default.resolve(fileArg);
212
+ if (!fs_1.default.existsSync(file))
213
+ output.error(`Flow script not found: ${file}`);
214
+ args._ = ['run', file];
215
+ await handleSessionCommand('run', command, args, output);
216
+ }
200
217
  async function handleSessionCommand(commandName, command, args, output) {
201
218
  try {
202
219
  const cmdArgs = splitArgs(args);
@@ -214,6 +231,8 @@ async function handleSessionCommand(commandName, command, args, output) {
214
231
  return;
215
232
  }
216
233
  console.log(result.text);
234
+ if (typeof result.exitCode === 'number' && result.exitCode !== 0)
235
+ process.exit(result.exitCode);
217
236
  }
218
237
  catch (e) {
219
238
  output.error(e instanceof Error ? e.message : String(e));
@@ -1,6 +1,7 @@
1
1
  import { Session } from '../daemon/session';
2
2
  import type { SessionConfig } from '../daemon/session';
3
3
  import type { AnyCommandSchema } from './command';
4
+ export { saveScreenshotFile } from '../utils/screenshot';
4
5
  export interface OpenOptions {
5
6
  url?: string;
6
7
  browser?: string;
@@ -39,5 +40,5 @@ export declare function getSessionStatus(): Promise<StatusResult>;
39
40
  export declare function buildDaemonArgs(commandName: string, command: AnyCommandSchema, parsed: Record<string, unknown>): string[];
40
41
  export declare function runSessionCommand(commandName: string, command: AnyCommandSchema, parsed: Record<string, unknown>, json?: boolean): Promise<{
41
42
  text: string;
43
+ exitCode?: number;
42
44
  }>;
43
- export declare function saveScreenshotFile(dataUrl: string, out?: string): string;
@@ -1,49 +1,15 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.saveScreenshotFile = void 0;
36
4
  exports.openSession = openSession;
37
5
  exports.waitForSession = waitForSession;
38
6
  exports.closeSession = closeSession;
39
7
  exports.getSessionStatus = getSessionStatus;
40
8
  exports.buildDaemonArgs = buildDaemonArgs;
41
9
  exports.runSessionCommand = runSessionCommand;
42
- exports.saveScreenshotFile = saveScreenshotFile;
43
- const fs = __importStar(require("fs"));
44
- const os = __importStar(require("os"));
45
- const path = __importStar(require("path"));
46
10
  const session_1 = require("../daemon/session");
11
+ var screenshot_1 = require("../utils/screenshot");
12
+ Object.defineProperty(exports, "saveScreenshotFile", { enumerable: true, get: function () { return screenshot_1.saveScreenshotFile; } });
47
13
  async function openSession(opts) {
48
14
  const url = opts.url || 'about:blank';
49
15
  const browser = opts.browser || 'chromium';
@@ -142,22 +108,4 @@ async function runSessionCommand(commandName, command, parsed, json = false) {
142
108
  runArgs.json = true;
143
109
  return await session.run(runArgs);
144
110
  }
145
- function saveScreenshotFile(dataUrl, out) {
146
- const base64 = dataUrl.replace(/^data:image\/png;base64,/, '');
147
- const buffer = Buffer.from(base64, 'base64');
148
- const resolvedOut = out
149
- ? path.resolve(out)
150
- : (() => {
151
- const dir = path.join(os.homedir(), '.cssprobe-cli', 'screenshots');
152
- if (!fs.existsSync(dir))
153
- fs.mkdirSync(dir, { recursive: true });
154
- const ts = new Date().toISOString().replace(/[:.]/g, '-');
155
- return path.join(dir, `screenshot-${ts}.png`);
156
- })();
157
- const dir = path.dirname(resolvedOut);
158
- if (!fs.existsSync(dir))
159
- fs.mkdirSync(dir, { recursive: true });
160
- fs.writeFileSync(resolvedOut, buffer);
161
- return resolvedOut;
162
- }
163
111
  //# sourceMappingURL=session-ops.js.map
@@ -17,6 +17,9 @@ const launcher_1 = require("../browser/launcher");
17
17
  const analyzer_1 = require("../engine/analyzer");
18
18
  const renderer_1 = require("../engine/renderer");
19
19
  const dom_tree_1 = require("../engine/renderers/dom-tree");
20
+ const interactions_1 = require("../browser/interactions");
21
+ const evalSerializer_1 = require("../engine/evalSerializer");
22
+ const flowRunner_1 = require("./flowRunner");
20
23
  const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
21
24
  async function startDaemon(sessionName, clientInfo, options) {
22
25
  const sessionConfig = createSessionConfig(clientInfo, sessionName, options);
@@ -173,12 +176,18 @@ async function executeCommand(launcher, args) {
173
176
  const page = launcher.getPage();
174
177
  if (!page)
175
178
  throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
176
- const result = await page.evaluate(expression);
177
- if (typeof result === 'string')
178
- return { text: result };
179
- if (result === undefined)
180
- return { text: 'undefined' };
181
- return { text: JSON.stringify(result, null, 2) };
179
+ const handle = await page.evaluateHandle(expression);
180
+ try {
181
+ const result = await handle.evaluate(evalSerializer_1.serializeEvalResult);
182
+ if (typeof result === 'string')
183
+ return { text: result };
184
+ if (result === undefined)
185
+ return { text: 'undefined' };
186
+ return { text: JSON.stringify(result, null, 2) };
187
+ }
188
+ finally {
189
+ await handle.dispose();
190
+ }
182
191
  }
183
192
  case 'resize': {
184
193
  const width = parseInt(params[0], 10);
@@ -206,6 +215,79 @@ async function executeCommand(launcher, args) {
206
215
  return { text: JSON.stringify({ ok: true, value: null, note: 'call returned no value' }) };
207
216
  return { text: JSON.stringify(result, null, 2) };
208
217
  }
218
+ case 'click':
219
+ case 'hover': {
220
+ const selector = params[0];
221
+ if (!selector)
222
+ throw new Error(`Selector is required for ${command} command`);
223
+ const page = requirePage(launcher);
224
+ const opts = { nth: args.nth, force: args.force, timeout: args.timeout };
225
+ const result = command === 'click'
226
+ ? await (0, interactions_1.clickElement)(page, selector, opts)
227
+ : await (0, interactions_1.hoverElement)(page, selector, opts);
228
+ if (args.json) {
229
+ return { text: JSON.stringify({ ok: true, selector, matches: result.matches, nth: result.nth }, null, 2) };
230
+ }
231
+ return { text: formatTargetAction(command === 'click' ? 'Clicked' : 'Hovered', selector, result) };
232
+ }
233
+ case 'type': {
234
+ const selector = params[0];
235
+ const text = params[1];
236
+ if (!selector || text === undefined)
237
+ throw new Error('Usage: type <selector> <text>');
238
+ await (0, interactions_1.typeInto)(requirePage(launcher), selector, text, {
239
+ press: args.press,
240
+ sequential: args.sequential,
241
+ clear: args.clear,
242
+ delay: args.delay,
243
+ nth: args.nth,
244
+ timeout: args.timeout,
245
+ });
246
+ if (args.json) {
247
+ const payload = { ok: true, selector, length: text.length };
248
+ if (args.press)
249
+ payload.pressed = args.press;
250
+ return { text: JSON.stringify(payload, null, 2) };
251
+ }
252
+ const pressedNote = args.press ? ` then pressed ${args.press}` : '';
253
+ return { text: `Typed ${text.length} chars into ${selector}${pressedNote}` };
254
+ }
255
+ case 'wait-for': {
256
+ // selector is read from the parsed object: option flags are appended to
257
+ // the argv array after positional args, so params[0] is not reliable here.
258
+ const selector = args.selector;
259
+ const timeout = args.timeout ?? 30000;
260
+ const target = (0, interactions_1.resolveWaitTarget)(selector, {
261
+ js: args.js,
262
+ networkIdle: args['network-idle'],
263
+ state: args.state,
264
+ });
265
+ let elapsedMs;
266
+ try {
267
+ elapsedMs = await (0, interactions_1.waitForCondition)(requirePage(launcher), target, timeout);
268
+ }
269
+ catch (e) {
270
+ throw new Error(`wait-for failed for ${(0, interactions_1.describeWaitTarget)(target)} within ${timeout}ms: ${e.message}`);
271
+ }
272
+ if (args.json) {
273
+ const payload = {
274
+ ok: true,
275
+ waitedFor: target.kind === 'networkIdle' ? 'networkidle' : target.kind,
276
+ elapsedMs,
277
+ };
278
+ if (target.kind === 'selector')
279
+ payload.state = target.state;
280
+ return { text: JSON.stringify(payload, null, 2) };
281
+ }
282
+ const label = target.kind === 'selector' ? `${selector} (${target.state})` : (0, interactions_1.describeWaitTarget)(target);
283
+ return { text: `Wait satisfied: ${label} (${elapsedMs}ms)` };
284
+ }
285
+ case 'run': {
286
+ const absPath = params[0];
287
+ if (!absPath)
288
+ throw new Error('Flow script path is required');
289
+ return await (0, flowRunner_1.runFlow)(launcher, absPath, { timeout: args.timeout, json: !!args.json });
290
+ }
209
291
  case 'state-save': {
210
292
  const context = launcher.getContext();
211
293
  if (!context)
@@ -217,6 +299,20 @@ async function executeCommand(launcher, args) {
217
299
  throw new Error(`Unknown command: ${command}`);
218
300
  }
219
301
  }
302
+ function requirePage(launcher) {
303
+ const page = launcher.getPage();
304
+ if (!page)
305
+ throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
306
+ return page;
307
+ }
308
+ function formatTargetAction(verb, selector, result) {
309
+ const descriptor = result.descriptor
310
+ ? ` → ${result.descriptor.tag}${result.descriptor.classes.map(c => `.${c}`).join('')}${result.descriptor.text ? ` "${result.descriptor.text}"` : ''}`
311
+ : '';
312
+ const matchNote = result.matches === 1 ? '1 match' : `${result.matches} matches`;
313
+ const nthNote = result.matches > 1 ? `, used #${result.nth}` : '';
314
+ return `${verb} ${selector}${descriptor} (${matchNote}${nthNote})`;
315
+ }
220
316
  async function socketExists(socketPath) {
221
317
  try {
222
318
  const stat = await fs_1.default.promises.stat(socketPath);
@@ -0,0 +1,62 @@
1
+ import type { Page } from 'playwright-core';
2
+ import type { BrowserLauncher } from '../browser/launcher';
3
+ import { type TargetResult, type TypeOptions, type WaitState } from '../browser/interactions';
4
+ import type { Finding, Snapshot } from '../engine/types';
5
+ export interface FlowEnvelope {
6
+ ok: boolean;
7
+ logs: string[];
8
+ result?: unknown;
9
+ error?: string;
10
+ elapsedMs: number;
11
+ }
12
+ export interface WaitForOptions {
13
+ state?: WaitState;
14
+ timeout?: number;
15
+ }
16
+ export interface WaitOutcome {
17
+ waitedFor: 'selector' | 'js' | 'networkidle';
18
+ state?: WaitState;
19
+ elapsedMs: number;
20
+ }
21
+ export interface FlowApi {
22
+ page: Page;
23
+ click(selector: string, opts?: {
24
+ nth?: number;
25
+ force?: boolean;
26
+ timeout?: number;
27
+ }): Promise<TargetResult>;
28
+ hover(selector: string, opts?: {
29
+ nth?: number;
30
+ force?: boolean;
31
+ timeout?: number;
32
+ }): Promise<TargetResult>;
33
+ type(selector: string, text: string, opts?: TypeOptions): Promise<void>;
34
+ waitFor(target: string | {
35
+ js: string;
36
+ timeout?: number;
37
+ } | {
38
+ networkIdle: true;
39
+ timeout?: number;
40
+ }, opts?: WaitForOptions): Promise<WaitOutcome>;
41
+ inspect(selector: string, opts?: {
42
+ format?: 'json' | 'markdown';
43
+ }): Promise<{
44
+ snapshot: Snapshot;
45
+ findings: Finding[];
46
+ } | string>;
47
+ screenshot(opts?: {
48
+ path?: string;
49
+ fullPage?: boolean;
50
+ }): Promise<string>;
51
+ log(...args: unknown[]): void;
52
+ }
53
+ type FlowFn = (api: FlowApi) => Promise<unknown> | unknown;
54
+ export declare function loadFlowScript(absPath: string): Promise<FlowFn>;
55
+ export declare function runFlow(launcher: BrowserLauncher, absPath: string, opts?: {
56
+ timeout?: number;
57
+ json?: boolean;
58
+ }): Promise<{
59
+ text: string;
60
+ exitCode: number;
61
+ }>;
62
+ export {};
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ // Flow script runner: loads a user script in the daemon process, injects the
3
+ // interaction API, captures console output and returns a result envelope.
4
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5
+ return (mod && mod.__esModule) ? mod : { "default": mod };
6
+ };
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.loadFlowScript = loadFlowScript;
9
+ exports.runFlow = runFlow;
10
+ const path_1 = __importDefault(require("path"));
11
+ const util_1 = __importDefault(require("util"));
12
+ const url_1 = require("url");
13
+ const interactions_1 = require("../browser/interactions");
14
+ const analyzer_1 = require("../engine/analyzer");
15
+ const renderer_1 = require("../engine/renderer");
16
+ const evalSerializer_1 = require("../engine/evalSerializer");
17
+ const screenshot_1 = require("../utils/screenshot");
18
+ const FLOW_DEFAULT_TIMEOUT = 300_000;
19
+ const CAPTURED_CONSOLE_METHODS = ['log', 'info', 'warn', 'error'];
20
+ // tsc compiles `import()` to `require()` under module=commonjs, which cannot
21
+ // load ESM scripts; a Function-constructed import keeps the native loader.
22
+ const nativeImport = new Function('p', 'return import(p)');
23
+ async function loadFlowScript(absPath) {
24
+ const mod = await nativeImport(`${(0, url_1.pathToFileURL)(absPath).href}?v=${Date.now()}`);
25
+ const fn = mod?.default;
26
+ if (typeof fn !== 'function')
27
+ throw new Error(`Flow script must default-export an async function: ${absPath}`);
28
+ return fn;
29
+ }
30
+ function captureConsole(logs) {
31
+ const originals = new Map();
32
+ for (const method of CAPTURED_CONSOLE_METHODS) {
33
+ originals.set(method, console[method]);
34
+ console[method] = (...args) => {
35
+ logs.push(`[${method}] ${util_1.default.format(...args)}`);
36
+ };
37
+ }
38
+ return () => {
39
+ for (const [method, original] of originals) {
40
+ console[method] = original;
41
+ }
42
+ };
43
+ }
44
+ async function withTimeout(promise, timeoutMs) {
45
+ if (!timeoutMs || timeoutMs <= 0)
46
+ return promise;
47
+ let timer;
48
+ try {
49
+ return await Promise.race([
50
+ promise,
51
+ new Promise((_, reject) => {
52
+ timer = setTimeout(() => reject(new Error(`Flow timed out after ${timeoutMs}ms (best-effort abort)`)), timeoutMs);
53
+ }),
54
+ ]);
55
+ }
56
+ finally {
57
+ clearTimeout(timer);
58
+ }
59
+ }
60
+ function requirePage(launcher) {
61
+ const page = launcher.getPage();
62
+ if (!page)
63
+ throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
64
+ return page;
65
+ }
66
+ async function flowWaitFor(page, target, opts) {
67
+ if (typeof target === 'string') {
68
+ const state = opts?.state ?? 'visible';
69
+ const timeout = opts?.timeout ?? 30_000;
70
+ const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'selector', selector: target, state }, timeout);
71
+ return { waitedFor: 'selector', state, elapsedMs };
72
+ }
73
+ if ('js' in target) {
74
+ const timeout = target.timeout ?? 30_000;
75
+ const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'js', expression: target.js }, timeout);
76
+ return { waitedFor: 'js', elapsedMs };
77
+ }
78
+ if (target.networkIdle) {
79
+ const timeout = target.timeout ?? 30_000;
80
+ const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'networkIdle' }, timeout);
81
+ return { waitedFor: 'networkidle', elapsedMs };
82
+ }
83
+ throw new Error('waitFor requires a selector string, { js }, or { networkIdle: true }');
84
+ }
85
+ function buildFlowApi(launcher, logs, baseDir) {
86
+ const page = requirePage(launcher);
87
+ return {
88
+ page,
89
+ click: (selector, opts) => (0, interactions_1.clickElement)(page, selector, opts),
90
+ hover: (selector, opts) => (0, interactions_1.hoverElement)(page, selector, opts),
91
+ type: (selector, text, opts) => (0, interactions_1.typeInto)(page, selector, text, opts),
92
+ waitFor: (target, opts) => flowWaitFor(page, target, opts),
93
+ inspect: async (selector, opts) => {
94
+ const snapshot = await launcher.collect({
95
+ rootSelector: selector,
96
+ downDepth: 6,
97
+ maxNodes: 60,
98
+ upTo: 'html',
99
+ });
100
+ const findings = (0, analyzer_1.analyze)(snapshot);
101
+ if (opts?.format === 'markdown')
102
+ return (0, renderer_1.renderMarkdown)(snapshot, findings);
103
+ return { snapshot, findings };
104
+ },
105
+ screenshot: async (opts) => {
106
+ const buffer = await page.screenshot({ fullPage: opts?.fullPage });
107
+ const out = opts?.path ? path_1.default.resolve(baseDir, opts.path) : undefined;
108
+ return (0, screenshot_1.saveScreenshotFile)(`data:image/png;base64,${buffer.toString('base64')}`, out);
109
+ },
110
+ log: (...args) => {
111
+ logs.push(util_1.default.format(...args));
112
+ },
113
+ };
114
+ }
115
+ function renderEnvelope(env) {
116
+ const lines = [];
117
+ if (env.ok) {
118
+ lines.push(`Flow completed in ${env.elapsedMs}ms`);
119
+ if (env.result !== undefined)
120
+ lines.push(`Result: ${JSON.stringify(env.result, null, 2)}`);
121
+ }
122
+ else {
123
+ lines.push(`Flow failed in ${env.elapsedMs}ms: ${env.error}`);
124
+ }
125
+ if (env.logs.length) {
126
+ lines.push('', 'Logs:');
127
+ lines.push(...env.logs.map(line => ` ${line}`));
128
+ }
129
+ return lines.join('\n');
130
+ }
131
+ async function runFlow(launcher, absPath, opts = {}) {
132
+ const logs = [];
133
+ const started = Date.now();
134
+ const restore = captureConsole(logs);
135
+ let env;
136
+ try {
137
+ const fn = await loadFlowScript(absPath);
138
+ const api = buildFlowApi(launcher, logs, path_1.default.dirname(absPath));
139
+ const result = await withTimeout(Promise.resolve(fn(api)), opts.timeout ?? FLOW_DEFAULT_TIMEOUT);
140
+ env = { ok: true, logs, elapsedMs: Date.now() - started };
141
+ if (result !== undefined)
142
+ env.result = (0, evalSerializer_1.serializeEvalResult)(result);
143
+ }
144
+ catch (e) {
145
+ env = { ok: false, logs, error: e.message, elapsedMs: Date.now() - started };
146
+ }
147
+ finally {
148
+ restore();
149
+ }
150
+ return {
151
+ text: opts.json ? JSON.stringify(env, null, 2) : renderEnvelope(env),
152
+ exitCode: env.ok ? 0 : 1,
153
+ };
154
+ }
155
+ //# sourceMappingURL=flowRunner.js.map
@@ -0,0 +1 @@
1
+ export declare function serializeEvalResult(value: unknown): unknown;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ // Self-contained serializer for eval results.
3
+ // Runs inside the page via JSHandle.evaluate (function source is transferred),
4
+ // and is unit-testable in Node — keep it free of external references.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.serializeEvalResult = serializeEvalResult;
7
+ function serializeEvalResult(value) {
8
+ const seen = new WeakSet();
9
+ const walk = (v, depth) => {
10
+ if (v === null || typeof v !== 'object') {
11
+ if (typeof v === 'function')
12
+ return `[Function: ${v.name || 'anonymous'}]`;
13
+ if (typeof v === 'bigint')
14
+ return `${v}n`;
15
+ return v;
16
+ }
17
+ if (typeof Node !== 'undefined' && v instanceof Node) {
18
+ if (v.nodeType === 1) {
19
+ const el = v;
20
+ return {
21
+ tag: el.tagName.toLowerCase(),
22
+ id: el.id,
23
+ classes: Array.from(el.classList),
24
+ text: (el.textContent ?? '').trim().slice(0, 120),
25
+ };
26
+ }
27
+ return `[${v.nodeName}]`;
28
+ }
29
+ if (seen.has(v))
30
+ return '[Circular]';
31
+ if (depth >= 6)
32
+ return '[Depth limit]';
33
+ seen.add(v);
34
+ if (Array.isArray(v))
35
+ return v.map(item => walk(item, depth + 1));
36
+ const out = {};
37
+ for (const [key, val] of Object.entries(v))
38
+ out[key] = walk(val, depth + 1);
39
+ return out;
40
+ };
41
+ return walk(value, 0);
42
+ }
43
+ //# sourceMappingURL=evalSerializer.js.map
package/dist/help.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "global": "Usage: cssprobe-cli <command> [args] [options]\n\n\nSession Management:\n open [url] open browser in session mode (non-blocking)\n close close the browser session\n status show current session status\n resize <width> <height> resize the browser viewport\n eval <expression> evaluate javascript expression on the page (browser context)\n playwright <call> execute playwright api call (node.js context)\n state-import [file] import cookies from netscape format file\n state-save save current browser session state (cookies + localstorage)\n mcp start mcp server (stdio transport) for ai clients\n mcp-install install mcp server config into ai client configs\n mcp-uninstall remove mcp server config from ai client configs\n\nCSS Inspection:\n inspect <selector> inspect the runtime css of a page element\n tree <selector> show dom tree structure\n layout <selector> show ascii layout diagram\n findings <selector> show only issues/warnings/errors\n inject-css <css> inject css into the current page\n\nExport:\n screenshot take a screenshot of the current page\n\nConfiguration:\n config-show show current configuration\n config-set <key> <value> set a configuration value\n config-list list all profiles\n config-use <name> switch active profile\n config-new <name> create a new profile\n config-path show configuration file path\n\nAgent Skills:\n skill-install install agent skill for cssprobe-cli\n skill-uninstall remove agent skill for cssprobe-cli\n\nGlobal options:\n --help [command] print help\n --json output response as JSON\n --raw output only the result value\n --version print version\n -p, --profile <name> use named profile (or CSSPROBE_PROFILE env)",
2
+ "global": "Usage: cssprobe-cli <command> [args] [options]\n\n\nSession Management:\n open [url] open browser in session mode (non-blocking)\n close close the browser session\n status show current session status\n resize <width> <height> resize the browser viewport\n eval <expression> evaluate javascript expression on the page (browser context)\n playwright <call> execute playwright api call (node.js context)\n click <selector> click an element with real mouse events (works with custom react components)\n hover <selector> hover an element with real mouse events\n type <selector> <text> type text into an element (fill by default; --sequential for real key events)\n wait-for [selector] wait for a condition: selector state, js predicate, or network idle\n run <file> run a multi-step flow script (default-exports an async function receiving the api object)\n state-import [file] import cookies from netscape format file\n state-save save current browser session state (cookies + localstorage)\n mcp start mcp server (stdio transport) for ai clients\n mcp-install install mcp server config into ai client configs\n mcp-uninstall remove mcp server config from ai client configs\n\nCSS Inspection:\n inspect <selector> inspect the runtime css of a page element\n tree <selector> show dom tree structure\n layout <selector> show ascii layout diagram\n findings <selector> show only issues/warnings/errors\n inject-css <css> inject css into the current page\n\nExport:\n screenshot take a screenshot of the current page\n\nConfiguration:\n config-show show current configuration\n config-set <key> <value> set a configuration value\n config-list list all profiles\n config-use <name> switch active profile\n config-new <name> create a new profile\n config-path show configuration file path\n\nAgent Skills:\n skill-install install agent skill for cssprobe-cli\n skill-uninstall remove agent skill for cssprobe-cli\n\nGlobal options:\n --help [command] print help\n --json output response as JSON\n --raw output only the result value\n --version print version\n -p, --profile <name> use named profile (or CSSPROBE_PROFILE env)",
3
3
  "commands": {
4
4
  "open": {
5
5
  "help": "cssprobe-cli open [url]\n\nOpen browser in session mode (non-blocking)\n\nArguments:\n [url] url, file:// path, or local html file path\nOptions:\n --browser browser engine (default chromium)\n --headed show the browser window\n --viewport viewport size as wxh (e.g. 1280x720)\n --state path to saved state file (cookies + localstorage)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
@@ -97,6 +97,64 @@
97
97
  },
98
98
  "args": []
99
99
  },
100
+ "click": {
101
+ "help": "cssprobe-cli click <selector>\n\nClick an element with real mouse events (works with custom React components)\n\nArguments:\n <selector> css selector\nOptions:\n --nth 1-based match index (default: first match)\n --force skip actionability checks\n --timeout action timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
102
+ "flags": {
103
+ "nth": "string",
104
+ "force": "boolean",
105
+ "timeout": "string"
106
+ },
107
+ "args": [
108
+ "selector"
109
+ ]
110
+ },
111
+ "hover": {
112
+ "help": "cssprobe-cli hover <selector>\n\nHover an element with real mouse events\n\nArguments:\n <selector> css selector\nOptions:\n --nth 1-based match index (default: first match)\n --force skip actionability checks\n --timeout action timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
113
+ "flags": {
114
+ "nth": "string",
115
+ "force": "boolean",
116
+ "timeout": "string"
117
+ },
118
+ "args": [
119
+ "selector"
120
+ ]
121
+ },
122
+ "type": {
123
+ "help": "cssprobe-cli type <selector> <text>\n\nType text into an element (fill by default; --sequential for real key events)\n\nArguments:\n <selector> css selector\n <text> text to type\nOptions:\n --press key to press after typing (e.g. enter, tab, control+a)\n --sequential type with real key events (for custom editors)\n --clear clear existing content before sequential typing\n --delay delay between keystrokes in ms (--sequential, default 0)\n --nth 1-based match index (default: first match)\n --timeout action timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
124
+ "flags": {
125
+ "press": "string",
126
+ "sequential": "boolean",
127
+ "clear": "boolean",
128
+ "delay": "string",
129
+ "nth": "string",
130
+ "timeout": "string"
131
+ },
132
+ "args": [
133
+ "selector",
134
+ "text"
135
+ ]
136
+ },
137
+ "wait-for": {
138
+ "help": "cssprobe-cli wait-for [selector]\n\nWait for a condition: selector state, JS predicate, or network idle\n\nArguments:\n [selector] css selector to wait for\nOptions:\n --state selector state to wait for (default visible)\n --js js predicate, e.g. \"() => document.queryselector('.x')?.offsetheight > 100\"\n --network-idle wait until the page is network-idle\n --timeout timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
139
+ "flags": {
140
+ "state": "string",
141
+ "js": "string",
142
+ "network-idle": "boolean",
143
+ "timeout": "string"
144
+ },
145
+ "args": [
146
+ "selector"
147
+ ]
148
+ },
149
+ "run": {
150
+ "help": "cssprobe-cli run <file>\n\nRun a multi-step flow script (default-exports an async function receiving the API object)\n\nArguments:\n <file> flow script path (.js / .mjs / .cjs)\nOptions:\n --timeout whole-script timeout in ms (default 300000, 0 = unlimited)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
151
+ "flags": {
152
+ "timeout": "string"
153
+ },
154
+ "args": [
155
+ "file"
156
+ ]
157
+ },
100
158
  "state-import": {
101
159
  "help": "cssprobe-cli state-import [file]\n\nImport cookies from Netscape format file\n\nArguments:\n [file] netscape cookie file path (reads from stdin if omitted)\nOptions:\n --out output state file path (default: ~/.cssprobe-cli/states/imported.json)\n --name custom name for the state file (e.g. mysite or mysite.json)\n --merge existing state file to merge into\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
102
160
  "flags": {
@@ -203,6 +261,10 @@
203
261
  "brief",
204
262
  "layout",
205
263
  "full-page",
264
+ "force",
265
+ "sequential",
266
+ "clear",
267
+ "network-idle",
206
268
  "local"
207
269
  ]
208
270
  }
@@ -127,6 +127,49 @@ function buildSkillMd() {
127
127
  'cssprobe-cli close # stop browser + daemon',
128
128
  BT3,
129
129
  '',
130
+ '## Multi-step UI Flows',
131
+ '',
132
+ 'Beyond inspection, cssprobe-cli drives the page: click, hover, type, wait, and',
133
+ 'run scripted flows -- all in the same session.',
134
+ '',
135
+ BT3 + 'bash',
136
+ 'cssprobe-cli click ".layout-switch" # real mouse click (auto-scroll + actionability wait)',
137
+ 'cssprobe-cli click ".item" --nth 2 # 1-based match index; multi-match without --nth fails (strict)',
138
+ 'cssprobe-cli hover ".menu-item"',
139
+ 'cssprobe-cli type ".block-title input" "Hi" --press Enter # fill by default',
140
+ 'cssprobe-cli type ".editor" "text" --sequential --clear # real key events for custom editors',
141
+ 'cssprobe-cli wait-for ".modal" --state visible --timeout 5000',
142
+ 'cssprobe-cli wait-for --js "() => ready === true"',
143
+ 'cssprobe-cli wait-for --network-idle',
144
+ BT3,
145
+ '',
146
+ '### Scripted flows: ' + BT + 'run <file>' + BT,
147
+ '',
148
+ 'A flow script default-exports an async function receiving the API object and',
149
+ 'runs inside the daemon session:',
150
+ '',
151
+ BT3 + 'js',
152
+ 'export default async ({ page, click, hover, type, waitFor, inspect, screenshot, log }) => {',
153
+ ' await waitFor(".editor-canvas");',
154
+ ' await click(".layout-switch", { nth: 1 });',
155
+ ' await type(".block-title input", "Hello", { press: "Enter", sequential: true, clear: true });',
156
+ ' await waitFor({ networkIdle: true });',
157
+ ' const { findings } = await inspect(".block-grid", { format: "json" });',
158
+ ' await screenshot({ path: "./after.png" });',
159
+ ' return { findings: findings.length };',
160
+ '};',
161
+ BT3,
162
+ '',
163
+ BT3 + 'bash',
164
+ 'cssprobe-cli run ./flow.js # --timeout ms (default 300000, 0 = unlimited)',
165
+ BT3,
166
+ '',
167
+ 'Notes:',
168
+ '',
169
+ '- Scripts run on your machine with full Node privileges -- only run trusted scripts.',
170
+ '- ' + BT + 'screenshot' + BT + ' relative paths resolve against the script directory.',
171
+ '- On failure the command exits 1, prints collected logs, and keeps the session alive.',
172
+ '',
130
173
  '## Output Modes',
131
174
  '',
132
175
  'Pass ' + BT + '--json' + BT + ' when consuming output programmatically:',
@@ -0,0 +1 @@
1
+ export declare function saveScreenshotFile(dataUrl: string, out?: string): string;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.saveScreenshotFile = saveScreenshotFile;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ function saveScreenshotFile(dataUrl, out) {
41
+ const base64 = dataUrl.replace(/^data:image\/png;base64,/, '');
42
+ const buffer = Buffer.from(base64, 'base64');
43
+ const resolvedOut = out
44
+ ? path.resolve(out)
45
+ : (() => {
46
+ const dir = path.join(os.homedir(), '.cssprobe-cli', 'screenshots');
47
+ if (!fs.existsSync(dir))
48
+ fs.mkdirSync(dir, { recursive: true });
49
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
50
+ return path.join(dir, `screenshot-${ts}.png`);
51
+ })();
52
+ const dir = path.dirname(resolvedOut);
53
+ if (!fs.existsSync(dir))
54
+ fs.mkdirSync(dir, { recursive: true });
55
+ fs.writeFileSync(resolvedOut, buffer);
56
+ return resolvedOut;
57
+ }
58
+ //# sourceMappingURL=screenshot.js.map
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # cssprobe-cli
2
2
 
3
- > Runtime CSS probe — inspect layout, scroll, colors, backgrounds, fonts in a live browser. Returns DOM tree, ASCII layout diagram and confidence-classified findings for humans and AI agents.
3
+ > Runtime CSS probe — inspect layout, scroll, colors, backgrounds, fonts in a live browser. Returns DOM tree, ASCII layout diagram and confidence-classified findings for humans and AI agents. Interaction commands (click / hover / type / wait-for) drive the page; `run <file>` executes scripted multi-step flows.
4
4
 
5
5
  ## Documentation
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cssprobe-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "mcpName": "io.github.mack-peng/cssprobe-cli",
5
5
  "description": "Runtime CSS probe — inspect layout, colors, backgrounds, fonts and more in a live browser. AI agents and shell automation.",
6
6
  "keywords": [