cssprobe-cli 0.2.26 → 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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes are documented on the [GitHub Releases](https://github.com/mack-peng/cssprobe-cli/releases) page.
4
4
 
5
+ ## [0.3.0] - 2026-09-16
6
+
7
+ ### Added
8
+ - MCP server (`cssprobe-cli mcp`, stdio): 10 tools — open / inspect / tree / layout / findings / eval / screenshot / inject_css / close / status
9
+ - `mcp-install` / `mcp-uninstall`: auto-configure MCP clients (Claude Code, Cursor, Codex CLI, opencode, Gemini CLI, Hermes)
10
+ - `mcpName` (`io.github.mack-peng/cssprobe-cli`) for the official MCP Registry
11
+ - README MCP section; `llms.txt` MCP pointer
12
+
13
+ ### Changed
14
+ - Session commands (open/close/status/inspect/…) now share one implementation between CLI and MCP (`src/cli/session-ops.ts`); behavior unchanged
15
+
5
16
  ## [0.2.26] - 2026-09-16
6
17
 
7
18
  ### Added
package/README.md CHANGED
@@ -119,6 +119,30 @@ cssprobe-cli inspect body --json
119
119
  cssprobe-cli inspect body --json | jq '.findings[] | {id, confidence, message}'
120
120
  ```
121
121
 
122
+ ### 4. Use as an MCP Server (AI clients)
123
+
124
+ cssprobe-cli ships an MCP server (stdio), so AI clients call it as a native tool — no shell needed:
125
+
126
+ ```bash
127
+ cssprobe-cli mcp-install # auto-detect and configure installed clients
128
+ cssprobe-cli mcp-install --target=claude,cursor,codex
129
+ cssprobe-cli mcp-install --local # write project-scoped config
130
+ ```
131
+
132
+ Manual configuration for any MCP client:
133
+
134
+ ```json
135
+ {
136
+ "mcpServers": {
137
+ "cssprobe-cli": { "command": "cssprobe-cli", "args": ["mcp"] }
138
+ }
139
+ }
140
+ ```
141
+
142
+ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprobe_layout`,
143
+ `cssprobe_findings`, `cssprobe_eval`, `cssprobe_screenshot`, `cssprobe_inject_css`,
144
+ `cssprobe_close`, `cssprobe_status`. Remove with `cssprobe-cli mcp-uninstall`.
145
+
122
146
  ---
123
147
 
124
148
  ## Commands
@@ -158,10 +182,24 @@ cssprobe-cli inspect body --json | jq '.findings[] | {id, confidence, message}'
158
182
  | Command | Description |
159
183
  |---------|-------------|
160
184
  | `resize <width> <height>` | Resize browser viewport |
161
- | `eval <expression>` | Evaluate JavaScript (browser context) |
185
+ | `eval <expression>` | Evaluate JavaScript (browser context); DOM nodes are auto-serialized |
162
186
  | `playwright <call>` | Execute Playwright API (Node.js context) |
163
187
  | `screenshot` | Take screenshot (saves to ~/.cssprobe-cli/screenshots/, --out to override) |
164
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
+
165
203
  ### State
166
204
 
167
205
  | Command | Description |
@@ -182,6 +220,51 @@ cssprobe-cli inspect body --json | jq '.findings[] | {id, confidence, message}'
182
220
 
183
221
  ---
184
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
+
185
268
  ## Confidence Model
186
269
 
187
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',
@@ -247,6 +316,30 @@ const skillUninstall = (0, command_1.declareCommand)({
247
316
  path: z.string().optional().describe('custom base directory'),
248
317
  }),
249
318
  });
319
+ // ── mcp ──
320
+ const mcp = (0, command_1.declareCommand)({
321
+ name: 'mcp',
322
+ category: 'core',
323
+ description: 'Start MCP server (stdio transport) for AI clients',
324
+ });
325
+ const mcpInstall = (0, command_1.declareCommand)({
326
+ name: 'mcp-install',
327
+ category: 'core',
328
+ description: 'Install MCP server config into AI client configs',
329
+ options: z.object({
330
+ target: z.string().optional().describe('client target: auto, all, none, claude, cursor, codex, opencode, hermes, gemini (default: auto)'),
331
+ local: z.boolean().optional().describe('write project-local config instead of global'),
332
+ }),
333
+ });
334
+ const mcpUninstall = (0, command_1.declareCommand)({
335
+ name: 'mcp-uninstall',
336
+ category: 'core',
337
+ description: 'Remove MCP server config from AI client configs',
338
+ options: z.object({
339
+ target: z.string().optional().describe('client target: auto, all, none, claude, cursor, codex, opencode, hermes, gemini (default: auto)'),
340
+ local: z.boolean().optional().describe('remove project-local config instead of global'),
341
+ }),
342
+ });
250
343
  // ── export ──
251
344
  const commandsArray = [
252
345
  // session management
@@ -255,12 +348,16 @@ const commandsArray = [
255
348
  inspect, tree, layout, findings, injectCss,
256
349
  // browser
257
350
  resize, evalCmd, playwrightCmd, screenshot,
351
+ // interaction
352
+ click, hover, typeCmd, waitForCmd, runCmd,
258
353
  // state
259
354
  stateImport, stateSave,
260
355
  // config
261
356
  configShow, configSet, configList, configUse, configNew, configPath,
262
357
  // skill
263
358
  skillInstall, skillUninstall,
359
+ // mcp
360
+ mcp, mcpInstall, mcpUninstall,
264
361
  ];
265
362
  exports.commands = Object.fromEntries(commandsArray.map(cmd => [cmd.name, cmd]));
266
363
  exports.inspectCommand = inspect;