rpc-agent 0.3.1 → 0.4.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,59 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ Authenticated captures: screenshot logged-in pages without a hand-written
6
+ "sign in, then screenshot" script.
7
+
8
+ ### Added
9
+
10
+ - `rpc login <url>` opens a browser window; sign in, press Enter (or close
11
+ the window) and the storage state is saved for that origin to
12
+ `~/.config/rpc-agent/sessions/<origin>.json` (file 0600, directory 0700).
13
+ `--save <file>` writes it elsewhere.
14
+ - `rpc login <url> --form`: headless email + password sign-in for CI and
15
+ agents, from `RPC_LOGIN_EMAIL` / `RPC_LOGIN_PASSWORD` (or `--email` /
16
+ `--password`), with `--email-selector`, `--password-selector` and
17
+ `--submit-selector`.
18
+ - `rpc shot` uses the saved session for the target origin automatically.
19
+ `--no-session` opts out. `rpc sessions [list]` and
20
+ `rpc sessions clear [origin]` manage them.
21
+ - `rpc shot --cookie "name=value[; Domain=…; Path=…]"`, `--header
22
+ "Name: value"` (both repeatable), `--storage-state <file>` and
23
+ `--allow-origin <origin>`.
24
+ - A capture redirected to a login page (`/login`, `/sign-in`, `/auth`, …)
25
+ fails with code `AUTH_REQUIRED` and the hint ``Session missing or
26
+ expired: run `rpc login <origin>` `` instead of screenshotting the sign-in
27
+ form. Sessions whose cookies have all expired aren't loaded.
28
+ - MCP `screenshot` takes `cookies`, `headers`, `storageState`, `useSession`
29
+ and `allowOrigins`. New `list_sessions` tool.
30
+ - `rpc install --headed` also downloads full Chromium, for the
31
+ `rpc login` window. Without it, `rpc login` uses an installed Google
32
+ Chrome if there is one.
33
+
34
+ ### Security
35
+
36
+ - Headers and cookies go to the target origin only (plus any
37
+ `--allow-origin`). Headers are injected per request through the CDP Fetch
38
+ domain, so a redirect to another host doesn't carry them. Saved sessions
39
+ and storage-state files load only the target origin's cookies and
40
+ localStorage.
41
+ - Cookie, header and localStorage values are never printed: output shows
42
+ names with `•••`, and console errors and failure messages are scrubbed.
43
+ - `--password` prints a warning; use the environment variable.
44
+
45
+ ### Changed
46
+
47
+ - A page that redirects to a login URL now fails that device with
48
+ `AUTH_REQUIRED` (exit code 1) even when no auth was given. In 0.3 it
49
+ screenshotted the sign-in page and reported success.
50
+
51
+ ### Unchanged
52
+
53
+ - With no auth in play, plain output, `--json` output and MCP results are
54
+ the 0.3 ones. With auth, they gain an `auth:` line / an `auth` field.
55
+ Existing MCP params and result fields are unchanged.
56
+
3
57
  ## 0.3.1
4
58
 
5
59
  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,11 @@ 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 login <url> [--form] # sign in once; later shots of that origin use the session
67
+ rpc sessions [list|clear [origin]]
63
68
  rpc devices [--json]
64
69
  rpc doctor [--json] # is Chromium installed, and does it launch?
65
- rpc install # download Chromium with rpc's own Playwright version
70
+ rpc install [--headed] # download Chromium with rpc's own Playwright version
66
71
  rpc mcp
67
72
  rpc help <command> # options and examples for one command
68
73
  ```
@@ -96,6 +101,11 @@ the files it wrote:
96
101
  | `--no-emulate` | Viewport and DPR only: no mobile user agent or touch |
97
102
  | `--json` | Print the result as JSON |
98
103
  | `--open` | Open the output folder when done (`open`, `xdg-open` or `explorer`) |
104
+ | `--cookie <c>` | `name=value[; Domain=…; Path=…]`, repeatable. See [Authenticated pages](#authenticated-pages) |
105
+ | `--header <h>` | `Name: value`, repeatable, sent to the target origin only |
106
+ | `--storage-state <file>` | Load a Playwright storageState JSON |
107
+ | `--no-session` | Don't use the session saved by `rpc login` |
108
+ | `--allow-origin <o>` | Also send headers and loaded cookies to this origin, repeatable |
99
109
 
100
110
  The URL can be a full URL, a bare local host (`localhost:3000` becomes
101
111
  `http://`), a bare domain (`example.com` becomes `https://`) or a path to a
@@ -168,9 +178,130 @@ listening on port N"), proxy, tunnel and offline errors, TLS errors (https
168
178
  against a plain-http dev server), missing files, redirect loops, navigation
169
179
  and `--wait-for` timeouts.
170
180
 
181
+ ## Authenticated pages
182
+
183
+ Dashboards, billing and settings pages sit behind a login. rpc can carry a
184
+ session so you don't need a hand-written "sign in, then screenshot" script.
185
+
186
+ ### Sign in once with `rpc login`
187
+
188
+ ```sh
189
+ rpc login localhost:3000/sign-in
190
+ rpc shot localhost:3000/app/settings/billing -d iphone-15,desktop-1440 --bezels
191
+ ```
192
+
193
+ `rpc login` opens a browser window at the URL. Sign in as you normally
194
+ would (SSO and 2FA work, it's a real browser), then press Enter in the
195
+ terminal or close the window. rpc saves the browser's storage state
196
+ (cookies and localStorage) to
197
+ `~/.config/rpc-agent/sessions/<origin>.json`, for example
198
+ `http-localhost-3000.json`. From then on, `rpc shot` on that origin uses it
199
+ automatically, and the output says so:
200
+
201
+ ```
202
+ auth: session http://localhost:3000 (saved 2026-10-29 22:55)
203
+ ok iphone-15 (393x852 @3x)
204
+ ```
205
+
206
+ The window needs full Chromium, which `rpc install` doesn't download (it
207
+ fetches only the headless shell). Run `rpc install --headed` once, or have
208
+ Google Chrome installed; rpc falls back to it.
209
+
210
+ `--save <file>` writes the storage state to a file of your choice instead;
211
+ use it with `rpc shot --storage-state <file>`.
212
+
213
+ ```sh
214
+ rpc sessions # origins, cookie counts, saved and expiry times
215
+ rpc sessions clear localhost:3000 # forget one origin
216
+ rpc sessions clear # forget all
217
+ rpc shot localhost:3000/app --no-session # capture as a signed-out visitor
218
+ ```
219
+
220
+ ### When the session is missing or expired
221
+
222
+ If the page redirects to a login URL (a path segment like `/login`,
223
+ `/sign-in`, `/signin` or `/auth`, different from the one you asked for),
224
+ rpc doesn't screenshot the sign-in form. The device fails with code
225
+ `AUTH_REQUIRED`:
226
+
227
+ ```
228
+ x iphone-15 (393x852 @3x): Redirected to a login page (http://localhost:3000/sign-in) instead of http://localhost:3000/app/settings/billing
229
+ hint: Session missing or expired: run `rpc login http://localhost:3000`
230
+ ```
231
+
232
+ A saved session whose cookies have all passed their expiry isn't loaded,
233
+ and gives the same hint.
234
+
235
+ ### CI and agents: `rpc login --form`
236
+
237
+ Without a person at the keyboard, `--form` signs in headlessly through a
238
+ standard email + password form:
239
+
240
+ ```sh
241
+ export RPC_LOGIN_EMAIL=ci@example.test
242
+ export RPC_LOGIN_PASSWORD=... # from your CI secret store
243
+ rpc login https://staging.example.com/sign-in --form
244
+ rpc shot https://staging.example.com/app/billing --json
245
+ ```
246
+
247
+ It loads the page, waits for it to settle (so a client-side form has
248
+ hydrated), fills the first match of `--email-selector` (default
249
+ `input[type=email]`, then `autocomplete=username`, `name=email`,
250
+ `name=username`, `#email`) and `--password-selector` (default
251
+ `input[type=password]`), clicks `--submit-selector` (default
252
+ `button[type=submit], input[type=submit]`, or presses Enter if there is no
253
+ button), and waits for the URL to leave the login page. If it doesn't
254
+ within `--timeout`, it fails and quotes any alert the page shows ("Invalid
255
+ email or password"). `--email` works as a flag too; see the security notes
256
+ before using `--password`.
257
+
258
+ ### Cookies and headers
259
+
260
+ ```sh
261
+ rpc shot localhost:3000/app --cookie "session=$SESSION"
262
+ rpc shot app.example.com/admin --cookie "sid=$SID; Domain=example.com; Path=/"
263
+ rpc shot api.example.com/docs --header "Authorization: Bearer $TOKEN"
264
+ rpc shot localhost:3000/app --storage-state auth.json
265
+ ```
266
+
267
+ - `--cookie` is host-only on the target unless you give a `Domain`, which
268
+ must match the target host (or an `--allow-origin` host). `Secure`,
269
+ `HttpOnly` and `SameSite` are accepted too.
270
+ - `--header` is sent only on requests to the target origin. Images,
271
+ scripts and API calls to other origins don't get it, and neither does a
272
+ redirect to another host (every redirect hop is checked).
273
+ - `--storage-state` and saved sessions load only the target origin's
274
+ cookies and localStorage, even if the file holds others (an SSO domain,
275
+ analytics).
276
+ - `--allow-origin https://api.example.com` widens all of the above to
277
+ another origin, for an app that calls a separate API host.
278
+ - A saved session is used unless `--no-session` or `--storage-state` is
279
+ given. Cookies and headers are added on top of it.
280
+
281
+ ### Security notes
282
+
283
+ - **Values are never printed.** Plain, rich and `--json` output and MCP
284
+ results show cookie and header names with `•••`. Console errors and
285
+ failure messages are scrubbed of every value rpc injected. The `--json`
286
+ result gains an `auth` field, for example
287
+ `{"cookies":[{"name":"sid","value":"•••","domain":"localhost","path":"/"}]}`.
288
+ - **Session files are credentials.** They are written with mode 0600 in a
289
+ 0700 directory (`$XDG_CONFIG_HOME/rpc-agent/sessions` if set;
290
+ `RPC_SESSIONS_DIR` overrides the location). Anyone who can read one can
291
+ act as you on that site until it expires. `rpc sessions clear` deletes
292
+ them. Don't commit a `--save` file.
293
+ - **Keep passwords out of shell history.** Prefer `RPC_LOGIN_PASSWORD` from
294
+ a secret store over `--password`, which lands in your shell history and
295
+ is visible to other users in the process list; rpc warns when you use it.
296
+ The same goes for tokens in `--cookie` and `--header`: pass them from an
297
+ environment variable (`--header "Authorization: Bearer $TOKEN"`), not
298
+ literally.
299
+ - **Use a test account.** Sign in as a user whose data you're happy for
300
+ screenshots (and the agent reading them) to see.
301
+
171
302
  ## MCP server
172
303
 
173
- `rpc mcp` runs an MCP server on stdio with two tools.
304
+ `rpc mcp` runs an MCP server on stdio with three tools.
174
305
 
175
306
  ### `screenshot`
176
307
 
@@ -187,6 +318,11 @@ and `--wait-for` timeouts.
187
318
  | `reducedMotion` | false | emulate `prefers-reduced-motion: reduce` |
188
319
  | `waitFor`, `waitMs`, `colorScheme`, `outDir`, `timeoutMs` | | as in the CLI |
189
320
  | `inline` | true | return images, not just paths |
321
+ | `cookies` | | `[{ name, value, domain?, path? }]`, set on the target origin |
322
+ | `headers` | | `{ "Authorization": "Bearer …" }`, sent to the target origin only |
323
+ | `storageState` | | path to a Playwright storageState JSON |
324
+ | `useSession` | true when one exists | use the session saved by `rpc login` for the origin |
325
+ | `allowOrigins` | | extra origins that also get the headers and cookies |
190
326
 
191
327
  The result starts with a plain-text block: one line per device (or the error
192
328
  and hint), followed by the JSON summary (file paths, pixel sizes,
@@ -213,13 +349,25 @@ skipped (`slices 11-26 (y=8520-21656 css px) skipped inline`), all slices are
213
349
  written to disk and the summary gives their paths and the full PNG, so the
214
350
  agent can Read the rest without slicing anything itself.
215
351
 
352
+ When auth is in play the text starts with an `auth:` line naming the
353
+ session, cookies and headers (values masked), and the JSON summary gains an
354
+ `auth` field. A capture that lands on a login page fails with
355
+ `AUTH_REQUIRED` and the `rpc login` hint. Signing in is something the user
356
+ does in a terminal (`rpc login`); the agent then just calls `screenshot`.
357
+
216
358
  ### `list_devices`
217
359
 
218
360
  Takes an optional `category` (`apple-phone`, `android-phone`, `tablet`,
219
361
  `laptop`) and returns ids, sizes and aliases.
220
362
 
363
+ ### `list_sessions`
364
+
365
+ Lists the origins with a saved session: cookie count, when it was saved and
366
+ whether it has expired. Never returns values.
367
+
221
368
  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.
369
+ directory. Claude Code and Cursor start the server in the project root; in
370
+ Codex, set `cwd` on the server to choose the directory.
223
371
 
224
372
  ### Claude Code
225
373
 
@@ -261,6 +409,25 @@ so. Add the domain to the allowlist.
261
409
  From a clone, use `"command": "node"` and
262
410
  `"args": ["/absolute/path/to/rpc-agent/dist/cli.js", "mcp"]`.
263
411
 
412
+ ### Codex
413
+
414
+ ```sh
415
+ codex mcp add rpc -- npx -y rpc-agent mcp
416
+ ```
417
+
418
+ Or add it to `~/.codex/config.toml` (or `.codex/config.toml` in a trusted
419
+ project). The Codex CLI and IDE extension read the same file:
420
+
421
+ ```toml
422
+ [mcp_servers.rpc]
423
+ command = "npx"
424
+ args = ["-y", "rpc-agent", "mcp"]
425
+ ```
426
+
427
+ From a clone, use `command = "node"` and
428
+ `args = ["/absolute/path/to/rpc-agent/dist/cli.js", "mcp"]`. `/mcp` in the
429
+ Codex TUI lists the servers it started.
430
+
264
431
  ## Development
265
432
 
266
433
  ```sh
@@ -295,7 +462,9 @@ from the `skills` CLI, are in `docs/cli-design.md`.
295
462
  - The bezel geometry is RPC's, unchanged. On kind-default phones the page
296
463
  covers the dynamic island or notch, and generic monitors are drawn edge to
297
464
  edge with only the stand showing.
298
- - No auth, cookies, custom headers or scripted interaction before capture.
465
+ - No scripted interaction before capture (beyond `rpc login --form`).
466
+ - Login-page detection goes by the URL path. An app that shows its sign-in
467
+ form at the requested URL without redirecting is captured as is.
299
468
  - Every MCP call launches a fresh Chromium, which adds about a second.
300
469
 
301
470
  ## License
package/dist/args.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { parseArgs } from 'node:util';
2
+ import { parseCookie, parseHeader, resolveAuth, toOrigin } from './auth.js';
2
3
  import { resolveDevices } from './devices.js';
3
- import { COLOR_SCHEMES, defaultShotOptions, } from './options.js';
4
- export const HELP_TOPICS = ['shot', 'devices', 'doctor', 'install', 'mcp'];
4
+ import { COLOR_SCHEMES, DEFAULT_TIMEOUT_MS, defaultShotOptions, normalizeUrl, } from './options.js';
5
+ export const HELP_TOPICS = ['shot', 'login', 'sessions', 'devices', 'doctor', 'install', 'mcp'];
5
6
  function isTopic(s) {
6
7
  return HELP_TOPICS.includes(s ?? '');
7
8
  }
@@ -15,10 +16,13 @@ export const USAGE = `rpc - screenshot a URL at device viewports, optionally in
15
16
 
16
17
  Usage:
17
18
  rpc shot <url> [options] capture screenshots
19
+ rpc login <url> [--form] sign in once and save the session for that origin
20
+ rpc sessions [list|clear [origin]]
21
+ list or delete saved sessions
18
22
  rpc devices [--json] list device ids and aliases
19
23
  rpc mcp run the MCP server on stdio
20
24
  rpc doctor [--json] check that Playwright's Chromium is installed and launches
21
- rpc install download Playwright's Chromium (same version rpc uses)
25
+ rpc install [--headed] download Playwright's Chromium (same version rpc uses)
22
26
 
23
27
  Shot options:
24
28
  -d, --devices <ids> comma-separated device ids or aliases
@@ -39,14 +43,23 @@ Shot options:
39
43
  --json print the result as JSON
40
44
  --open open the output folder when done
41
45
 
46
+ Auth options (values are never printed):
47
+ --cookie <c> "name=value[; Domain=…; Path=…]", repeatable;
48
+ sent to the target origin only
49
+ --header <h> "Name: value", repeatable; target origin only
50
+ --storage-state <file> load a Playwright storageState JSON
51
+ --no-session don't use the session saved by \`rpc login\`
52
+ --allow-origin <o> also send headers/cookies to this origin, repeatable
53
+
42
54
  Run \`rpc help <command>\` for one command's options and examples.
43
55
  Run \`rpc\` with no arguments in a terminal for an interactive capture.
44
56
 
45
57
  Examples:
46
58
  rpc shot localhost:3000 -d iphone-15,ipad,desktop-1440 --bezels
47
59
  rpc shot https://example.com --full-page --color-scheme dark
60
+ rpc login localhost:3000/sign-in && rpc shot localhost:3000/dashboard
48
61
  `;
49
- export function parseCli(argv) {
62
+ export function parseCli(argv, env = process.env) {
50
63
  const [cmd, ...rest] = argv;
51
64
  switch (cmd) {
52
65
  case undefined:
@@ -67,10 +80,16 @@ export function parseCli(argv) {
67
80
  if (wantsHelp(rest))
68
81
  return { command: 'help', topic: 'mcp' };
69
82
  return { command: 'mcp' };
70
- case 'install':
83
+ case 'install': {
71
84
  if (wantsHelp(rest))
72
85
  return { command: 'help', topic: 'install' };
73
- return { command: 'install' };
86
+ const { values } = parse(rest, { headed: { type: 'boolean' } }, false);
87
+ return values.headed ? { command: 'install', headed: true } : { command: 'install' };
88
+ }
89
+ case 'login':
90
+ return parseLogin(rest, env);
91
+ case 'sessions':
92
+ return parseSessions(rest);
74
93
  case 'doctor': {
75
94
  if (wantsHelp(rest))
76
95
  return { command: 'help', topic: 'doctor' };
@@ -108,6 +127,11 @@ function parseShot(args) {
108
127
  'reduced-motion': { type: 'boolean' },
109
128
  json: { type: 'boolean' },
110
129
  open: { type: 'boolean' },
130
+ cookie: { type: 'string', multiple: true },
131
+ header: { type: 'string', multiple: true },
132
+ 'storage-state': { type: 'string' },
133
+ 'no-session': { type: 'boolean' },
134
+ 'allow-origin': { type: 'string', multiple: true },
111
135
  }, true);
112
136
  if (positionals.length === 0)
113
137
  throw new ArgError('Missing <url>. Usage: rpc shot <url> [options]');
@@ -162,11 +186,106 @@ function parseShot(args) {
162
186
  if (typeof values['max-height'] === 'string') {
163
187
  options.maxHeight = parseInt_('--max-height', values['max-height'], 500, 200_000);
164
188
  }
189
+ const auth = {};
190
+ try {
191
+ if (values.cookie?.length)
192
+ auth.cookies = values.cookie.map(parseCookie);
193
+ if (values.header?.length)
194
+ auth.headers = Object.fromEntries(values.header.map(parseHeader));
195
+ if (typeof values['storage-state'] === 'string')
196
+ auth.storageState = values['storage-state'];
197
+ if (values['no-session'])
198
+ auth.useSession = false;
199
+ if (values['allow-origin']?.length)
200
+ auth.allowOrigins = values['allow-origin'].flatMap(splitList).map(toOrigin);
201
+ if (Object.keys(auth).length) {
202
+ resolveAuth(options.url, auth); // validate now: bad input is a usage error
203
+ options.auth = auth;
204
+ }
205
+ }
206
+ catch (err) {
207
+ throw new ArgError(err.message);
208
+ }
165
209
  return { command: 'shot', options, json: Boolean(values.json), open: Boolean(values.open) };
166
210
  }
211
+ function parseLogin(args, env) {
212
+ if (wantsHelp(args))
213
+ return { command: 'help', topic: 'login' };
214
+ const { values, positionals } = parse(args, {
215
+ save: { type: 'string' },
216
+ form: { type: 'boolean' },
217
+ email: { type: 'string' },
218
+ password: { type: 'string' },
219
+ 'email-selector': { type: 'string' },
220
+ 'password-selector': { type: 'string' },
221
+ 'submit-selector': { type: 'string' },
222
+ timeout: { type: 'string' },
223
+ json: { type: 'boolean' },
224
+ }, true);
225
+ if (positionals.length !== 1)
226
+ throw new ArgError('Usage: rpc login <url> [--form] [--save <file>]');
227
+ let url;
228
+ try {
229
+ url = normalizeUrl(positionals[0]);
230
+ toOrigin(url);
231
+ }
232
+ catch (err) {
233
+ throw new ArgError(err.message);
234
+ }
235
+ const cmd = {
236
+ command: 'login',
237
+ url,
238
+ timeoutMs: typeof values.timeout === 'string' ? parseMs('--timeout', values.timeout) : DEFAULT_TIMEOUT_MS,
239
+ json: Boolean(values.json),
240
+ };
241
+ if (typeof values.save === 'string')
242
+ cmd.save = values.save;
243
+ const formOnly = ['email', 'password', 'email-selector', 'password-selector', 'submit-selector'];
244
+ if (!values.form) {
245
+ const used = formOnly.filter(k => values[k] !== undefined);
246
+ if (used.length)
247
+ throw new ArgError(`--${used[0]} needs --form`);
248
+ return cmd;
249
+ }
250
+ const email = values.email ?? env.RPC_LOGIN_EMAIL;
251
+ const password = values.password ?? env.RPC_LOGIN_PASSWORD;
252
+ if (!email)
253
+ throw new ArgError('--form needs an email: set RPC_LOGIN_EMAIL (or pass --email)');
254
+ if (!password)
255
+ throw new ArgError('--form needs a password: set RPC_LOGIN_PASSWORD');
256
+ cmd.form = {
257
+ email,
258
+ password,
259
+ ...(values['email-selector'] ? { emailSelector: values['email-selector'] } : {}),
260
+ ...(values['password-selector'] ? { passwordSelector: values['password-selector'] } : {}),
261
+ ...(values['submit-selector'] ? { submitSelector: values['submit-selector'] } : {}),
262
+ };
263
+ if (values.password !== undefined)
264
+ cmd.passwordFromFlag = true;
265
+ return cmd;
266
+ }
267
+ function parseSessions(args) {
268
+ if (wantsHelp(args))
269
+ return { command: 'help', topic: 'sessions' };
270
+ const { values, positionals } = parse(args, { json: { type: 'boolean' } }, true);
271
+ const [action = 'list', origin, ...extra] = positionals;
272
+ if (action !== 'list' && action !== 'clear')
273
+ throw new ArgError(`Unknown sessions action "${action}". Use list or clear.`);
274
+ if (extra.length || (action === 'list' && origin))
275
+ throw new ArgError('Usage: rpc sessions list | rpc sessions clear [origin]');
276
+ if (origin) {
277
+ try {
278
+ toOrigin(origin);
279
+ }
280
+ catch (err) {
281
+ throw new ArgError(err.message);
282
+ }
283
+ }
284
+ return { command: 'sessions', action, ...(origin ? { origin } : {}), json: Boolean(values.json) };
285
+ }
167
286
  function parse(args, options, allowPositionals) {
168
287
  try {
169
- return parseArgs({ args, options: options, allowPositionals, strict: true });
288
+ return parseArgs({ args, options, allowPositionals, strict: true });
170
289
  }
171
290
  catch (err) {
172
291
  throw new ArgError(err.message);