pidge-cli 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.
Files changed (2) hide show
  1. package/bin/pidge.js +105 -3
  2. package/package.json +9 -2
package/bin/pidge.js CHANGED
@@ -66,7 +66,8 @@ const BASE = process.env.PIDGE_URL || process.env.HERALD_URL || FILE_ENV.PIDGE_U
66
66
  const TOKEN = process.env.PIDGE_TOKEN || process.env.HERALD_TOKEN || FILE_ENV.PIDGE_TOKEN;
67
67
 
68
68
  function die(msg, code = 1) { console.error(msg); process.exit(code); }
69
- if (!TOKEN) die('pidge: set PIDGE_TOKEN (env var, or put PIDGE_TOKEN=… in ~/.config/pidge/env)');
69
+ // NB: the TOKEN requirement is enforced AFTER help/usage handling (below) — a
70
+ // first-time `npx pidge-cli --help` must work without any setup.
70
71
 
71
72
  const OPTIONS = {
72
73
  help: { type: 'boolean', short: 'h' },
@@ -88,10 +89,16 @@ const OPTIONS = {
88
89
  'deliver-at': { type: 'string' },
89
90
  'reply-to': { type: 'string' },
90
91
  'correlation-id': { type: 'string' },
92
+ thread: { type: 'string' }, // conversation handle (#49) — same id ⇒ one strand on the phone
91
93
  'collapse-key': { type: 'string' },
92
94
  param: { type: 'string', multiple: true }, // key=value escape hatch → raw /notify field
93
95
  timeout: { type: 'string' },
94
96
  interval: { type: 'string' },
97
+ // inbox flags (#83)
98
+ pending: { type: 'boolean' },
99
+ summary: { type: 'boolean' },
100
+ all: { type: 'boolean' },
101
+ limit: { type: 'string' },
95
102
  };
96
103
 
97
104
  const USAGE = `pidge — send an iPhone notification to a human and block until they answer.
@@ -101,6 +108,8 @@ USAGE
101
108
  pidge notify [options] send only (prints the 201 JSON)
102
109
  pidge wait <correlation_id> [options] block on an already-sent notification
103
110
  pidge cancel <correlation_id> cancel a still-scheduled notification (#56)
111
+ pidge inbox [--pending|--summary|--all|--limit N] what you sent: list, pending slice, or counts+latency (#83)
112
+ pidge listen [--timeout N] block until the human MESSAGES you from the app, print + ack + exit (#48)
104
113
  pidge --help
105
114
 
106
115
  OPTIONS (notify / ask)
@@ -130,6 +139,8 @@ OPTIONS (notify / ask)
130
139
  --deliver-at ISO8601 schedule for later
131
140
  --reply-to URL also POST the answer to your webhook (HMAC-signed)
132
141
  --correlation-id ID idempotency + routing key (auto-generated if omitted)
142
+ --thread ID conversation handle (#49): sends sharing it group as ONE
143
+ strand on the phone — use it for follow-ups
133
144
  --collapse-key KEY replace/update a prior notification
134
145
  --param KEY=VALUE pass ANY raw /notify field (repeatable) — future server
135
146
  fields work without a CLI update; the manifest is the contract
@@ -168,6 +179,7 @@ const command = parsed.positionals[0];
168
179
  // `pidge --help` / `-h` / `help` → full help on stdout, exit 0. No command → stderr, exit 1.
169
180
  if (v.help || command === 'help') { console.log(USAGE); process.exit(0); }
170
181
  if (!command) { console.error(USAGE); process.exit(1); }
182
+ if (!TOKEN) die('pidge: set PIDGE_TOKEN (env var, or put PIDGE_TOKEN=… in ~/.config/pidge/env)');
171
183
 
172
184
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
173
185
  const headers = { authorization: `Bearer ${TOKEN}`, 'content-type': 'application/json' };
@@ -175,7 +187,7 @@ const headers = { authorization: `Bearer ${TOKEN}`, 'content-type': 'application
175
187
  // The server advertises its manifest version on every response. When it's newer
176
188
  // than what this CLI shipped knowing, nudge ONCE on stderr — the agent re-reads
177
189
  // the manifest (whats_new) and learns the new capabilities without polling.
178
- const KNOWN_MANIFEST_VERSION = 7;
190
+ const KNOWN_MANIFEST_VERSION = 11;
179
191
  let newsWarned = false;
180
192
  function checkManifestNews(res) {
181
193
  const v = parseInt(res.headers.get('x-pidge-manifest-version') || '0', 10);
@@ -202,6 +214,7 @@ function buildBody() {
202
214
  if (v['deliver-at'] !== undefined) body.deliver_at = v['deliver-at'];
203
215
  if (v['reply-to'] !== undefined) body.reply_to = v['reply-to'];
204
216
  if (v['correlation-id'] !== undefined) body.correlation_id = v['correlation-id'];
217
+ if (v.thread !== undefined) body.thread_id = v.thread;
205
218
  if (v['collapse-key'] !== undefined) body.collapse_key = v['collapse-key'];
206
219
  if (v.actions !== undefined) body.actions = v.actions.split(',').filter(Boolean);
207
220
 
@@ -319,6 +332,9 @@ async function doNotify() {
319
332
  }
320
333
  if (info.degraded)
321
334
  console.error(`pidge: DEGRADED by channel policy — ${info.degrade_reason} (delivered anyway, quieter; the human's setting, don't retry harder)`);
335
+ // #49: threads — remind the agent how to keep the conversation grouped.
336
+ if (info.thread_id)
337
+ console.error(`pidge: thread=${info.thread_id} — send follow-ups with the same --thread to group them on the phone`);
322
338
  } else {
323
339
  console.error(`pidge: send failed (${res.status}): ${raw}`);
324
340
  }
@@ -353,7 +369,9 @@ async function doWait(cid, { timeout, interval }) {
353
369
  }
354
370
  } else if (!firedNotice && data.escalation && data.escalation.state === 'fired') {
355
371
  firedNotice = true;
356
- console.error('pidge: the escalation alarm FIRED and there is still no answer — the human may have stopped the alarm on-device (that is not reported back); keep waiting or back off');
372
+ // #70: stopping the ring on-device now reports `seen` (seen_at flips);
373
+ // snoozing it is a real snoozed event this loop narrates.
374
+ console.error('pidge: the escalation alarm FIRED and there is still no answer — seen_at tells you if the human at least silenced it; keep waiting or back off');
357
375
  }
358
376
  } else if (res.status === 404) {
359
377
  console.error(`pidge: no notification for correlation_id=${cid}`);
@@ -436,6 +454,90 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
436
454
  process.exit(2);
437
455
  break;
438
456
  }
457
+ case 'inbox': {
458
+ // #83: what this channel sent — the list (default), the pending slice
459
+ // (--pending = delivered + still unanswered) or the one-call summary
460
+ // (--summary = counts + answer latency). stdout = raw server JSON.
461
+ const qs = new URLSearchParams();
462
+ if (v.all) qs.set('all', 'true');
463
+ let inboxPath = '/api/v1/inbox/summary';
464
+ if (!v.summary) {
465
+ inboxPath = '/api/v1/notifications';
466
+ if (v.pending) qs.set('pending', 'true');
467
+ if (v.limit !== undefined) qs.set('limit', v.limit);
468
+ }
469
+ let res, raw;
470
+ try {
471
+ res = await fetch(`${BASE}${inboxPath}${qs.size ? `?${qs}` : ''}`, { headers });
472
+ raw = await res.text();
473
+ } catch (e) {
474
+ die(`pidge: inbox failed (network): ${e.message}`, 2);
475
+ }
476
+ checkManifestNews(res);
477
+ console.log(raw);
478
+ if (!(res.status >= 200 && res.status < 300)) die(`pidge: inbox failed (${res.status})`, 2);
479
+ let data = {};
480
+ try { data = JSON.parse(raw); } catch { /* leave {} */ }
481
+ if (v.summary) {
482
+ const latency = data.avg_response_seconds != null
483
+ ? `, human answers in ~${Math.round(data.avg_response_seconds / 60)} min` : '';
484
+ console.error(`pidge: ${data.total} sent (${data.scope}) — ${data.pending} pending${latency}`);
485
+ } else {
486
+ const rows = data.notifications || [];
487
+ const pendingCount = rows.filter((r) => r.status === 'delivered' && !r.responded).length;
488
+ console.error(`pidge: ${rows.length} notification(s)${v.pending ? ' pending' : ` — ${pendingCount} pending`} (add --summary for counts+latency)`);
489
+ }
490
+ process.exit(0);
491
+ break;
492
+ }
493
+ case 'listen': {
494
+ // #48: block until the human messages this channel (the app's composer),
495
+ // print the messages as JSON, ACK them, exit 0. One-shot by design (loop
496
+ // it, don't daemonize) — same contract as `wait`. Exit 3 on timeout.
497
+ // At-least-once: the ack happens AFTER the print — a crash re-serves them;
498
+ // dedupe by id if you've seen one before.
499
+ const timeout = num(v.timeout, 600);
500
+ const deadline = Date.now() + timeout * 1000;
501
+ for (;;) {
502
+ const waitS = Math.max(0, Math.min(55, Math.ceil((deadline - Date.now()) / 1000)));
503
+ const askedAt = Date.now();
504
+ try {
505
+ const res = await fetch(`${BASE}/api/v1/messages?wait=${waitS}`, { headers });
506
+ checkManifestNews(res);
507
+ if (res.status === 200) {
508
+ const data = await res.json().catch(() => ({}));
509
+ const msgs = data.messages || [];
510
+ if (msgs.length) {
511
+ console.log(JSON.stringify(msgs, null, 2));
512
+ const upTo = Math.max(...msgs.map((m) => m.id));
513
+ try {
514
+ const ack = await fetch(`${BASE}/api/v1/messages/ack`, {
515
+ method: 'POST', headers, body: JSON.stringify({ up_to: upTo }),
516
+ });
517
+ if (ack.status >= 200 && ack.status < 300) {
518
+ console.error(`pidge: ${msgs.length} message(s) from the human — acked (answer via notify; reuse thread_id when present)`);
519
+ } else {
520
+ console.error(`pidge: WARNING — ack failed (${ack.status}); these messages will be re-served next listen`);
521
+ }
522
+ } catch (e) {
523
+ console.error(`pidge: WARNING — ack failed (network: ${e.message}); these messages will be re-served next listen`);
524
+ }
525
+ process.exit(0);
526
+ }
527
+ } else {
528
+ console.error(`pidge: listen error ${res.status}`);
529
+ }
530
+ } catch (e) {
531
+ console.error(`pidge: listen error (network): ${e.message}`);
532
+ }
533
+ if (Date.now() >= deadline) {
534
+ console.error(`pidge: timed out after ${timeout}s — no message from the human (not a failure)`);
535
+ process.exit(3);
536
+ }
537
+ if (Date.now() - askedAt < 2000) await sleep(num(v.interval, 5) * 1000);
538
+ }
539
+ break;
540
+ }
439
541
  default:
440
542
  die(USAGE, 1);
441
543
  }
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "pidge-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Send rich, actionable iPhone notifications to a human and block until they answer. Built for AI agents.",
5
- "keywords": ["pidge", "notifications", "agent", "cli", "push", "ai"],
5
+ "keywords": [
6
+ "pidge",
7
+ "notifications",
8
+ "agent",
9
+ "cli",
10
+ "push",
11
+ "ai"
12
+ ],
6
13
  "license": "MIT",
7
14
  "type": "commonjs",
8
15
  "bin": {