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 +94 -0
- package/README.md +428 -5
- package/dist/args.js +214 -25
- package/dist/auth.js +416 -0
- package/dist/browser.js +6 -4
- package/dist/capture.js +47 -11
- package/dist/check-format.js +120 -0
- package/dist/check-page.js +336 -0
- package/dist/check.js +677 -0
- package/dist/cli.js +115 -4
- package/dist/help.js +78 -4
- package/dist/login.js +150 -0
- package/dist/mcp.js +152 -7
- package/dist/options.js +14 -0
- package/dist/png.js +47 -0
- package/dist/term.js +20 -0
- package/dist/ui.js +220 -3
- package/package.json +3 -2
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|