rpc-agent 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,99 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ Ship-check: `rpc check <url>` audits a page before launch, from the CLI, in
6
+ CI or through MCP.
7
+
8
+ ### Added
9
+
10
+ - `rpc check <url>` runs on a phone and a desktop width by default (`-d`
11
+ to change) and reports findings with an id, severity (`error`, `warn`,
12
+ `info`), message, selector or URL, devices and a fix. Checks:
13
+ - Basics: HTTP status, mixed content, uncaught exceptions, console errors,
14
+ `theme-color`.
15
+ - Meta and social: title, description, canonical, `lang`, viewport (and
16
+ zoom blocking), favicon, apple-touch-icon, `og:title`,
17
+ `og:description`, `og:image`, `og:url`, `twitter:card`. `og:image` is
18
+ fetched for its status, content type and pixel size.
19
+ - Layout shift: CLS (largest session window) after load and one scroll,
20
+ with the elements that moved. Warn above 0.1, error above 0.25.
21
+ - Unlabelled controls: buttons, links and `[role=button]` with no
22
+ accessible name, images without `alt`, form fields without a label.
23
+ - Fonts: missing or `block` `font-display`, above-the-fold fonts that
24
+ aren't preloaded, preloads without `crossorigin`, unused preloads,
25
+ font files that fail.
26
+ - Mobile viewport (phones): `100vh`, sideways scroll with the elements
27
+ that cause it, tap targets under 44x44, text fields under 16px.
28
+ - Exit code 1 on errors; `--fail-on warn` fails on warnings too.
29
+ `--json` prints a versioned report (`schema: 1`, documented in the
30
+ README). `--shots` saves PNGs of the elements behind layout-shift and
31
+ overflow findings.
32
+ - Auth works as for `rpc shot`: `--cookie`, `--header`,
33
+ `--storage-state`, `--allow-origin`, saved `rpc login` sessions and
34
+ `AUTH_REQUIRED` on a redirect to sign-in.
35
+ - MCP `check` tool with the same options and the `screenshot` tool's auth
36
+ params. It returns a short text summary and the JSON report.
37
+
38
+ ### Unchanged
39
+
40
+ - `rpc shot`, its output and the MCP `screenshot` tool. The MCP tool list
41
+ gains `check`.
42
+
43
+ ## 0.4.0
44
+
45
+ Authenticated captures: screenshot logged-in pages without a hand-written
46
+ "sign in, then screenshot" script.
47
+
48
+ ### Added
49
+
50
+ - `rpc login <url>` opens a browser window; sign in, press Enter (or close
51
+ the window) and the storage state is saved for that origin to
52
+ `~/.config/rpc-agent/sessions/<origin>.json` (file 0600, directory 0700).
53
+ `--save <file>` writes it elsewhere.
54
+ - `rpc login <url> --form`: headless email + password sign-in for CI and
55
+ agents, from `RPC_LOGIN_EMAIL` / `RPC_LOGIN_PASSWORD` (or `--email` /
56
+ `--password`), with `--email-selector`, `--password-selector` and
57
+ `--submit-selector`.
58
+ - `rpc shot` uses the saved session for the target origin automatically.
59
+ `--no-session` opts out. `rpc sessions [list]` and
60
+ `rpc sessions clear [origin]` manage them.
61
+ - `rpc shot --cookie "name=value[; Domain=…; Path=…]"`, `--header
62
+ "Name: value"` (both repeatable), `--storage-state <file>` and
63
+ `--allow-origin <origin>`.
64
+ - A capture redirected to a login page (`/login`, `/sign-in`, `/auth`, …)
65
+ fails with code `AUTH_REQUIRED` and the hint ``Session missing or
66
+ expired: run `rpc login <origin>` `` instead of screenshotting the sign-in
67
+ form. Sessions whose cookies have all expired aren't loaded.
68
+ - MCP `screenshot` takes `cookies`, `headers`, `storageState`, `useSession`
69
+ and `allowOrigins`. New `list_sessions` tool.
70
+ - `rpc install --headed` also downloads full Chromium, for the
71
+ `rpc login` window. Without it, `rpc login` uses an installed Google
72
+ Chrome if there is one.
73
+
74
+ ### Security
75
+
76
+ - Headers and cookies go to the target origin only (plus any
77
+ `--allow-origin`). Headers are injected per request through the CDP Fetch
78
+ domain, so a redirect to another host doesn't carry them. Saved sessions
79
+ and storage-state files load only the target origin's cookies and
80
+ localStorage.
81
+ - Cookie, header and localStorage values are never printed: output shows
82
+ names with `•••`, and console errors and failure messages are scrubbed.
83
+ - `--password` prints a warning; use the environment variable.
84
+
85
+ ### Changed
86
+
87
+ - A page that redirects to a login URL now fails that device with
88
+ `AUTH_REQUIRED` (exit code 1) even when no auth was given. In 0.3 it
89
+ screenshotted the sign-in page and reported success.
90
+
91
+ ### Unchanged
92
+
93
+ - With no auth in play, plain output, `--json` output and MCP results are
94
+ the 0.3 ones. With auth, they gain an `auth:` line / an `auth` field.
95
+ Existing MCP params and result fields are unchanged.
96
+
3
97
  ## 0.3.1
4
98
 
5
99
  Published to npm as `rpc-agent`.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # rpc-agent
2
2
 
3
+ <img src="https://raw.githubusercontent.com/akintola4/rpc-agent/main/docs/media/banner.png" width="100%" alt="rpc-agent: CLI + MCP server · device screenshots for AI agents · Claude Code, Codex, Cursor. A line drawing of a laptop, tablet and phone with a terminal in front running rpc shot; the Claude Code and Cursor marks and a terminal-prompt glyph for Codex sit beside it, with lines into the prompt.">
4
+
3
5
  <picture>
4
6
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/akintola4/rpc-agent/main/docs/media/hero-dark.png">
5
7
  <img alt="A site captured by rpc on an iPhone 15, an iPad Air 11 and a 1440 px desktop, each framed in its device bezel" src="https://raw.githubusercontent.com/akintola4/rpc-agent/main/docs/media/hero-light.png">
@@ -35,7 +37,8 @@ Chromium is not downloaded when the package installs. `rpc install` (or
35
37
  `npx rpc-agent install`) fetches it with rpc's own Playwright version, so
36
38
  the browser always matches the one rpc launches. It installs only
37
39
  Chromium's headless shell (plus Playwright's small ffmpeg), not the full
38
- Chrome for Testing build, since rpc never opens a visible window. Until you run it, every
40
+ Chrome for Testing build, since captures never open a visible window
41
+ (`rpc install --headed` adds it for `rpc login`). Until you run it, every
39
42
  command says Chromium is missing and prints that command, instead of
40
43
  failing with Playwright's own error. Browsers go to Playwright's usual
41
44
  cache, so other Playwright projects on the same version share them.
@@ -60,9 +63,12 @@ these docs say `rpc`, or `pnpm rpc` from inside the repo.
60
63
  ```sh
61
64
  rpc # interactive: pick a URL, devices and options
62
65
  rpc shot <url> [options]
66
+ rpc check <url> [options] # pre-launch audit, see Ship-check below
67
+ rpc login <url> [--form] # sign in once; later shots of that origin use the session
68
+ rpc sessions [list|clear [origin]]
63
69
  rpc devices [--json]
64
70
  rpc doctor [--json] # is Chromium installed, and does it launch?
65
- rpc install # download Chromium with rpc's own Playwright version
71
+ rpc install [--headed] # download Chromium with rpc's own Playwright version
66
72
  rpc mcp
67
73
  rpc help <command> # options and examples for one command
68
74
  ```
@@ -96,6 +102,11 @@ the files it wrote:
96
102
  | `--no-emulate` | Viewport and DPR only: no mobile user agent or touch |
97
103
  | `--json` | Print the result as JSON |
98
104
  | `--open` | Open the output folder when done (`open`, `xdg-open` or `explorer`) |
105
+ | `--cookie <c>` | `name=value[; Domain=…; Path=…]`, repeatable. See [Authenticated pages](#authenticated-pages) |
106
+ | `--header <h>` | `Name: value`, repeatable, sent to the target origin only |
107
+ | `--storage-state <file>` | Load a Playwright storageState JSON |
108
+ | `--no-session` | Don't use the session saved by `rpc login` |
109
+ | `--allow-origin <o>` | Also send headers and loaded cookies to this origin, repeatable |
99
110
 
100
111
  The URL can be a full URL, a bare local host (`localhost:3000` becomes
101
112
  `http://`), a bare domain (`example.com` becomes `https://`) or a path to a
@@ -168,9 +179,352 @@ listening on port N"), proxy, tunnel and offline errors, TLS errors (https
168
179
  against a plain-http dev server), missing files, redirect loops, navigation
169
180
  and `--wait-for` timeouts.
170
181
 
182
+ ## Authenticated pages
183
+
184
+ Dashboards, billing and settings pages sit behind a login. rpc can carry a
185
+ session so you don't need a hand-written "sign in, then screenshot" script.
186
+
187
+ ### Sign in once with `rpc login`
188
+
189
+ ```sh
190
+ rpc login localhost:3000/sign-in
191
+ rpc shot localhost:3000/app/settings/billing -d iphone-15,desktop-1440 --bezels
192
+ ```
193
+
194
+ `rpc login` opens a browser window at the URL. Sign in as you normally
195
+ would (SSO and 2FA work, it's a real browser), then press Enter in the
196
+ terminal or close the window. rpc saves the browser's storage state
197
+ (cookies and localStorage) to
198
+ `~/.config/rpc-agent/sessions/<origin>.json`, for example
199
+ `http-localhost-3000.json`. From then on, `rpc shot` on that origin uses it
200
+ automatically, and the output says so:
201
+
202
+ ```
203
+ auth: session http://localhost:3000 (saved 2026-10-29 22:55)
204
+ ok iphone-15 (393x852 @3x)
205
+ ```
206
+
207
+ The window needs full Chromium, which `rpc install` doesn't download (it
208
+ fetches only the headless shell). Run `rpc install --headed` once, or have
209
+ Google Chrome installed; rpc falls back to it.
210
+
211
+ `--save <file>` writes the storage state to a file of your choice instead;
212
+ use it with `rpc shot --storage-state <file>`.
213
+
214
+ ```sh
215
+ rpc sessions # origins, cookie counts, saved and expiry times
216
+ rpc sessions clear localhost:3000 # forget one origin
217
+ rpc sessions clear # forget all
218
+ rpc shot localhost:3000/app --no-session # capture as a signed-out visitor
219
+ ```
220
+
221
+ ### When the session is missing or expired
222
+
223
+ If the page redirects to a login URL (a path segment like `/login`,
224
+ `/sign-in`, `/signin` or `/auth`, different from the one you asked for),
225
+ rpc doesn't screenshot the sign-in form. The device fails with code
226
+ `AUTH_REQUIRED`:
227
+
228
+ ```
229
+ x iphone-15 (393x852 @3x): Redirected to a login page (http://localhost:3000/sign-in) instead of http://localhost:3000/app/settings/billing
230
+ hint: Session missing or expired: run `rpc login http://localhost:3000`
231
+ ```
232
+
233
+ A saved session whose cookies have all passed their expiry isn't loaded,
234
+ and gives the same hint.
235
+
236
+ ### CI and agents: `rpc login --form`
237
+
238
+ Without a person at the keyboard, `--form` signs in headlessly through a
239
+ standard email + password form:
240
+
241
+ ```sh
242
+ export RPC_LOGIN_EMAIL=ci@example.test
243
+ export RPC_LOGIN_PASSWORD=... # from your CI secret store
244
+ rpc login https://staging.example.com/sign-in --form
245
+ rpc shot https://staging.example.com/app/billing --json
246
+ ```
247
+
248
+ It loads the page, waits for it to settle (so a client-side form has
249
+ hydrated), fills the first match of `--email-selector` (default
250
+ `input[type=email]`, then `autocomplete=username`, `name=email`,
251
+ `name=username`, `#email`) and `--password-selector` (default
252
+ `input[type=password]`), clicks `--submit-selector` (default
253
+ `button[type=submit], input[type=submit]`, or presses Enter if there is no
254
+ button), and waits for the URL to leave the login page. If it doesn't
255
+ within `--timeout`, it fails and quotes any alert the page shows ("Invalid
256
+ email or password"). `--email` works as a flag too; see the security notes
257
+ before using `--password`.
258
+
259
+ ### Cookies and headers
260
+
261
+ ```sh
262
+ rpc shot localhost:3000/app --cookie "session=$SESSION"
263
+ rpc shot app.example.com/admin --cookie "sid=$SID; Domain=example.com; Path=/"
264
+ rpc shot api.example.com/docs --header "Authorization: Bearer $TOKEN"
265
+ rpc shot localhost:3000/app --storage-state auth.json
266
+ ```
267
+
268
+ - `--cookie` is host-only on the target unless you give a `Domain`, which
269
+ must match the target host (or an `--allow-origin` host). `Secure`,
270
+ `HttpOnly` and `SameSite` are accepted too.
271
+ - `--header` is sent only on requests to the target origin. Images,
272
+ scripts and API calls to other origins don't get it, and neither does a
273
+ redirect to another host (every redirect hop is checked).
274
+ - `--storage-state` and saved sessions load only the target origin's
275
+ cookies and localStorage, even if the file holds others (an SSO domain,
276
+ analytics).
277
+ - `--allow-origin https://api.example.com` widens all of the above to
278
+ another origin, for an app that calls a separate API host.
279
+ - A saved session is used unless `--no-session` or `--storage-state` is
280
+ given. Cookies and headers are added on top of it.
281
+
282
+ ### Security notes
283
+
284
+ - **Values are never printed.** Plain, rich and `--json` output and MCP
285
+ results show cookie and header names with `•••`. Console errors and
286
+ failure messages are scrubbed of every value rpc injected. The `--json`
287
+ result gains an `auth` field, for example
288
+ `{"cookies":[{"name":"sid","value":"•••","domain":"localhost","path":"/"}]}`.
289
+ - **Session files are credentials.** They are written with mode 0600 in a
290
+ 0700 directory (`$XDG_CONFIG_HOME/rpc-agent/sessions` if set;
291
+ `RPC_SESSIONS_DIR` overrides the location). Anyone who can read one can
292
+ act as you on that site until it expires. `rpc sessions clear` deletes
293
+ them. Don't commit a `--save` file.
294
+ - **Keep passwords out of shell history.** Prefer `RPC_LOGIN_PASSWORD` from
295
+ a secret store over `--password`, which lands in your shell history and
296
+ is visible to other users in the process list; rpc warns when you use it.
297
+ The same goes for tokens in `--cookie` and `--header`: pass them from an
298
+ environment variable (`--header "Authorization: Bearer $TOKEN"`), not
299
+ literally.
300
+ - **Use a test account.** Sign in as a user whose data you're happy for
301
+ screenshots (and the agent reading them) to see.
302
+
303
+ ## Ship-check
304
+
305
+ `rpc check <url>` audits one page before you call it done. It loads the page
306
+ on a phone and a desktop width, the way `rpc shot` does, and lists what's
307
+ wrong: missing or broken social tags, layout shift, buttons a screen reader
308
+ can't name, fonts that hide text while they load, and the mobile viewport
309
+ bugs that only show up on a phone.
310
+
311
+ ```sh
312
+ rpc check localhost:3000
313
+ rpc check localhost:3000 --fail-on warn --json # CI
314
+ rpc check example.com/pricing -d pixel,iphone-se,desktop --shots
315
+ ```
316
+
317
+ <img alt="rpc check in a terminal on turbo-start-paystack.vercel.app: both devices load with HTTP 200 and CLS 0.000, then a Meta and social box (no canonical link, no og:url) and a Mobile viewport box (16 of 54 tap targets under 44x44 px, with selectors and sizes), each finding with a fix, and the line 0 errors, 3 warnings, 0 info" src="https://raw.githubusercontent.com/akintola4/rpc-agent/main/docs/media/terminal-check.png" width="720">
318
+
319
+ URLs, devices and auth work as in `rpc shot`: `localhost:3000` needs no
320
+ scheme, `-d` takes ids or aliases (default `iphone-15-pro,desktop-1440`),
321
+ and `--cookie`, `--header`, `--storage-state` and the session saved by
322
+ `rpc login` all apply. A page that redirects to a login URL fails with
323
+ `AUTH_REQUIRED`. Each run checks one URL. It isn't Lighthouse: CLS is the
324
+ only performance number it reports.
325
+
326
+ | Option | Meaning |
327
+ | --- | --- |
328
+ | `-d, --devices <ids>` | Devices to check on. The mobile checks run on phones only |
329
+ | `--fail-on <level>` | `error` (default) exits 1 on errors; `warn` exits 1 on warnings too |
330
+ | `--shots` | Save PNGs of the elements behind layout-shift and overflow findings into `--out` |
331
+ | `-o, --out <dir>` | Where `--shots` writes. Default `./.rpc-shots` |
332
+ | `--json` | Print the report as JSON (see below) |
333
+ | `--wait-for`, `--wait`, `--timeout`, `--color-scheme` | As in `rpc shot` |
334
+ | `--cookie`, `--header`, `--storage-state`, `--no-session`, `--allow-origin` | As in `rpc shot` |
335
+
336
+ The exit code is 0 when there are no errors (with `--fail-on warn`, no
337
+ warnings either), 1 otherwise and 2 for a usage error.
338
+
339
+ ### How a check runs
340
+
341
+ Each device gets a fresh browser context, the same one `rpc shot` uses.
342
+ Before the page's own scripts run, rpc installs a `PerformanceObserver` for
343
+ `layout-shift`. It loads the page and waits for network idle (up to 10 s),
344
+ web fonts and pending images. Then it waits a fixed 500 ms, scrolls down one
345
+ viewport and back with a 300 ms pause each way, and reads the page. Because
346
+ the sequence is fixed, CLS comes out the same on every run; the tests check
347
+ the same page twice and allow 0.005 of difference.
348
+
349
+ When a finding is identical on several devices it's reported once, with
350
+ every device listed, so a missing `lang` appears one time, not two.
351
+
352
+ ### What it checks
353
+
354
+ Severity decides the exit code. `error` fails the run, `warn` fails it only
355
+ with `--fail-on warn`, and `info` never does.
356
+
357
+ #### Basics
358
+
359
+ | id | Severity | When | Fix |
360
+ | --- | --- | --- | --- |
361
+ | `http-status` | error | The page answered 400 or above | Serve it with a 200 |
362
+ | `page-load-failed`, `auth-required` | error | The page didn't load on a device (DNS, refused, timeout) or landed on a login page | The error's own hint, e.g. ``run `rpc login <origin>` `` |
363
+ | `mixed-content` | error (warn if the browser upgraded every request) | `http://` requests on an https page | Load everything over https |
364
+ | `uncaught-exception` | error | Exceptions thrown during load, first three listed | Fix the first one; the rest often follow from it |
365
+ | `console-errors` | warn | `console.error` during load: the count and the first three | Fix or silence them |
366
+ | `theme-color-missing` | info | No `<meta name="theme-color">` | Add one to tint the mobile browser bar |
367
+
368
+ #### Meta and social
369
+
370
+ | id | Severity | When | Fix |
371
+ | --- | --- | --- | --- |
372
+ | `title-missing`, `title-long` | error, warn | No `<title>`, or one over 60 characters | 50 to 60 characters, key words first |
373
+ | `description-missing`, `description-long` | warn, info | No meta description, or one over 160 characters | One or two sentences |
374
+ | `canonical-missing` | warn | No `<link rel="canonical">` | The absolute URL to index |
375
+ | `lang-missing` | error | No `lang` on `<html>` | `<html lang="en">` |
376
+ | `viewport-missing`, `viewport-no-zoom` | error | No viewport meta, or one with `user-scalable=no` or `maximum-scale` under 2 | `width=device-width, initial-scale=1` and nothing that blocks zoom |
377
+ | `favicon-missing` | warn | No `<link rel="icon">`, and `/favicon.ico` doesn't load | Add an icon link |
378
+ | `apple-touch-icon-missing` | info | No `apple-touch-icon` | A 180x180 PNG |
379
+ | `og-title-missing`, `og-description-missing`, `og-image-missing`, `og-url-missing` | warn | That Open Graph tag is absent | Add it; link previews read it |
380
+ | `twitter-card-missing` | warn | No `twitter:card` | `summary_large_image` |
381
+ | `og-image-relative` | warn | `og:image` isn't an absolute URL | Make it absolute; most scrapers don't resolve relative URLs |
382
+ | `og-image-broken`, `og-image-not-image` | error | `og:image` doesn't return 200, or isn't served as an image | Point it at the image file |
383
+ | `og-image-small`, `og-image-ratio` | warn, info | Smaller than 600x315, or not the 1.91:1 shape of 1200x630 | Export it at 1200x630 |
384
+
385
+ rpc fetches `og:image` once per run and reads its pixel size from the file
386
+ header (PNG, JPEG, GIF or WebP). The result is in the report's `ogImage`.
387
+
388
+ #### Layout shift
389
+
390
+ | id | Severity | When | Fix |
391
+ | --- | --- | --- | --- |
392
+ | `layout-shift` | error above 0.25, warn above 0.1, info above 0 | CLS for the load and the scroll, with the three elements that moved most and each one's share | Reserve space for late content: `width`/`height` or `aspect-ratio` on media, `min-height` on async sections, nothing inserted above what's on screen |
393
+
394
+ The score is computed the way Chrome computes it: the largest session
395
+ window, meaning shifts less than 1 s apart and at most 5 s in total. Chrome
396
+ marks shifts that follow input as `hadRecentInput`, including any in the
397
+ first 500 ms after navigation, and those don't count. Totals under 0.001
398
+ count as zero. Each device's score is also in `devices[].cls`.
399
+
400
+ #### Unlabelled controls
401
+
402
+ | id | Severity | When | Fix |
403
+ | --- | --- | --- | --- |
404
+ | `button-no-name`, `link-no-name` | error | A button, link or `[role=button]` with no accessible name, such as an icon-only button | `aria-label`, visually hidden text, or alt text on the image inside |
405
+ | `img-no-alt` | error | An `<img>` with no `alt` attribute (`alt=""` is fine) | Describe the image, or `alt=""` if it's decorative |
406
+ | `field-no-label` | error | An input, select or textarea with no label | `<label for>`, a wrapping `<label>`, or `aria-label` |
407
+ | `field-placeholder-only` | warn | A field labelled only by its placeholder | Add a visible label |
408
+
409
+ rpc works out accessible names in the page from `aria-labelledby`,
410
+ `aria-label`, `<label>`, `alt`, the text inside (minus anything
411
+ `aria-hidden`), an SVG `<title>` and `title`. It skips elements assistive
412
+ tech can't reach: `display: none`, `visibility: hidden`, `aria-hidden` and
413
+ `inert`.
414
+
415
+ #### Fonts
416
+
417
+ | id | Severity | When | Fix |
418
+ | --- | --- | --- | --- |
419
+ | `font-display-missing`, `font-display-block` | warn | A loaded `@font-face` has no `font-display` (Chrome then hides its text for up to 3 s) or uses `block`. The message says when it sets text above the fold (FOIT) | `font-display: swap` or `optional`; `&display=swap` for Google Fonts |
420
+ | `font-not-preloaded` | warn | A font file that sets text in the first viewport isn't preloaded | `<link rel="preload" as="font" type="font/woff2" crossorigin>` |
421
+ | `font-preload-no-crossorigin` | warn | A font preload without `crossorigin`, so the file downloads twice | Add `crossorigin` |
422
+ | `font-preload-unused` | info | A preloaded font that no `@font-face` uses | Remove it, or match the URL exactly |
423
+ | `font-failed` | error | A font file returned 400 or above | Fix the `src` URL |
424
+
425
+ Every font finding names the file URL. The `@font-face` rules come from the
426
+ page's stylesheets; the page can't read cross-origin ones (Google Fonts, a
427
+ CDN), so rpc downloads and parses those itself. Faces with only `local()`
428
+ sources, like next/font's size-adjusted fallbacks, never block text and are
429
+ skipped.
430
+
431
+ #### Mobile viewport
432
+
433
+ These run on phone devices only.
434
+
435
+ | id | Severity | When | Fix |
436
+ | --- | --- | --- | --- |
437
+ | `horizontal-overflow` | error | `scrollWidth` is wider than the layout viewport. Lists the outermost elements that stick out, furthest first | `max-width: 100%` on the element, or `overflow-x: auto` on its wrapper |
438
+ | `vh-units` | warn | A rendered element sized with `100vh` in `height`, `min-height` or `max-height`, from a stylesheet rule that applies or an inline style | `100dvh` or `100svh`, with `100vh` before it as a fallback |
439
+ | `tap-target-small` | warn | Links, buttons and form controls smaller than 44x44 CSS px. Links inside running text are exempt | Padding, `min-height`, or a bigger hit area |
440
+ | `input-zoom` | warn | A text field with `font-size` under 16px; iOS Safari zooms the page when it gets focus | `font-size: 16px` or more |
441
+
442
+ A headless phone has no browser bars, so `100vh` and `100dvh` measure the
443
+ same there. That's why `vh-units` reads the CSS instead of measuring
444
+ elements. The `height: 100vh; height: 100dvh` fallback pattern isn't
445
+ flagged: the browser keeps only the last value it understands.
446
+
447
+ ### JSON report
448
+
449
+ `--json` prints this object, and the MCP `check` tool returns the same one.
450
+ `schema` goes up only when a field changes meaning or is removed; new fields
451
+ can appear without it.
452
+
453
+ ```jsonc
454
+ {
455
+ "schema": 1,
456
+ "url": "https://example.com/",
457
+ "devices": [
458
+ {
459
+ "device": "iphone-15-pro",
460
+ "name": "iPhone 15 Pro",
461
+ "viewport": { "width": 393, "height": 852 },
462
+ "dpr": 3,
463
+ "mobile": true, // the phone-only checks ran here
464
+ "status": 200,
465
+ "finalUrl": "https://example.com/",
466
+ "title": "Example",
467
+ "cls": 0.012,
468
+ "layoutShifts": 2,
469
+ // only when the page didn't load on this device:
470
+ "error": "…", "code": "ERR_NAME_NOT_RESOLVED", "hint": "…"
471
+ }
472
+ ],
473
+ "summary": { "errors": 1, "warnings": 4, "info": 2 },
474
+ "findings": [
475
+ {
476
+ "id": "horizontal-overflow", // stable; see the tables above
477
+ "check": "mobile", // basics | meta | layout-shift | labels | fonts | mobile
478
+ "severity": "error", // error | warn | info
479
+ "message": "The page scrolls sideways: 601px wide in a 393px viewport",
480
+ "fix": "Find the element that sticks out …",
481
+ "devices": ["iphone-15-pro"],
482
+ "selector": "div.pricing-table", // when one element is the answer
483
+ "url": "…", // when a file is (a font, og:image)
484
+ "value": 208, // the number behind it, if there is one
485
+ "items": [ // elements, files or log lines, when there are several
486
+ {
487
+ "selector": "div.pricing-table",
488
+ "text": "600px wide, 207px past the edge",
489
+ "value": 207,
490
+ "screenshot": "/abs/.rpc-shots/…-check-horizontal-overflow-1.png" // with --shots
491
+ }
492
+ ]
493
+ }
494
+ ],
495
+ "ogImage": { "url": "…", "status": 200, "contentType": "image/png", "width": 1200, "height": 630 },
496
+ "auth": { … } // as in rpc shot, values masked
497
+ }
498
+ ```
499
+
500
+ Findings are sorted by check, in the order of the tables above, then by
501
+ severity. `id`, `check`, `severity`, `message`, `fix` and `devices` are
502
+ always there; the other fields appear when they apply.
503
+
504
+ ### In CI
505
+
506
+ ```sh
507
+ npx rpc-agent install
508
+ npx rpc-agent check https://staging.example.com --fail-on warn --json > ship-check.json
509
+ ```
510
+
511
+ The exit code fails the job, and the JSON can go into a PR comment or a
512
+ build artifact. For a page behind a login, sign in first with
513
+ `rpc login <url> --form` (see [CI and agents](#ci-and-agents-rpc-login---form)).
514
+
515
+ ### With a coding agent
516
+
517
+ Add the MCP server (below) and tell the agent to run `check` before it
518
+ reports UI work as finished, for example in `CLAUDE.md` or `AGENTS.md`:
519
+
520
+ ```md
521
+ Before you say UI work is done, run the rpc `check` tool on each page you
522
+ changed and fix every error. Fix the warnings too, or say why one doesn't apply.
523
+ ```
524
+
171
525
  ## MCP server
172
526
 
173
- `rpc mcp` runs an MCP server on stdio with two tools.
527
+ `rpc mcp` runs an MCP server on stdio with four tools.
174
528
 
175
529
  ### `screenshot`
176
530
 
@@ -187,6 +541,11 @@ and `--wait-for` timeouts.
187
541
  | `reducedMotion` | false | emulate `prefers-reduced-motion: reduce` |
188
542
  | `waitFor`, `waitMs`, `colorScheme`, `outDir`, `timeoutMs` | | as in the CLI |
189
543
  | `inline` | true | return images, not just paths |
544
+ | `cookies` | | `[{ name, value, domain?, path? }]`, set on the target origin |
545
+ | `headers` | | `{ "Authorization": "Bearer …" }`, sent to the target origin only |
546
+ | `storageState` | | path to a Playwright storageState JSON |
547
+ | `useSession` | true when one exists | use the session saved by `rpc login` for the origin |
548
+ | `allowOrigins` | | extra origins that also get the headers and cookies |
190
549
 
191
550
  The result starts with a plain-text block: one line per device (or the error
192
551
  and hint), followed by the JSON summary (file paths, pixel sizes,
@@ -213,13 +572,44 @@ skipped (`slices 11-26 (y=8520-21656 css px) skipped inline`), all slices are
213
572
  written to disk and the summary gives their paths and the full PNG, so the
214
573
  agent can Read the rest without slicing anything itself.
215
574
 
575
+ When auth is in play the text starts with an `auth:` line naming the
576
+ session, cookies and headers (values masked), and the JSON summary gains an
577
+ `auth` field. A capture that lands on a login page fails with
578
+ `AUTH_REQUIRED` and the `rpc login` hint. Signing in is something the user
579
+ does in a terminal (`rpc login`); the agent then just calls `screenshot`.
580
+
581
+ ### `check`
582
+
583
+ Runs [Ship-check](#ship-check) on one URL.
584
+
585
+ | Param | Default | |
586
+ | --- | --- | --- |
587
+ | `url` | required | http(s), file, bare host or local path |
588
+ | `devices` | `["iphone-15-pro", "desktop-1440"]` | ids or aliases; the mobile checks run on phones |
589
+ | `shots` | false | save PNGs of the elements behind layout-shift and overflow findings |
590
+ | `outDir`, `waitFor`, `waitMs`, `colorScheme`, `timeoutMs` | | as for `screenshot` |
591
+ | `cookies`, `headers`, `storageState`, `useSession`, `allowOrigins` | | as for `screenshot` |
592
+
593
+ The result is two text blocks. The first is a short summary: the counts,
594
+ then one line per error and warning with its id and selector, like
595
+ `ERROR [button-no-name] 2 buttons with no accessible name at button.icon-menu`.
596
+ The second is the JSON report described under [JSON report](#json-report).
597
+ The call is a tool error only when the page failed to load on every device;
598
+ findings, even errors, come back as a normal result for the agent to act on.
599
+
216
600
  ### `list_devices`
217
601
 
218
602
  Takes an optional `category` (`apple-phone`, `android-phone`, `tablet`,
219
603
  `laptop`) and returns ids, sizes and aliases.
220
604
 
605
+ ### `list_sessions`
606
+
607
+ Lists the origins with a saved session: cookie count, when it was saved and
608
+ whether it has expired. Never returns values.
609
+
221
610
  Files go to `outDir`, which is resolved against the server's working
222
- directory. Claude Code and Cursor start the server in the project root.
611
+ directory. Claude Code and Cursor start the server in the project root; in
612
+ Codex, set `cwd` on the server to choose the directory.
223
613
 
224
614
  ### Claude Code
225
615
 
@@ -261,6 +651,25 @@ so. Add the domain to the allowlist.
261
651
  From a clone, use `"command": "node"` and
262
652
  `"args": ["/absolute/path/to/rpc-agent/dist/cli.js", "mcp"]`.
263
653
 
654
+ ### Codex
655
+
656
+ ```sh
657
+ codex mcp add rpc -- npx -y rpc-agent mcp
658
+ ```
659
+
660
+ Or add it to `~/.codex/config.toml` (or `.codex/config.toml` in a trusted
661
+ project). The Codex CLI and IDE extension read the same file:
662
+
663
+ ```toml
664
+ [mcp_servers.rpc]
665
+ command = "npx"
666
+ args = ["-y", "rpc-agent", "mcp"]
667
+ ```
668
+
669
+ From a clone, use `command = "node"` and
670
+ `args = ["/absolute/path/to/rpc-agent/dist/cli.js", "mcp"]`. `/mcp` in the
671
+ Codex TUI lists the servers it started.
672
+
264
673
  ## Development
265
674
 
266
675
  ```sh
@@ -278,6 +687,13 @@ appear once, that the IntersectionObserver section rendered, and the slice
278
687
  count and labels, including through the MCP server. The missing-browser tests
279
688
  point `PLAYWRIGHT_BROWSERS_PATH` at an empty temp dir.
280
689
 
690
+ The Ship-check tests serve `test/fixtures/check`: one page per check
691
+ (`meta.html`, `shift.html`, `labels.html`, `fonts.html`, `mobile.html`,
692
+ `basics.html`) and `clean.html`, which must produce no errors or warnings.
693
+ `shift.html` inserts a 300 px banner 800 ms after load, so CLS is known in
694
+ advance (300/852 on the phone, 300/1440 on desktop); the test runs it twice
695
+ and compares.
696
+
281
697
  The README media are built from real captures by `scripts/media/build.ts`;
282
698
  `docs/media.md` explains how. The CLI's design notes, and what it borrows
283
699
  from the `skills` CLI, are in `docs/cli-design.md`.
@@ -295,8 +711,15 @@ from the `skills` CLI, are in `docs/cli-design.md`.
295
711
  - The bezel geometry is RPC's, unchanged. On kind-default phones the page
296
712
  covers the dynamic island or notch, and generic monitors are drawn edge to
297
713
  edge with only the stand showing.
298
- - No auth, cookies, custom headers or scripted interaction before capture.
714
+ - No scripted interaction before capture (beyond `rpc login --form`).
715
+ - Login-page detection goes by the URL path. An app that shows its sign-in
716
+ form at the requested URL without redirecting is captured as is.
299
717
  - Every MCP call launches a fresh Chromium, which adds about a second.
718
+ - Ship-check reads `100vh` only from stylesheets the page can read and from
719
+ inline styles; a `100vh` rule in a cross-origin stylesheet is missed. Tap
720
+ targets are measured by their box, so a hit area enlarged with a
721
+ pseudo-element still counts as small. CLS covers the load and one scroll,
722
+ not shifts caused by clicking around.
300
723
 
301
724
  ## License
302
725