rpc-agent 0.4.0 → 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/dist/cli.js CHANGED
@@ -5,14 +5,19 @@ import { ArgError, parseCli, USAGE } from './args.js';
5
5
  import { clearSessions, describeAuth, listSessions, localTime, sessionsDir } from './auth.js';
6
6
  import { doctor, installChromium } from './browser.js';
7
7
  import { capture } from './capture.js';
8
+ import { checkExitCode, runCheck, summaryLine } from './check.js';
9
+ import { deviceLine, plainReport } from './check-format.js';
8
10
  import { CATEGORY_LABELS, CATEGORY_ORDER, DEVICE_ALIASES, DEVICES } from './devices.js';
9
11
  import { currentMode, openPath, waitForEnter } from './term.js';
10
12
  const NO_PAINT = { ok: s => s, bad: s => s, warn: s => s };
11
13
  let paint = NO_PAINT;
14
+ /** The full palette for check output; identity functions unless FORCE_COLOR. */
15
+ let colours;
12
16
  async function plainPaint() {
17
+ const { default: pc } = await import('picocolors');
18
+ colours = pc.createColors(currentMode().color);
13
19
  if (!currentMode().color)
14
20
  return NO_PAINT;
15
- const { default: pc } = await import('picocolors');
16
21
  const c = pc.createColors(true);
17
22
  return { ok: c.green, bad: c.red, warn: c.yellow };
18
23
  }
@@ -102,6 +107,23 @@ async function main(argv) {
102
107
  openPath(result.outDir);
103
108
  return result.shots.some(s => s.error) ? 1 : 0;
104
109
  }
110
+ case 'check': {
111
+ if (mode.rich) {
112
+ const { ui, c } = await loadUi();
113
+ return ui.runCheck(cmd.options, c);
114
+ }
115
+ const report = await runCheck(cmd.options, cmd.json
116
+ ? {}
117
+ : {
118
+ onAuth: a => console.log(`auth: ${describeAuth(a)}`),
119
+ onDevice: d => console.log(deviceLine(d, colours)),
120
+ });
121
+ if (cmd.json)
122
+ console.log(JSON.stringify(report, null, 2));
123
+ else
124
+ printCheck(report);
125
+ return checkExitCode(report, cmd.options.failOn);
126
+ }
105
127
  case 'login': {
106
128
  if (cmd.passwordFromFlag) {
107
129
  console.error(`rpc: ${paint.warn('warning:')} --password is visible in shell history and the process list; set RPC_LOGIN_PASSWORD instead`);
@@ -187,6 +209,15 @@ function rel(p) {
187
209
  const r = relative(process.cwd(), p);
188
210
  return r.startsWith('..') ? p : r;
189
211
  }
212
+ function printCheck(report) {
213
+ const body = plainReport(report, colours);
214
+ if (body)
215
+ console.log(body);
216
+ console.log('');
217
+ const s = report.summary;
218
+ const line = summaryLine(s);
219
+ console.log(s.errors ? paint.bad(line) : s.warnings ? paint.warn(line) : paint.ok(line));
220
+ }
190
221
  function printDoctor(r) {
191
222
  console.log(`node ${r.node}`);
192
223
  console.log(`playwright ${r.playwright}`);
package/dist/help.js CHANGED
@@ -22,6 +22,17 @@ const AUTH_OPTIONS = [
22
22
  [' --no-session', "don't use the session saved by `rpc login` for this origin"],
23
23
  [' --allow-origin <o>', 'also send headers and loaded cookies to this origin, repeatable'],
24
24
  ];
25
+ const CHECK_OPTIONS = [
26
+ ['-d, --devices <ids>', 'comma-separated device ids or aliases (default: iphone-15-pro,desktop-1440)'],
27
+ [' --fail-on <level>', 'error (default) or warn: also exit 1 on warnings'],
28
+ [' --shots', 'screenshot the elements behind layout-shift and overflow findings'],
29
+ ['-o, --out <dir>', 'where --shots writes (default: ./.rpc-shots)'],
30
+ [' --wait-for <selector>', 'wait for a CSS selector before checking'],
31
+ [' --wait <ms>', 'extra settle delay after the scroll (default: 0)'],
32
+ [' --color-scheme <s>', 'light | dark | no-preference'],
33
+ [' --timeout <ms>', 'navigation timeout (default: 30000)'],
34
+ [' --json', 'print the report as JSON (schema in the README)'],
35
+ ];
25
36
  const LOGIN_OPTIONS = [
26
37
  [' --save <file>', 'write the storage state here instead of the per-origin session'],
27
38
  [' --form', 'headless: fill an email + password form (CI, agents)'],
@@ -47,6 +58,18 @@ export const COMMANDS = {
47
58
  ["rpc shot api.example.com/admin --header 'Authorization: Bearer $TOKEN'", ''],
48
59
  ],
49
60
  },
61
+ check: {
62
+ usage: 'rpc check <url> [options]',
63
+ summary: 'Audit one page before launch: meta and social tags, layout shift (CLS), unlabelled controls, web fonts, ' +
64
+ 'mobile viewport bugs and basics (status, mixed content, console errors). Exits 1 when there are errors.',
65
+ options: [...CHECK_OPTIONS, ...AUTH_OPTIONS],
66
+ examples: [
67
+ ['rpc check localhost:3000', 'phone + desktop'],
68
+ ['rpc check localhost:3000 --fail-on warn --json', 'CI: fail on warnings too'],
69
+ ['rpc check example.com -d pixel,iphone-se,desktop --shots', 'more devices, element screenshots'],
70
+ ['rpc check localhost:3000/billing', 'uses the session saved by rpc login'],
71
+ ],
72
+ },
50
73
  login: {
51
74
  usage: 'rpc login <url> [--form] [--save <file>]',
52
75
  summary: 'Open a browser window at the URL, sign in, then press Enter (or close the window). The session is saved for that ' +
@@ -94,12 +117,13 @@ export const COMMANDS = {
94
117
  },
95
118
  mcp: {
96
119
  usage: 'rpc mcp',
97
- summary: 'Run the MCP server on stdio (tools: screenshot, list_devices, list_sessions).',
120
+ summary: 'Run the MCP server on stdio (tools: screenshot, check, list_devices, list_sessions).',
98
121
  examples: [['claude mcp add --scope user rpc -- node /abs/path/dist/cli.js mcp', 'add to Claude Code']],
99
122
  },
100
123
  };
101
124
  const TOP = [
102
125
  ['shot <url>', 'capture screenshots'],
126
+ ['check <url>', 'pre-launch audit: meta, layout shift, labels, fonts, mobile'],
103
127
  ['login <url>', 'sign in once; later shots of that origin use the session'],
104
128
  ['sessions', 'list or clear saved sessions'],
105
129
  ['devices', 'list device ids and aliases'],
package/dist/mcp.js CHANGED
@@ -4,9 +4,11 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
4
  import { z } from 'zod';
5
5
  import { describeAuth, listSessions, resolveAuth, sessionsDir, toOrigin } from './auth.js';
6
6
  import { capture, launchBrowser } from './capture.js';
7
+ import { runCheck } from './check.js';
8
+ import { textSummary } from './check-format.js';
7
9
  import { renderPreview } from './composite.js';
8
10
  import { CATEGORY_LABELS, CATEGORY_ORDER, DEFAULT_DEVICES, DEVICE_ALIASES, DEVICES, resolveDevices, } from './devices.js';
9
- import { COLOR_SCHEMES, DEFAULT_MAX_SLICES, DEFAULT_OUT_DIR, DEFAULT_SLICE_WIDTH, DEFAULT_TIMEOUT_MS, normalizeUrl, } from './options.js';
11
+ import { COLOR_SCHEMES, DEFAULT_MAX_SLICES, DEFAULT_OUT_DIR, DEFAULT_SLICE_WIDTH, DEFAULT_TIMEOUT_MS, defaultCheckOptions, normalizeUrl, } from './options.js';
10
12
  import { DEFAULT_MAX_HEIGHT } from './slices.js';
11
13
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
12
14
  const screenshotInput = {
@@ -65,6 +67,8 @@ const screenshotInput = {
65
67
  .boolean()
66
68
  .optional()
67
69
  .describe('Return JPEG previews inline: one per device, or the full-page slices when fullPage. Default true.'),
70
+ };
71
+ const authInput = {
68
72
  cookies: z
69
73
  .array(z.object({
70
74
  name: z.string().min(1),
@@ -93,6 +97,41 @@ const screenshotInput = {
93
97
  .optional()
94
98
  .describe('Extra origins (e.g. an API host) that also get the headers and loaded cookies.'),
95
99
  };
100
+ const checkInput = {
101
+ url: z
102
+ .string()
103
+ .describe('Page to audit. http(s)://, file://, a bare host like localhost:3000, or a path to a local .html file.'),
104
+ devices: z
105
+ .array(z.string())
106
+ .min(1)
107
+ .max(8)
108
+ .optional()
109
+ .describe(`Device ids or aliases (see list_devices). Default: ${DEFAULT_DEVICES.join(', ')}. ` +
110
+ 'The mobile viewport checks (100vh, sideways scroll, tap targets, input zoom) run on phones only.'),
111
+ shots: z
112
+ .boolean()
113
+ .optional()
114
+ .describe('Save PNGs of the elements behind layout-shift and horizontal-overflow findings (paths in the report). Default false.'),
115
+ outDir: z.string().optional().describe(`Where shots go, relative to the server's cwd. Default ${DEFAULT_OUT_DIR}.`),
116
+ waitFor: z.string().optional().describe('CSS selector to wait for (visible) before checking.'),
117
+ waitMs: z.number().int().min(0).max(30_000).optional().describe('Extra settle delay after the scroll, in ms.'),
118
+ colorScheme: z.enum(COLOR_SCHEMES).optional().describe('Emulate prefers-color-scheme.'),
119
+ timeoutMs: z.number().int().min(1000).max(120_000).optional().describe(`Navigation timeout. Default ${DEFAULT_TIMEOUT_MS}.`),
120
+ };
121
+ function authFrom(args) {
122
+ const auth = {};
123
+ if (args.cookies?.length)
124
+ auth.cookies = args.cookies;
125
+ if (args.headers && Object.keys(args.headers).length)
126
+ auth.headers = args.headers;
127
+ if (args.storageState)
128
+ auth.storageState = args.storageState;
129
+ if (args.useSession === false)
130
+ auth.useSession = false;
131
+ if (args.allowOrigins?.length)
132
+ auth.allowOrigins = args.allowOrigins.map(toOrigin);
133
+ return auth;
134
+ }
96
135
  export function createServer() {
97
136
  const server = new McpServer({ name: 'rpc', version: pkg.version });
98
137
  server.registerTool('screenshot', {
@@ -100,22 +139,12 @@ export function createServer() {
100
139
  description: 'Load a URL in headless Chromium at one or more device viewports (with device pixel ratio, mobile user agent and touch) ' +
101
140
  'and save PNG screenshots, optionally framed in a device bezel. Use it to check responsive layout of a page you are building, ' +
102
141
  'including local dev servers. Returns file paths, page title, HTTP status and console errors, plus inline previews.',
103
- inputSchema: screenshotInput,
142
+ inputSchema: { ...screenshotInput, ...authInput },
104
143
  annotations: { readOnlyHint: false, openWorldHint: true },
105
144
  }, async (args) => {
106
145
  let options;
107
146
  try {
108
- const auth = {};
109
- if (args.cookies?.length)
110
- auth.cookies = args.cookies;
111
- if (args.headers && Object.keys(args.headers).length)
112
- auth.headers = args.headers;
113
- if (args.storageState)
114
- auth.storageState = args.storageState;
115
- if (args.useSession === false)
116
- auth.useSession = false;
117
- if (args.allowOrigins?.length)
118
- auth.allowOrigins = args.allowOrigins.map(toOrigin);
147
+ const auth = authFrom(args);
119
148
  options = {
120
149
  url: normalizeUrl(args.url),
121
150
  devices: resolveDevices(args.devices ?? DEFAULT_DEVICES).map(d => d.id),
@@ -183,9 +212,58 @@ export function createServer() {
183
212
  await browser.close();
184
213
  }
185
214
  });
215
+ server.registerTool('check', {
216
+ title: 'Pre-launch check of a page',
217
+ description: 'Audit one URL before calling the work done: meta and social tags (title, description, canonical, lang, viewport zoom, ' +
218
+ 'favicon, Open Graph, twitter:card, and whether og:image loads and its size), layout shift (CLS with the elements that moved), ' +
219
+ 'buttons/links/images/fields without an accessible name, web fonts (font-display, preloads), mobile viewport bugs on phones ' +
220
+ '(100vh, sideways scroll, tap targets under 44px, inputs under 16px) and basics (HTTP status, mixed content, console errors). ' +
221
+ 'Returns a short summary, then the JSON report: each finding has id, severity (error|warn|info), message, selector or url, ' +
222
+ 'devices and a fix. Fix the errors (and ideally warnings), then run it again.',
223
+ inputSchema: { ...checkInput, ...authInput },
224
+ annotations: { readOnlyHint: false, openWorldHint: true },
225
+ }, async (args) => {
226
+ let options;
227
+ try {
228
+ const auth = authFrom(args);
229
+ options = {
230
+ ...defaultCheckOptions(args.url),
231
+ devices: resolveDevices(args.devices ?? DEFAULT_DEVICES).map(d => d.id),
232
+ outDir: args.outDir ?? DEFAULT_OUT_DIR,
233
+ waitFor: args.waitFor,
234
+ waitMs: args.waitMs ?? 0,
235
+ colorScheme: args.colorScheme,
236
+ timeoutMs: args.timeoutMs ?? DEFAULT_TIMEOUT_MS,
237
+ shots: args.shots ?? false,
238
+ ...(Object.keys(auth).length ? { auth } : {}),
239
+ };
240
+ resolveAuth(options.url, auth);
241
+ }
242
+ catch (err) {
243
+ return { isError: true, content: [{ type: 'text', text: err.message }] };
244
+ }
245
+ let browser;
246
+ try {
247
+ browser = await launchBrowser();
248
+ }
249
+ catch (err) {
250
+ return { isError: true, content: [{ type: 'text', text: `Could not start the browser: ${err.message}` }] };
251
+ }
252
+ try {
253
+ const report = await runCheck(options, { browser });
254
+ const content = [
255
+ { type: 'text', text: (report.auth ? `auth: ${describeAuth(report.auth)}\n` : '') + textSummary(report) },
256
+ { type: 'text', text: JSON.stringify(report, null, 2) },
257
+ ];
258
+ return report.devices.every(d => d.error) ? { isError: true, content } : { content };
259
+ }
260
+ finally {
261
+ await browser.close();
262
+ }
263
+ });
186
264
  server.registerTool('list_devices', {
187
265
  title: 'List device presets',
188
- description: 'List device ids, CSS viewport sizes and pixel ratios usable in screenshot.devices, plus short aliases.',
266
+ description: 'List device ids, CSS viewport sizes and pixel ratios usable in screenshot.devices and check.devices, plus short aliases.',
189
267
  inputSchema: {
190
268
  category: z
191
269
  .enum(CATEGORY_ORDER)
package/dist/options.js CHANGED
@@ -24,6 +24,20 @@ export function defaultShotOptions(url) {
24
24
  sliceWidth: DEFAULT_SLICE_WIDTH,
25
25
  };
26
26
  }
27
+ export const FAIL_ON = ['error', 'warn'];
28
+ export function defaultCheckOptions(url) {
29
+ return {
30
+ url: normalizeUrl(url),
31
+ devices: [...DEFAULT_DEVICES],
32
+ outDir: DEFAULT_OUT_DIR,
33
+ waitMs: 0,
34
+ timeoutMs: DEFAULT_TIMEOUT_MS,
35
+ emulate: true,
36
+ reducedMotion: false,
37
+ shots: false,
38
+ failOn: 'error',
39
+ };
40
+ }
27
41
  /**
28
42
  * Accept what an agent is likely to type:
29
43
  * http(s)://…, file://… -> unchanged
package/dist/png.js CHANGED
@@ -8,6 +8,53 @@ export function pngSize(buf) {
8
8
  const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
9
9
  return { width: view.getUint32(16), height: view.getUint32(20) };
10
10
  }
11
+ /** Width/height of a PNG, JPEG, GIF or WebP from its header; undefined for anything else. */
12
+ export function imageSize(buf) {
13
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
14
+ const ascii = (at, len) => String.fromCharCode(...buf.subarray(at, at + len));
15
+ try {
16
+ if (buf.length >= 24 && SIGNATURE.every((b, i) => buf[i] === b))
17
+ return pngSize(buf);
18
+ if (ascii(0, 4) === 'GIF8' && buf.length >= 10) {
19
+ return { width: view.getUint16(6, true), height: view.getUint16(8, true) };
20
+ }
21
+ if (ascii(0, 4) === 'RIFF' && ascii(8, 4) === 'WEBP') {
22
+ const kind = ascii(12, 4);
23
+ if (kind === 'VP8 ')
24
+ return { width: view.getUint16(26, true) & 0x3fff, height: view.getUint16(28, true) & 0x3fff };
25
+ if (kind === 'VP8L') {
26
+ const bits = view.getUint32(21, true);
27
+ return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 };
28
+ }
29
+ if (kind === 'VP8X') {
30
+ const u24 = (at) => buf[at] | (buf[at + 1] << 8) | (buf[at + 2] << 16);
31
+ return { width: u24(24) + 1, height: u24(27) + 1 };
32
+ }
33
+ return undefined;
34
+ }
35
+ if (buf[0] === 0xff && buf[1] === 0xd8) {
36
+ // Walk the JPEG segments to the first start-of-frame marker.
37
+ for (let at = 2; at + 9 < buf.length;) {
38
+ if (buf[at] !== 0xff)
39
+ return undefined;
40
+ const marker = buf[at + 1];
41
+ if (marker === 0xff) {
42
+ at++;
43
+ continue;
44
+ }
45
+ const len = view.getUint16(at + 2);
46
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
47
+ return { width: view.getUint16(at + 7), height: view.getUint16(at + 5) };
48
+ }
49
+ at += 2 + len;
50
+ }
51
+ }
52
+ }
53
+ catch {
54
+ // Truncated header.
55
+ }
56
+ return undefined;
57
+ }
11
58
  /**
12
59
  * Decode an 8-bit RGB/RGBA non-interlaced PNG, which is what Chromium
13
60
  * screenshots are. Anything else throws: this is not a general decoder.
package/dist/ui.js CHANGED
@@ -9,6 +9,8 @@ import pc from 'picocolors';
9
9
  import { clearSessions, listSessions, localTime, MASK, resolveAuth, sessionsDir, } from './auth.js';
10
10
  import { BrowserMissingError, installHint, launchBrowser } from './browser.js';
11
11
  import { capture } from './capture.js';
12
+ import { checkExitCode, runCheck as audit, summaryLine } from './check.js';
13
+ import { countsPhrase, groupFindings, groupLines } from './check-format.js';
12
14
  import { CATEGORY_ORDER, DEVICE_ALIASES, DEVICES, } from './devices.js';
13
15
  import { defaultShotOptions, normalizeUrl } from './options.js';
14
16
  import { openCommand, openPath, waitForEnter } from './term.js';
@@ -389,6 +391,110 @@ export async function runShot(options, run, c) {
389
391
  p.outro(`${c.green('Done!')} ${c.dim(advice)}`);
390
392
  return failed > 0 ? 1 : 0;
391
393
  }
394
+ // ------------------------------------------------------------------ check
395
+ /** Lines for the "Check Summary" box. */
396
+ export function checkSummaryLines(o, c, auth) {
397
+ const rows = [
398
+ ['devices', formatList(o.devices.map(d => c.cyan(d)))],
399
+ ['fail on', o.failOn === 'warn' ? c.yellow('errors and warnings') : 'errors'],
400
+ ];
401
+ if (o.colorScheme)
402
+ rows.push(['colour scheme', o.colorScheme]);
403
+ if (o.waitFor)
404
+ rows.push(['wait for', o.waitFor]);
405
+ if (auth)
406
+ rows.push(...authRows(auth, c));
407
+ if (o.shots)
408
+ rows.push(['shots', `${rel(o.outDir)}/`]);
409
+ const w = Math.max(...rows.map(([k]) => k.length)) + 1;
410
+ return [c.cyan(o.url), ...rows.map(([k, v]) => ` ${c.dim(`${k}:`.padEnd(w))} ${v}`)];
411
+ }
412
+ /** The spinner line once a device is done: status, CLS, time. */
413
+ function checkDeviceLine(d, ms, label, c) {
414
+ if (d.error)
415
+ return `${label} ${c.red(d.code ?? 'error')} ${c.dim(formatMs(ms))}`;
416
+ const status = d.status == null ? c.dim('--') : d.status >= 400 ? c.red(String(d.status)) : c.green(String(d.status));
417
+ const cls = d.cls === undefined ? '' : d.cls > 0.25 ? c.red(d.cls.toFixed(3)) : d.cls > 0.1 ? c.yellow(d.cls.toFixed(3)) : c.green(d.cls.toFixed(3));
418
+ return `${label} ${status} ${c.dim('CLS')} ${cls} ${c.dim(formatMs(ms))}`;
419
+ }
420
+ /** One box per check that found something, titled with its counts. */
421
+ export function checkBoxes(report, c) {
422
+ const all = report.devices.map(d => d.device);
423
+ return groupFindings(report).map(g => {
424
+ const counts = countsPhrase(g.findings);
425
+ const tint = g.findings.some(f => f.severity === 'error') ? c.red : g.findings.some(f => f.severity === 'warn') ? c.yellow : c.cyan;
426
+ return { title: `${g.title} ${c.dim('·')} ${tint(counts)}`, lines: groupLines(g, all, c, { icons: true, width: textWidth() }) };
427
+ });
428
+ }
429
+ export async function runCheck(options, c) {
430
+ const auth = resolveAuth(options.url, options.auth ?? {});
431
+ intro(c);
432
+ p.note(checkSummaryLines(options, c, auth.summary).join('\n'), 'Check Summary');
433
+ const s = p.spinner();
434
+ s.start('Starting Chromium');
435
+ let browser;
436
+ try {
437
+ browser = await launchBrowser();
438
+ }
439
+ catch (err) {
440
+ if (err instanceof BrowserMissingError) {
441
+ s.error(c.red("Playwright's Chromium is not installed"));
442
+ p.note(wrap(installHint().replace(/^Fix: /, ''), textWidth()).join('\n'), c.yellow('Fix'));
443
+ p.outro(`${c.red('Nothing checked.')} ${c.dim('Run: rpc install')}`);
444
+ return 1;
445
+ }
446
+ s.error(c.red(`Chromium did not start: ${err.message.split('\n')[0]}`));
447
+ p.outro(c.red('Nothing checked.'));
448
+ return 1;
449
+ }
450
+ s.stop(`Chromium ${c.dim(browser.version())}`);
451
+ let current;
452
+ let started = 0;
453
+ let report;
454
+ const picked = options.devices.map(id => DEVICES.find(d => d.id === id)).filter((d) => Boolean(d));
455
+ const idW = Math.max(0, ...picked.map(d => d.id.length));
456
+ const vpW = Math.max(0, ...picked.map(d => viewport(d).length));
457
+ const label = (id, vp) => `${id.padEnd(idW)} ${c.dim(vp.padEnd(vpW))}`;
458
+ try {
459
+ report = await audit(options, {
460
+ browser,
461
+ auth,
462
+ onDeviceStart: (d) => {
463
+ started = performance.now();
464
+ current = p.spinner();
465
+ current.start(label(d.id, viewport(d)));
466
+ },
467
+ onDevice: d => {
468
+ const line = checkDeviceLine(d, performance.now() - started, label(d.device, viewport({ ...d.viewport, dpr: d.dpr })), c);
469
+ if (d.error)
470
+ current?.error(line);
471
+ else
472
+ current?.stop(line);
473
+ current = undefined;
474
+ },
475
+ });
476
+ }
477
+ catch (err) {
478
+ current?.error(c.red(err.message));
479
+ p.outro(c.red('Check failed.'));
480
+ return 1;
481
+ }
482
+ finally {
483
+ await browser.close().catch(() => { });
484
+ }
485
+ for (const box of checkBoxes(report, c))
486
+ p.note(box.lines.join('\n'), box.title);
487
+ const code = checkExitCode(report, options.failOn);
488
+ const line = summaryLine(report.summary);
489
+ const sum = report.summary;
490
+ if (sum.errors)
491
+ p.outro(`${c.red(line)} ${c.dim('Exit code 1.')}`);
492
+ else if (sum.warnings)
493
+ p.outro(`${c.yellow(line)} ${c.dim(code ? 'Exit code 1 (--fail-on warn).' : 'No errors.')}`);
494
+ else
495
+ p.outro(`${c.green(line)} ${c.dim('Ready to ship.')}`);
496
+ return code;
497
+ }
392
498
  // ------------------------------------------------------------------ interactive
393
499
  const PRESELECT = ['iphone-15-pro', 'ipad-air-11', 'desktop-1440'];
394
500
  const DEV_PORTS = [3000, 5173, 4321, 8080];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rpc-agent",
3
- "version": "0.4.0",
4
- "description": "Screenshot a URL at device viewports, optionally framed in device bezels, signed in or not. CLI and MCP server for AI coding agents.",
3
+ "version": "0.5.0",
4
+ "description": "Screenshot a URL at device viewports, optionally framed in device bezels, signed in or not, and audit it before launch. CLI and MCP server for AI coding agents.",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "mcp-server",