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.
- package/bin/pidge.js +105 -3
- 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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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": [
|
|
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": {
|