@pidgeon-ai/cli 2.1.0 → 2.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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.4.0
4
+
5
+ - `pidgeon agents rename <id> <name>`, `pidgeon agents revoke <id>`,
6
+ `pidgeon keys revoke <agent-id> <kid>` and `pidgeon pods list|create|remove`.
7
+ - Lists that page (`agents`, `inboxes`, `mail list`, `search`, `threads
8
+ list`, `events`) take `--limit` and `--cursor`, and end with
9
+ `More: --cursor <next>` when there is more; they were cut off silently.
10
+ - `mail list` shows sent mail by who it went to, not the inbox's address.
11
+ - `agents create` and `inboxes create` take `--pod`.
12
+ - `keys proof` takes `--name`, so the key file names its agent.
13
+ - Signed requests carry a nonce and cover the query (SDK 2.4.0).
14
+
15
+ ## 2.3.0
16
+
17
+ - `pidgeon webhooks create --inbox <id>...` takes several inboxes.
18
+ - `--days` must be a whole number of days; `abc` used to ask for 30.
19
+ - The help lists `--cc` and `--bcc`, and that `drafts create` takes the
20
+ same options as `send`.
21
+
22
+ ## 2.2.0
23
+
24
+ - `pidgeon webhooks create --inbox <id>`: the endpoint hears only that
25
+ inbox's events; `webhooks list` shows an endpoint's inboxes.
26
+
3
27
  ## 2.1.0
4
28
 
5
29
  - Email: `pidgeon mail list|read|wait`, `pidgeon search`, `pidgeon threads
package/README.md CHANGED
@@ -17,6 +17,9 @@ or `PIDGEON_AGENT_KEY` (the file's `private_jwk`) and `PIDGEON_DIRECTORY`.
17
17
 
18
18
  ```sh
19
19
  pidgeon agents show <agent-id> # its keys, and how websites see it
20
+ pidgeon agents rename <agent-id> "Table agent"
21
+ pidgeon keys revoke <agent-id> <kid> # requests signed with it stop verifying
22
+ pidgeon agents revoke <agent-id> # it and every key; cannot be undone
20
23
  pidgeon inboxes create bookings --agent <agent-id>
21
24
  pidgeon send <inbox-id> --to ana@example.com --subject "Table for two" --text "Friday at 8?"
22
25
  pidgeon drafts create <inbox-id> --to ana@example.com --subject "…" --text "…"
@@ -28,10 +31,13 @@ pidgeon app-passwords create <inbox-id> "Apple Mail"
28
31
  pidgeon sites create Shop --redirect https://shop.example/callback
29
32
  pidgeon reports create <verification-id> --category spam --evidence "…"
30
33
  pidgeon events
34
+ pidgeon pods list | create <name> | remove <pod-id>
31
35
  pidgeon api GET /v1/email/rules # any /v1 call
32
36
  ```
33
37
 
34
38
  `pidgeon --help` lists every command; `--json` prints the API's own answer.
39
+ A list prints a page at a time (`--limit`); when there is more, its last line
40
+ is the `--cursor` that reads the next page.
35
41
 
36
42
  Signing in opens the browser once; the session is kept in
37
43
  `~/.config/pidgeon/credentials.json` and refreshed. On a machine with no
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "2.1.0";
1
+ export declare const VERSION = "2.4.0";
2
2
  export type Io = {
3
3
  out: (line: string) => void;
4
4
  err: (line: string) => void;
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@
9
9
  import { parseArgs } from 'node:util';
10
10
  import { PidgeonError } from '@pidgeon-ai/sdk';
11
11
  import { addAgentKey, baseUrlOf, clientFromEnvironment, listAgentKeys, makeKeyProof, readSession, setUpAgent, signIn, signOut, } from '@pidgeon-ai/sdk/node';
12
- export const VERSION = '2.1.0';
12
+ export const VERSION = '2.4.0';
13
13
  const HELP = `pidgeon ${VERSION}: Pidgeon from a terminal.
14
14
 
15
15
  Signing in
@@ -19,14 +19,17 @@ Signing in
19
19
  Or set PIDGEON_API_KEY; it wins over a session. PIDGEON_BASE_URL points elsewhere.
20
20
 
21
21
  Agents (Passport)
22
- pidgeon agents list
23
- pidgeon agents create <name> [--purpose <text>]
22
+ pidgeon agents list [--limit <n>] [--cursor <cursor>]
23
+ pidgeon agents create <name> [--purpose <text>] [--pod <pod-id>]
24
24
  Registers the agent, makes its key on this machine, publishes the public
25
25
  half signed with it, and keeps the key here (pidgeon keys list).
26
26
  pidgeon agents show <agent-id> its keys and its operator's reputation
27
+ pidgeon agents rename <agent-id> <name> [--purpose <text>]
28
+ pidgeon agents revoke <agent-id> it and every key it has; cannot be undone
27
29
  pidgeon keys create <agent-id> a new key for it, made here
28
30
  pidgeon keys list the keys this machine keeps
29
- pidgeon keys proof <agent-id> --directory <url> --url <publish-url>
31
+ pidgeon keys revoke <agent-id> <kid> requests signed with it stop verifying
32
+ pidgeon keys proof <agent-id> --directory <url> --url <publish-url> [--name <agent name>]
30
33
  For the hosted MCP server: makes the key here, keeps it, and prints the
31
34
  public half and the signed proof that publishing needs. No sign-in.
32
35
 
@@ -41,19 +44,21 @@ Sign in with Pidgeon (Auth)
41
44
  pidgeon mandates request --agent <id> --client <client-id> --scope <scope> --description <text> [--days <n>]
42
45
 
43
46
  Email
44
- pidgeon inboxes list
45
- pidgeon inboxes create <name> [--kind agent|events] [--agent <id>] [--domain <your domain>]
47
+ pidgeon inboxes list [--limit <n>] [--cursor <cursor>]
48
+ pidgeon inboxes create <name> [--kind agent|events] [--agent <id>] [--domain <your domain>] [--pod <pod-id>]
46
49
  pidgeon inboxes remove <inbox-id>
47
- pidgeon send <inbox-id> --to <address>... --subject <text> (--text <body> | --html <body>) [--reply-to <message-id>]
48
- pidgeon drafts create <inbox-id> --to <address>... --subject <text> --text <body>
50
+ pidgeon send <inbox-id> --to <address>... [--cc <address>...] [--bcc <address>...] --subject <text>
51
+ (--text <body> | --html <body>) [--reply-to <message-id>]
52
+ pidgeon drafts create <inbox-id> the same options as send
49
53
  pidgeon drafts show|remove <draft-id>
50
54
  pidgeon drafts send <draft-id> [--at <ISO time>]
51
55
  pidgeon drafts approval <draft-id> hold it for a person to send or reject
52
- pidgeon mail list <inbox-id> the inbox's mail, newest first
56
+ pidgeon mail list <inbox-id> [--limit <n>] [--cursor <cursor>] the inbox's mail, newest first
53
57
  pidgeon mail read <message-id> [--original] cleaned by the Firewall unless --original
54
58
  pidgeon mail wait <inbox-id> [--since <time|message-id>] [--timeout <seconds>]
55
- pidgeon search <words> [--inbox <inbox-id>]
59
+ pidgeon search <words> [--inbox <inbox-id>] [--limit <n>] [--cursor <cursor>]
56
60
  pidgeon threads list [--inbox <inbox-id>] [--folder inbox|archive|spam|trash]
61
+ [--limit <n>] [--cursor <cursor>]
57
62
  pidgeon threads show <thread-id>
58
63
  pidgeon threads file <thread-id> --folder inbox|archive|spam|trash
59
64
  pidgeon threads hand-over <thread-id> to a person: owner human, status human_review
@@ -61,10 +66,14 @@ Email
61
66
  pidgeon app-passwords list <inbox-id> | revoke <app-password-id>
62
67
 
63
68
  The account
64
- pidgeon events [--type <type>] [--limit <n>]
65
- pidgeon webhooks list | create <url> --event <type>... | remove <webhook-id>
69
+ pidgeon events [--type <type>] [--limit <n>] [--cursor <cursor>]
70
+ pidgeon pods list | create <name> | remove <pod-id>
71
+ pidgeon webhooks list | create <url> --event <type>... [--inbox <id>...] | remove <webhook-id>
66
72
  pidgeon api <GET|POST|PATCH|DELETE> <path> [--data <json>] any /v1 call
67
73
 
74
+ A list prints a page at a time; when there is more, its last line is the
75
+ --cursor that reads the next page.
76
+
68
77
  Add --json to any command for the API's own JSON.`;
69
78
  const OPTIONS = {
70
79
  json: { type: 'boolean' },
@@ -92,7 +101,7 @@ const OPTIONS = {
92
101
  original: { type: 'boolean' },
93
102
  since: { type: 'string' },
94
103
  timeout: { type: 'string' },
95
- inbox: { type: 'string' },
104
+ inbox: { type: 'string', multiple: true },
96
105
  folder: { type: 'string' },
97
106
  event: { type: 'string', multiple: true },
98
107
  method: { type: 'string' },
@@ -104,6 +113,8 @@ const OPTIONS = {
104
113
  client: { type: 'string' },
105
114
  description: { type: 'string' },
106
115
  days: { type: 'string' },
116
+ name: { type: 'string' },
117
+ cursor: { type: 'string' },
107
118
  };
108
119
  class Usage extends Error {
109
120
  }
@@ -148,6 +159,18 @@ function need(value, what) {
148
159
  throw new Usage(`Say which ${what}.`);
149
160
  return value;
150
161
  }
162
+ /** An option given at most once. */
163
+ function one(values, flag) {
164
+ if (values && values.length > 1)
165
+ throw new Usage(`Give ${flag} once here.`);
166
+ return values?.[0];
167
+ }
168
+ function wholeDays(value) {
169
+ const days = Number(value);
170
+ if (!Number.isInteger(days) || days < 1)
171
+ throw new Usage('--days is a whole number of days.');
172
+ return days;
173
+ }
151
174
  function message(values) {
152
175
  if (!values.to?.length)
153
176
  throw new Usage('Send it to someone: --to <address>.');
@@ -176,6 +199,10 @@ async function dispatch(positionals, values, io) {
176
199
  for (const line of plain())
177
200
  io.out(line);
178
201
  };
202
+ /** A page's lines, and how to read the next page when there is one. */
203
+ const paged = (page, lines) => page.next_cursor ? [...lines, `More: --cursor ${page.next_cursor}`] : lines;
204
+ const cursor = values.cursor;
205
+ const limit = values.limit ? Number(values.limit) : undefined;
179
206
  switch (command) {
180
207
  case 'login': {
181
208
  const scope = (values.scope ?? 'admin');
@@ -222,14 +249,14 @@ async function dispatch(positionals, values, io) {
222
249
  case 'agents': {
223
250
  const pidgeon = client();
224
251
  if (sub === 'list') {
225
- const page = await pidgeon.agents.list();
252
+ const page = await pidgeon.agents.list({ cursor, limit });
226
253
  show(page, () => page.data.length
227
- ? page.data.map((a) => `${a.id} ${a.status.padEnd(7)} ${a.name}`)
254
+ ? paged(page, page.data.map((a) => `${a.id} ${a.status.padEnd(7)} ${a.name}`))
228
255
  : ['No agents yet: pidgeon agents create <name>.']);
229
256
  }
230
257
  else if (sub === 'create') {
231
258
  const name = need(arg, 'name to give the agent');
232
- const made = await setUpAgent(pidgeon, { name, purpose: values.purpose }, io.env);
259
+ const made = await setUpAgent(pidgeon, { name, purpose: values.purpose, podId: values.pod }, io.env);
233
260
  show({ agent: made.agent, kid: made.kid, key_file: made.keyFile }, () => [
234
261
  `Registered ${made.agent.name} (${made.agent.id}) and published its key ${made.kid}.`,
235
262
  `Its private key is in ${made.keyFile}, on this machine only.`,
@@ -250,8 +277,25 @@ async function dispatch(positionals, values, io) {
250
277
  ...(agent.reputation.next ? [agent.reputation.next] : []),
251
278
  ]);
252
279
  }
280
+ else if (sub === 'rename') {
281
+ const id = need(arg, 'agent (its id)');
282
+ const name = positionals.slice(3).join(' ') || undefined;
283
+ if (!name && values.purpose === undefined)
284
+ throw new Usage('pidgeon agents rename <agent-id> <name> [--purpose <text>]');
285
+ const agent = await pidgeon.agents.update(id, { name, purpose: values.purpose });
286
+ show(agent, () => [
287
+ `${agent.name} (${agent.id})${agent.purpose ? `: ${agent.purpose}` : ''}`,
288
+ ]);
289
+ }
290
+ else if (sub === 'revoke') {
291
+ const id = need(arg, 'agent (its id)');
292
+ await pidgeon.agents.revoke(id);
293
+ show({ revoked: id }, () => [
294
+ 'Revoked, with every key it had. No request it signs verifies again.',
295
+ ]);
296
+ }
253
297
  else
254
- throw new Usage('pidgeon agents list | create <name> | show <id>');
298
+ throw new Usage('pidgeon agents list | create <name> | show <id> | rename <id> <name> | revoke <id>');
255
299
  return;
256
300
  }
257
301
  case 'keys': {
@@ -271,20 +315,26 @@ async function dispatch(positionals, values, io) {
271
315
  const agentId = need(arg, 'agent (its id)');
272
316
  const directory = need(values.directory, 'directory (--directory)');
273
317
  const publishUrl = values.url ?? `${baseUrl}/v1/agents/${encodeURIComponent(agentId)}/keys`;
274
- const proof = await makeKeyProof({ agentId, directory, publishUrl }, io.env);
318
+ const proof = await makeKeyProof({ agentId, directory, publishUrl, name: values.name }, io.env);
275
319
  // Always JSON: it is for a coding agent to hand to publish_agent_key.
276
320
  io.out(JSON.stringify(proof, null, 2));
277
321
  }
322
+ else if (sub === 'revoke') {
323
+ const agentId = need(arg, 'agent (its id)');
324
+ const kid = need(positionals[3], 'key (its kid: pidgeon agents show <agent-id>)');
325
+ await client().agents.revokeKey(agentId, kid);
326
+ show({ revoked: kid }, () => [`Revoked ${kid}. Requests signed with it stop verifying.`]);
327
+ }
278
328
  else
279
- throw new Usage('pidgeon keys create <agent-id> | list | proof <agent-id>');
329
+ throw new Usage('pidgeon keys create <agent-id> | list | proof <agent-id> | revoke <agent-id> <kid>');
280
330
  return;
281
331
  }
282
332
  case 'inboxes': {
283
333
  const pidgeon = client();
284
334
  if (sub === 'list') {
285
- const page = await pidgeon.inboxes.list();
335
+ const page = await pidgeon.inboxes.list({ cursor, limit });
286
336
  show(page, () => page.data.length
287
- ? page.data.map((i) => `${i.id} ${i.kind.padEnd(6)} ${i.address}`)
337
+ ? paged(page, page.data.map((i) => `${i.id} ${i.kind.padEnd(6)} ${i.address}`))
288
338
  : ['No inboxes yet.']);
289
339
  }
290
340
  else if (sub === 'create') {
@@ -294,6 +344,7 @@ async function dispatch(positionals, values, io) {
294
344
  kind,
295
345
  agentId: values.agent,
296
346
  domain: values.domain,
347
+ podId: values.pod,
297
348
  });
298
349
  show(inbox, () => [`Made ${inbox.address} (${inbox.id}).`]);
299
350
  }
@@ -430,7 +481,7 @@ async function dispatch(positionals, values, io) {
430
481
  clientId: need(values.client, 'site (--client, its client_id)'),
431
482
  scope: need(values.scope, 'scope (--scope)'),
432
483
  description: need(values.description, 'what for (--description)'),
433
- days: values.days ? Number(values.days) : undefined,
484
+ days: values.days === undefined ? undefined : wholeDays(values.days),
434
485
  });
435
486
  show(made, () => [`Send the person this link to allow or decline: ${made.consent_url}`]);
436
487
  }
@@ -444,10 +495,12 @@ async function dispatch(positionals, values, io) {
444
495
  }
445
496
  case 'mail': {
446
497
  const pidgeon = client();
447
- const line = (m) => `${m.id} ${m.created_at.slice(0, 16)} ${m.direction === 'in' ? '←' : '→'} ${m.from.address} ${m.is_held ? '[held for a person]' : (m.subject ?? '')}${m.direction === 'in' && !m.seen ? ' (unread)' : ''}`;
498
+ const line = (m) =>
499
+ // Received mail names who sent it; sent mail, who it went to.
500
+ `${m.id} ${m.created_at.slice(0, 16)} ${m.direction === 'in' ? `← ${m.from.address}` : `→ ${m.to.join(', ')}`} ${m.is_held ? '[held for a person]' : (m.subject ?? '')}${m.direction === 'in' && !m.seen ? ' (unread)' : ''}`;
448
501
  if (sub === 'list') {
449
- const page = await pidgeon.messages.list(need(arg, 'inbox (its id)'));
450
- show(page, () => (page.data.length ? page.data.map(line) : ['No mail yet.']));
502
+ const page = await pidgeon.messages.list(need(arg, 'inbox (its id)'), { cursor, limit });
503
+ show(page, () => (page.data.length ? paged(page, page.data.map(line)) : ['No mail yet.']));
451
504
  }
452
505
  else if (sub === 'read') {
453
506
  const m = await pidgeon.messages.get(need(arg, 'message (its id)'), {
@@ -487,10 +540,12 @@ async function dispatch(positionals, values, io) {
487
540
  case 'search': {
488
541
  const words = positionals.slice(1).join(' ');
489
542
  const page = await client().messages.search(need(words || undefined, 'words to look for'), {
490
- inboxId: values.inbox,
543
+ inboxId: one(values.inbox, '--inbox'),
544
+ cursor,
545
+ limit,
491
546
  });
492
547
  show(page, () => page.data.length
493
- ? page.data.map((m) => `${m.id} ${m.created_at.slice(0, 10)} ${m.from.address} ${m.subject ?? ''}`)
548
+ ? paged(page, page.data.map((m) => `${m.id} ${m.created_at.slice(0, 10)} ${m.from.address} ${m.subject ?? ''}`))
494
549
  : ['Nothing found.']);
495
550
  return;
496
551
  }
@@ -501,11 +556,13 @@ async function dispatch(positionals, values, io) {
501
556
  if (values.folder && !folders.includes(values.folder))
502
557
  throw new Usage('--folder is inbox, archive, spam or trash.');
503
558
  const page = await pidgeon.threads.list({
504
- inboxId: values.inbox,
559
+ inboxId: one(values.inbox, '--inbox'),
505
560
  folder: values.folder,
561
+ cursor,
562
+ limit,
506
563
  });
507
564
  show(page, () => page.data.length
508
- ? page.data.map((t) => `${t.id} ${t.folder.padEnd(7)} ${t.unread_count ? `${t.unread_count} unread ` : ''}${t.subject ?? '(held)'}`)
565
+ ? paged(page, page.data.map((t) => `${t.id} ${t.folder.padEnd(7)} ${t.unread_count ? `${t.unread_count} unread ` : ''}${t.subject ?? '(held)'}`))
509
566
  : ['No conversations.']);
510
567
  }
511
568
  else if (sub === 'show') {
@@ -562,13 +619,15 @@ async function dispatch(positionals, values, io) {
562
619
  if (sub === 'list') {
563
620
  const list = await pidgeon.webhooks.list();
564
621
  show(list, () => list.data.length
565
- ? list.data.map((h) => `${h.id} ${h.url} ${h.events.join(', ')}`)
622
+ ? list.data.map((h) => `${h.id} ${h.url} ${h.events.join(', ')}${h.inbox_ids ? ` (inboxes: ${h.inbox_ids.join(', ')})` : ''}`)
566
623
  : ['No webhooks.']);
567
624
  }
568
625
  else if (sub === 'create') {
569
626
  const made = await pidgeon.webhooks.create({
570
627
  url: need(arg, 'url (https)'),
571
- events: values.event ?? [],
628
+ // The API refuses, by name, an event webhooks do not carry.
629
+ events: (values.event ?? []),
630
+ ...(values.inbox?.length ? { inboxIds: values.inbox } : {}),
572
631
  });
573
632
  show(made, () => [`Made ${made.id}. Secret, shown once: ${made.secret}`]);
574
633
  }
@@ -577,19 +636,38 @@ async function dispatch(positionals, values, io) {
577
636
  show({ removed: arg }, () => ['Removed.']);
578
637
  }
579
638
  else
580
- throw new Usage('pidgeon webhooks list | create <url> --event <type>... | remove <id>');
639
+ throw new Usage('pidgeon webhooks list | create <url> --event <type>... [--inbox <id>...] | remove <id>');
581
640
  return;
582
641
  }
583
642
  case 'events': {
584
643
  const page = await client().events.list({
585
644
  type: values.type,
586
- limit: values.limit ? Number(values.limit) : undefined,
645
+ limit,
646
+ cursor,
587
647
  });
588
648
  show(page, () => page.data.length
589
- ? page.data.map((e) => `${e.created_at} ${e.type}`)
649
+ ? paged(page, page.data.map((e) => `${e.created_at} ${e.type}`))
590
650
  : ['Nothing has happened yet.']);
591
651
  return;
592
652
  }
653
+ case 'pods': {
654
+ const pidgeon = client();
655
+ if (sub === 'list') {
656
+ const list = await pidgeon.pods.list();
657
+ show(list, () => list.data.map((p) => `${p.id} ${p.name}${p.default ? ' (default)' : ''}`));
658
+ }
659
+ else if (sub === 'create') {
660
+ const pod = await pidgeon.pods.create(need(positionals.slice(2).join(' ') || undefined, 'name'));
661
+ show(pod, () => [`Made ${pod.name} (${pod.id}).`]);
662
+ }
663
+ else if (sub === 'remove') {
664
+ await pidgeon.pods.remove(need(arg, 'pod (its id)'));
665
+ show({ removed: arg }, () => ['Removed.']);
666
+ }
667
+ else
668
+ throw new Usage('pidgeon pods list | create <name> | remove <pod-id>');
669
+ return;
670
+ }
593
671
  case 'api': {
594
672
  const method = (sub ?? '').toUpperCase();
595
673
  if (!['GET', 'POST', 'PATCH', 'DELETE'].includes(method) || !arg?.startsWith('/v1/'))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pidgeon-ai/cli",
3
- "version": "2.1.0",
3
+ "version": "2.4.0",
4
4
  "description": "Pidgeon from a terminal: sign in, set agents up with keys made on your machine, and send from their inboxes.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  "LICENSE"
37
37
  ],
38
38
  "dependencies": {
39
- "@pidgeon-ai/sdk": "^2.1.1"
39
+ "@pidgeon-ai/sdk": "^2.4.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^24.10.1",