pidge-cli 0.11.1 → 0.13.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 +49 -0
- package/README.md +14 -3
- package/bin/pidge.js +410 -37
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.13.0 — 2026-06-25
|
|
4
|
+
|
|
5
|
+
Template system (#246) — the agent now declares an intent TYPE; the server maps it to
|
|
6
|
+
the human's delivery profile (the human never sees the type). Soft-rollout: typeless
|
|
7
|
+
sends still work in 0.13.x (server falls back to `fyi`); 0.14 will require a type.
|
|
8
|
+
|
|
9
|
+
- **feat:** 6 type subcommands — `pidge fyi` · `report` · `ask` · `event` · `alert` ·
|
|
10
|
+
`live`. Each stamps `template_kind` on the `/notify` payload. fyi/report/event/alert/
|
|
11
|
+
live are fire-and-forget (like `notify`); `ask` send+waits for the answer. (#246)
|
|
12
|
+
- **feat:** `pidge notify` (and `pidge send`) is **deprecated** with a local warning —
|
|
13
|
+
it still works for one minor (0.13.x; the server falls back to `fyi`) and will 422 in
|
|
14
|
+
0.14. Use a typed send instead. (#246)
|
|
15
|
+
- **feat:** local validation before the round-trip — `ask` requires a way to answer
|
|
16
|
+
(`--actions`, `--custom-action`, or a `--template` that supplies them); `event`
|
|
17
|
+
requires a valid ISO8601 `--event-at`. Friendly exit-1 errors, nothing sent. (#246)
|
|
18
|
+
- **feat:** `alert --escalate` adds `escalate: true` (ask the Urgente profile for an
|
|
19
|
+
AlarmKit alarm that breaks through silent/Focus; the human's profile decides). (#246)
|
|
20
|
+
- **feat:** an unknown subcommand now points at the type catalog instead of dumping the
|
|
21
|
+
whole USAGE; `pidge <type> --help` shows each typed send's own flags. (#246)
|
|
22
|
+
- **docs:** `pidge skill install` writes a **"Choose the right type"** catalog table
|
|
23
|
+
into the generated `SKILL.md`. (#246)
|
|
24
|
+
|
|
25
|
+
## 0.12.0 — 2026-06-25
|
|
26
|
+
|
|
27
|
+
CLI bugs batch, all reported by an agent in real production use. No breaking changes.
|
|
28
|
+
|
|
29
|
+
- **fix:** `pidge <sub> --help` shows the SUBCOMMAND's own help (its synopsis + own
|
|
30
|
+
flags), not the global USAGE dump — e.g. `pidge ask --help` now leads with ask's
|
|
31
|
+
`--actions`/`--timeout` instead of burying them. `pidge --help` and `pidge help`
|
|
32
|
+
still show the full command overview; `pidge help <cmd>` is the focused form. (#240)
|
|
33
|
+
- **feat:** the "server has new capabilities" manifest-version nag is throttled to
|
|
34
|
+
**once per 24 h** (cached in `~/.config/pidge/state.json`, per-agent when
|
|
35
|
+
`PIDGE_AGENT` is set) and only re-fires when the server version actually changed —
|
|
36
|
+
no more a nag on every call. New `--quiet-nag` flag and `PIDGE_QUIET_NAG=1` env to
|
|
37
|
+
silence it entirely (scripts/CI). (#241)
|
|
38
|
+
- **feat:** `--actions` accepts a **JSON array** of custom `{id,label,…}` actions for
|
|
39
|
+
custom labels — `--actions '[{"id":"approve","label":"Aprovar agora"},{"id":"defer","label":"Deixa pra amanhã"}]'`.
|
|
40
|
+
A leading `[` selects JSON; bad JSON / a missing `id`/`label` is a friendly LOCAL
|
|
41
|
+
error (exit 1, nothing sent). The short form `--actions yes,no,reply` is unchanged,
|
|
42
|
+
and JSON composes with `--custom-action`. (#242)
|
|
43
|
+
- **docs:** the manifest re-read instruction (the version nag) now shows the
|
|
44
|
+
**authenticated** curl — `curl -H "Authorization: Bearer $PIDGE_TOKEN" $PIDGE_URL/api/v1/manifest`
|
|
45
|
+
— so an agent that follows it doesn't take a 401. (#243)
|
|
46
|
+
- **docs:** `pidge skill install` now writes an **"always-on for turn-based agents"**
|
|
47
|
+
recipe into the generated `SKILL.md` — an interactive listening window
|
|
48
|
+
(`pidge listen --follow`) and a no-daemon supervisor poll (looped one-shot
|
|
49
|
+
`pidge listen`). (#244)
|
package/README.md
CHANGED
|
@@ -11,6 +11,15 @@ then gets the answer as JSON — no webhook, no polling loop to write.
|
|
|
11
11
|
> current spec (fields, profiles, guarantees). This CLI is a thin pipe over it — any
|
|
12
12
|
> new server field works without a CLI update via `--param key=value`.
|
|
13
13
|
|
|
14
|
+
> **New in v0.12.0** — CLI bugs batch (all reported by an agent in real use): **`pidge
|
|
15
|
+
> <sub> --help`** now shows that subcommand's own help (its flags), not the global dump
|
|
16
|
+
> (#240); the **manifest-version nag is throttled to once / 24 h** (cached in
|
|
17
|
+
> `~/.config/pidge/state.json`) with `--quiet-nag` / `PIDGE_QUIET_NAG=1` to silence it
|
|
18
|
+
> (#241); **`--actions` accepts a JSON array** of custom `{id,label}` actions for custom
|
|
19
|
+
> labels (#242, the short `yes,no,reply` form unchanged); the nag's manifest re-read
|
|
20
|
+
> example now shows the **`Authorization: Bearer`** header so it doesn't 401 (#243); and
|
|
21
|
+
> `skill install` writes an **"always-on for turn-based agents"** recipe (#244).
|
|
22
|
+
>
|
|
14
23
|
> **New in v0.9.0** (ships with Pidge manifest v27): **`listen` no longer consumes on
|
|
15
24
|
> read** — a read message is DELIVERED, and you `ack` it after the work (a ~10-min
|
|
16
25
|
> server lease re-serves un-acked messages, so a crash never loses one; `--ack-on-read`
|
|
@@ -192,10 +201,12 @@ WebSocket → ?wait= long-poll (capped 25 s server-side) → plain GETs ever
|
|
|
192
201
|
and saves on the phone; uploaded automatically (≤25 MB)
|
|
193
202
|
--url URL deep link the app opens when the user taps (PR, dashboard, log)
|
|
194
203
|
--copy TEXT value offered as tap-to-copy on the detail (code, token)
|
|
195
|
-
--actions LIST
|
|
196
|
-
done,snooze,reschedule,reply,mute
|
|
204
|
+
--actions LIST|JSON comma list of catalog ids: yes,no,approve,reject,accept,
|
|
205
|
+
decline,later,done,snooze,reschedule,reply,mute — OR a JSON
|
|
206
|
+
array of custom actions for your own labels:
|
|
207
|
+
'[{"id":"approve","label":"Aprovar agora"},{"id":"defer","label":"Depois"}]'
|
|
197
208
|
--custom-action SPEC "id:label[:destructive][:confirm][:biometric][:terminal]"
|
|
198
|
-
(repeatable — your own buttons)
|
|
209
|
+
(repeatable — your own buttons; composes with --actions JSON)
|
|
199
210
|
--deliver-at ISO8601 schedule for later
|
|
200
211
|
--reply-to URL also POST the answer to your webhook (HMAC-signed)
|
|
201
212
|
--correlation-id ID idempotency + routing key (auto-generated if omitted)
|
package/bin/pidge.js
CHANGED
|
@@ -103,6 +103,7 @@ const OPTIONS = {
|
|
|
103
103
|
'event-at': { type: 'string' }, // WHEN the thing happens (profile event)
|
|
104
104
|
'lead-minutes': { type: 'string' }, // notify/countdown lead before event_at
|
|
105
105
|
urgency: { type: 'string' }, // normal | persistent | alarm (low-level — prefer --profile)
|
|
106
|
+
escalate: { type: 'boolean' }, // #246: alert type — force an AlarmKit alarm (escalate:true)
|
|
106
107
|
image: { type: 'string' }, // banner+feed image: local path → uploaded; URL → as-is
|
|
107
108
|
file: { type: 'string' }, // real artifact (xlsx/pdf/csv…): local path → uploaded
|
|
108
109
|
url: { type: 'string' }, // deep link the app opens on tap (#45)
|
|
@@ -126,6 +127,7 @@ const OPTIONS = {
|
|
|
126
127
|
// realtime (#118): WS by default when the runtime has a WebSocket (Node ≥22)
|
|
127
128
|
realtime: { type: 'boolean' }, // force WS (warn+fallback if unavailable)
|
|
128
129
|
'no-realtime': { type: 'boolean' }, // polling only
|
|
130
|
+
'quiet-nag': { type: 'boolean' }, // #241: silence the manifest-version nag for this run
|
|
129
131
|
// onboarding v2 (#110)
|
|
130
132
|
claim: { type: 'string' }, // setup --claim <single-use code>
|
|
131
133
|
// #157 P2: listen keeps going after a batch (supervisor loop, one process)
|
|
@@ -161,8 +163,14 @@ USAGE
|
|
|
161
163
|
narrated LIVE on the lock screen by a 3-stage Live Activity
|
|
162
164
|
(Conectando → toque para confirmar → Concluído ✓). send + wait
|
|
163
165
|
in one — run it as your FIRST contact on a fresh channel.
|
|
164
|
-
|
|
165
|
-
pidge
|
|
166
|
+
TYPED SENDS (#246 — pick the one that matches your INTENT):
|
|
167
|
+
pidge fyi [options] passive info, no action — log/registro (template_kind fyi)
|
|
168
|
+
pidge report [options] a curated result the human will want to read now (report)
|
|
169
|
+
pidge ask [options] a DECISION — send AND wait; needs --actions (prints chosen_action JSON)
|
|
170
|
+
pidge event [options] a scheduled thing with a time — needs --event-at (event)
|
|
171
|
+
pidge alert [options] an anomaly/error; --escalate forces an AlarmKit alarm (alert)
|
|
172
|
+
pidge live [options] an in-flight task with incremental updates (live)
|
|
173
|
+
pidge notify [options] DEPRECATED (0.13.x) — send without a type (server falls back to fyi)
|
|
166
174
|
pidge wait <correlation_id> [options] block on an already-sent notification
|
|
167
175
|
pidge cancel <correlation_id> cancel a still-scheduled notification (#56)
|
|
168
176
|
pidge inbox [--pending|--summary|--all|--limit N] what you sent: list, pending slice, or counts+latency (#83)
|
|
@@ -268,6 +276,191 @@ only: it never produces an answer, so \`ask\` refuses it.
|
|
|
268
276
|
|
|
269
277
|
Full spec (the contract — always current): GET $PIDGE_URL/api/v1/manifest`;
|
|
270
278
|
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// #240: per-subcommand help. `pidge <cmd> --help` (and `pidge help <cmd>`) must
|
|
281
|
+
// show the focused help for THAT command — its synopsis, what it does, and only
|
|
282
|
+
// the flags that apply — instead of dumping the global USAGE (the bug an agent
|
|
283
|
+
// hit: `pidge ask --help` listed the global flags, burying ask's own
|
|
284
|
+
// --actions/--timeout). The global USAGE stays the no-command / `pidge --help`
|
|
285
|
+
// view. One option dictionary feeds both so the text can't drift.
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
const OPTION_DOCS = {
|
|
288
|
+
title: '--title TEXT (required) the headline',
|
|
289
|
+
body: '--body TEXT the message shown on the banner',
|
|
290
|
+
'body-markdown': '--body-markdown MD rich body for the tap-through detail screen',
|
|
291
|
+
subtitle: '--subtitle TEXT a secondary line under the title',
|
|
292
|
+
template: '--template ID content/action pattern: context · decision · approval · reminder · nudge · sensitive',
|
|
293
|
+
profile: '--profile ID delivery profile (the human owns it): default · event · escalating · custom',
|
|
294
|
+
'event-at': '--event-at ISO8601 WHEN the thing happens (required by profile event)',
|
|
295
|
+
'lead-minutes': '--lead-minutes N notify/countdown N min before event_at (5–240)',
|
|
296
|
+
urgency: '--urgency LEVEL normal | persistent | alarm (low-level — prefer --profile)',
|
|
297
|
+
escalate: '--escalate alert: force an AlarmKit alarm that breaks through silent/Focus',
|
|
298
|
+
image: '--image PATH_OR_URL banner+feed image: a local path is uploaded; an https URL is sent as-is',
|
|
299
|
+
file: '--file PATH a real artifact (xlsx/pdf/csv…) uploaded for the human (≤25 MB)',
|
|
300
|
+
url: '--url URL deep link the app opens on tap (PR, dashboard, log)',
|
|
301
|
+
copy: '--copy TEXT tap-to-copy value on the detail screen',
|
|
302
|
+
actions: '--actions LIST|JSON comma list from the catalog (yes,no,reply) OR a JSON array of {"id","label"} custom actions',
|
|
303
|
+
'custom-action': '--custom-action SPEC "id:label[:destructive][:confirm][:biometric][:terminal]" (repeatable)',
|
|
304
|
+
'deliver-at': '--deliver-at ISO8601 schedule the send for later',
|
|
305
|
+
'reply-to': '--reply-to URL also POST the answer to your webhook (HMAC-signed)',
|
|
306
|
+
'correlation-id': '--correlation-id ID idempotency + routing key (auto-generated if omitted)',
|
|
307
|
+
thread: '--thread ID conversation handle (#49): same id ⇒ one strand on the phone',
|
|
308
|
+
after: '--after CID decision queue (#157): held until that notification is answered',
|
|
309
|
+
'collapse-key': '--collapse-key KEY replace/update a prior notification',
|
|
310
|
+
param: '--param KEY=VALUE pass ANY raw /notify field (repeatable) — the manifest is the contract',
|
|
311
|
+
timeout: '--timeout SECONDS how long to block (ask: 600 · wait: 300 · listen: 600)',
|
|
312
|
+
interval: '--interval SECONDS FALLBACK poll cadence (default 30) — normally unused (WS/long-poll)',
|
|
313
|
+
realtime: '--realtime force the realtime WebSocket (warn + fall back to polling if unavailable)',
|
|
314
|
+
'no-realtime': '--no-realtime polling only (skip the WebSocket)',
|
|
315
|
+
pending: '--pending only delivered + still-unanswered notifications',
|
|
316
|
+
summary: '--summary counts + answer latency (one call)',
|
|
317
|
+
'all-inbox': '--all whole-account scope (not just this channel)',
|
|
318
|
+
'all-listen': '--all single ear: also hear notification ANSWERS, not just messages (#131)',
|
|
319
|
+
limit: '--limit N cap the number of rows',
|
|
320
|
+
claim: '--claim CODE the single-use setup code (the human copies it from the Pidge app)',
|
|
321
|
+
'url-base': '--url BASE the Pidge server base URL (default https://pidge.sh)',
|
|
322
|
+
print: '--print emit `export …` lines instead of writing a file (per-agent; you run it)',
|
|
323
|
+
force: '--force overwrite a shared config owned by another channel',
|
|
324
|
+
'listen-mode': '--listen-mode MODE declare how you operate: turn_based | persistent | external_daemon',
|
|
325
|
+
follow: '--follow KEEP listening until --timeout (supervisor-only; traps a turn-based agent)',
|
|
326
|
+
'ack-on-read': '--ack-on-read consume messages on read (pre-0.9 immediate-consume)',
|
|
327
|
+
'up-to': '--up-to ID process every message up to this id',
|
|
328
|
+
ids: '--ids a,b process this comma-list of ids',
|
|
329
|
+
renew: '--renew heartbeat the visibility-timeout lease instead of processing',
|
|
330
|
+
window: '--window N reachability window in seconds (default 30)',
|
|
331
|
+
'quiet-nag': '--quiet-nag silence the "server has new capabilities" nag for this run',
|
|
332
|
+
};
|
|
333
|
+
// Content flags shared by notify / ask / hello.
|
|
334
|
+
const CONTENT_OPTS = ['title', 'body', 'body-markdown', 'subtitle', 'template', 'profile',
|
|
335
|
+
'event-at', 'lead-minutes', 'urgency', 'image', 'file', 'url', 'copy', 'actions',
|
|
336
|
+
'custom-action', 'deliver-at', 'reply-to', 'correlation-id', 'thread', 'after',
|
|
337
|
+
'collapse-key', 'param'];
|
|
338
|
+
|
|
339
|
+
const HELP = {
|
|
340
|
+
setup: {
|
|
341
|
+
summary: 'one-shot onboarding (#110): exchange a single-use claim code for the channel key, store it, run doctor.',
|
|
342
|
+
usage: 'pidge setup --claim CODE [--url BASE] [--print] [--force] [--listen-mode MODE]',
|
|
343
|
+
body: 'The CLI writes the key itself (chmod 600) — it never appears on screen or in the agent\'s chat. MULTI-AGENT: set PIDGE_AGENT=<id> at each agent\'s launch for an isolated config.',
|
|
344
|
+
opts: ['claim', 'url-base', 'print', 'force', 'listen-mode'],
|
|
345
|
+
},
|
|
346
|
+
doctor: {
|
|
347
|
+
summary: 'validate the setup WITHOUT exposing secrets (env source, server, key, device reach, realtime probe).',
|
|
348
|
+
usage: 'pidge doctor',
|
|
349
|
+
opts: [],
|
|
350
|
+
},
|
|
351
|
+
whoami: {
|
|
352
|
+
summary: 'which channel does this key speak for (prints the identity JSON).',
|
|
353
|
+
usage: 'pidge whoami',
|
|
354
|
+
opts: [],
|
|
355
|
+
},
|
|
356
|
+
hello: {
|
|
357
|
+
summary: 'first-contact WOW (#217): your channel\'s debut handshake, narrated live by a 3-stage Live Activity. send + wait in one.',
|
|
358
|
+
usage: 'pidge hello [options]',
|
|
359
|
+
body: 'A thin wrapper over `ask --template onboarding` with friendly default copy. Run it as your FIRST contact on a fresh channel.',
|
|
360
|
+
opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
|
|
361
|
+
},
|
|
362
|
+
fyi: {
|
|
363
|
+
summary: 'send passive info the human can read later — no action (#246 type fyi → profile Mensagem).',
|
|
364
|
+
usage: 'pidge fyi --title TEXT [--body TEXT | --body-markdown MD] [--image PATH] [--url URL]',
|
|
365
|
+
body: 'Fire-and-forget: stdout is the raw 201. Use it for logs, registros and neutral summaries — if you need a DECISION use `pidge ask`.',
|
|
366
|
+
opts: [...CONTENT_OPTS],
|
|
367
|
+
},
|
|
368
|
+
report: {
|
|
369
|
+
summary: 'send a curated result/digest the human will want to read now (#246 type report → Relevante).',
|
|
370
|
+
usage: 'pidge report --title TEXT [--body-markdown MD] [--image PATH] [--url URL]',
|
|
371
|
+
body: 'Fire-and-forget, like fyi, but flagged as worth reading now (the feed gives it a highlighted hairline).',
|
|
372
|
+
opts: [...CONTENT_OPTS],
|
|
373
|
+
},
|
|
374
|
+
ask: {
|
|
375
|
+
summary: 'ask the human a yes/no/choice and block until they answer (#246 type ask → Relevante + ação badge).',
|
|
376
|
+
usage: 'pidge ask --title TEXT --actions yes,no,reply [--reply-to URL] [options]',
|
|
377
|
+
body: 'Sends, then holds a WebSocket (or polls) until a TERMINAL answer. REQUIRES a way to answer — --actions (catalog or JSON), --custom-action, or a --template that supplies them. A snooze/reschedule re-fires (ask keeps waiting, prints snooze_until). profile "tracking" is refused (it never produces an answer).',
|
|
378
|
+
opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
|
|
379
|
+
},
|
|
380
|
+
event: {
|
|
381
|
+
summary: 'surface a scheduled thing with a known time — countdown Live Activity (#246 type event → Evento).',
|
|
382
|
+
usage: 'pidge event --title TEXT --event-at ISO8601 [--lead-minutes N] [--body-markdown MD]',
|
|
383
|
+
body: 'REQUIRES --event-at (ISO8601, e.g. 2026-06-26T14:00-03:00 — no offset ⇒ the user\'s timezone). --lead-minutes (5–240) starts the countdown N min before.',
|
|
384
|
+
opts: [...CONTENT_OPTS],
|
|
385
|
+
},
|
|
386
|
+
alert: {
|
|
387
|
+
summary: 'flag an anomaly/error needing attention; --escalate forces an AlarmKit alarm (#246 type alert → Urgente).',
|
|
388
|
+
usage: 'pidge alert --title TEXT [--body TEXT | --body-markdown MD] [--escalate]',
|
|
389
|
+
body: 'Fire-and-forget. The channel\'s Urgente profile decides the modality; --escalate asks for an AlarmKit alarm that breaks through silent/Focus (the human\'s profile still has the final say).',
|
|
390
|
+
opts: [...CONTENT_OPTS, 'escalate'],
|
|
391
|
+
},
|
|
392
|
+
live: {
|
|
393
|
+
summary: 'track an in-flight task (deploy/build/trip) with incremental updates (#246 type live → Live Activity).',
|
|
394
|
+
usage: 'pidge live --title TEXT [--body TEXT] [--lead-minutes N]',
|
|
395
|
+
body: 'Fire-and-forget. Records the live type; the LA-as-primitive is being built — today the send is delivered as a normal notification.',
|
|
396
|
+
opts: [...CONTENT_OPTS],
|
|
397
|
+
},
|
|
398
|
+
notify: {
|
|
399
|
+
summary: 'DEPRECATED (0.13.x) — send WITHOUT a type; the server falls back to fyi. Use a typed send instead.',
|
|
400
|
+
usage: 'pidge notify [options]',
|
|
401
|
+
body: 'Kept for one minor for compat retro — it warns and still sends (template_kind defaults to fyi server-side; 0.14 will 422). Prefer `pidge fyi/report/ask/event/alert/live`.',
|
|
402
|
+
opts: [...CONTENT_OPTS],
|
|
403
|
+
},
|
|
404
|
+
wait: {
|
|
405
|
+
summary: 'block on an already-sent notification until it is answered (prints chosen_action JSON).',
|
|
406
|
+
usage: 'pidge wait <correlation_id> [options]',
|
|
407
|
+
opts: ['timeout', 'interval', 'realtime', 'no-realtime'],
|
|
408
|
+
},
|
|
409
|
+
cancel: {
|
|
410
|
+
summary: 'cancel a still-scheduled notification before it fires (#56; idempotent; 409 once it reached the phone).',
|
|
411
|
+
usage: 'pidge cancel <correlation_id>',
|
|
412
|
+
opts: [],
|
|
413
|
+
},
|
|
414
|
+
inbox: {
|
|
415
|
+
summary: 'what you sent: the list (default), the pending slice, or counts + answer latency (#83).',
|
|
416
|
+
usage: 'pidge inbox [--pending | --summary] [--all] [--limit N]',
|
|
417
|
+
opts: ['pending', 'summary', 'all-inbox', 'limit'],
|
|
418
|
+
},
|
|
419
|
+
listen: {
|
|
420
|
+
summary: 'block until the human MESSAGES you from the app, print, ACK after the work, exit (#48).',
|
|
421
|
+
usage: 'pidge listen [--timeout N] [--all] [--ack-on-read] [--follow]',
|
|
422
|
+
body: 'One-shot by design (loop it, don\'t daemonize). #170: a read message is DELIVERED (gray ✓✓), NOT done — ack it AFTER the work with `pidge ack --up-to <id>` (a ~10-min lease re-serves un-acked messages, so a crash never loses one).',
|
|
423
|
+
opts: ['timeout', 'all-listen', 'ack-on-read', 'follow', 'interval', 'realtime', 'no-realtime'],
|
|
424
|
+
},
|
|
425
|
+
ack: {
|
|
426
|
+
summary: 'mark messages PROCESSED (green ✓✓) after you handled them, or --renew the lease on a long task (#170).',
|
|
427
|
+
usage: 'pidge ack --up-to <id> | --ids a,b [--renew]',
|
|
428
|
+
opts: ['up-to', 'ids', 'renew'],
|
|
429
|
+
},
|
|
430
|
+
contract: {
|
|
431
|
+
summary: 'DECLARE how you operate (#182) — ADVISORY, never policy (the human SEES if you honor it).',
|
|
432
|
+
usage: 'pidge contract set <key>=<value> | pidge contract show',
|
|
433
|
+
body: 'Keys: keep_connection_alive, mirror_in_origin_session, listen_mode=turn_based|persistent|external_daemon, quiet_when_idle. An unknown key / bad value is rejected locally (exit 1).',
|
|
434
|
+
opts: [],
|
|
435
|
+
},
|
|
436
|
+
selftest: {
|
|
437
|
+
summary: 'prove your listener works by ROUND-TRIP (#205): fire a nonce, run the listener, confirm it acks in time.',
|
|
438
|
+
usage: 'pidge selftest [--window N]',
|
|
439
|
+
body: 'PASS exit 0 / FAIL exit 2 (with the likely cause). Run it as the last onboarding step + whenever sends seem to go unheard.',
|
|
440
|
+
opts: ['window'],
|
|
441
|
+
},
|
|
442
|
+
skill: {
|
|
443
|
+
summary: 'write .claude/skills/pidge/SKILL.md generated from the live manifest (persistent Pidge knowledge for Claude Code).',
|
|
444
|
+
usage: 'pidge skill install',
|
|
445
|
+
opts: [],
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// Render the focused help for one command, or the global USAGE when the topic is
|
|
450
|
+
// unknown / absent (so `pidge --help` and `pidge help` keep the full overview).
|
|
451
|
+
function helpFor(topic) {
|
|
452
|
+
const h = HELP[topic];
|
|
453
|
+
if (!h) return USAGE;
|
|
454
|
+
const lines = [`pidge ${topic} — ${h.summary}`, '', 'USAGE', ` ${h.usage}`];
|
|
455
|
+
if (h.body) { lines.push('', h.body); }
|
|
456
|
+
if (h.opts && h.opts.length) {
|
|
457
|
+
lines.push('', 'OPTIONS');
|
|
458
|
+
for (const key of h.opts) lines.push(` ${OPTION_DOCS[key] || key}`);
|
|
459
|
+
}
|
|
460
|
+
lines.push('', 'Run `pidge --help` for all commands; GET $PIDGE_URL/api/v1/manifest is the full contract (Bearer auth).');
|
|
461
|
+
return lines.join('\n');
|
|
462
|
+
}
|
|
463
|
+
|
|
271
464
|
let parsed;
|
|
272
465
|
try {
|
|
273
466
|
parsed = parseArgs({ options: OPTIONS, allowPositionals: true });
|
|
@@ -276,9 +469,18 @@ try {
|
|
|
276
469
|
}
|
|
277
470
|
const v = parsed.values;
|
|
278
471
|
const command = parsed.positionals[0];
|
|
279
|
-
|
|
280
|
-
//
|
|
281
|
-
|
|
472
|
+
// #241: silence the manifest-version nag entirely (per run via --quiet-nag, or
|
|
473
|
+
// per environment via PIDGE_QUIET_NAG=1) — for scripts and CI where the nudge is noise.
|
|
474
|
+
const QUIET_NAG = !!v['quiet-nag'] || process.env.PIDGE_QUIET_NAG === '1';
|
|
475
|
+
|
|
476
|
+
// Help on stdout, exit 0. #240: `pidge <cmd> --help` / `pidge help <cmd>` show the
|
|
477
|
+
// FOCUSED help for that command (its synopsis + own flags); `pidge --help` / `help`
|
|
478
|
+
// with no command show the global USAGE. No command at all → USAGE on stderr, exit 1.
|
|
479
|
+
if (v.help || command === 'help') {
|
|
480
|
+
const topic = command === 'help' ? parsed.positionals[1] : command;
|
|
481
|
+
console.log(helpFor(topic));
|
|
482
|
+
process.exit(0);
|
|
483
|
+
}
|
|
282
484
|
if (!command) { console.error(USAGE); process.exit(1); }
|
|
283
485
|
// `setup` is the command that CREATES the token config — it must run without one.
|
|
284
486
|
if (!TOKEN && command !== 'setup')
|
|
@@ -300,17 +502,48 @@ function fetchT(url, opts = {}, timeoutMs = 30000) {
|
|
|
300
502
|
}
|
|
301
503
|
|
|
302
504
|
// The server advertises its manifest version on every response. When it's newer
|
|
303
|
-
// than what this CLI shipped knowing, nudge
|
|
304
|
-
//
|
|
505
|
+
// than what this CLI shipped knowing, nudge on stderr — the agent re-reads the
|
|
506
|
+
// manifest (whats_new) and learns the new capabilities without polling.
|
|
305
507
|
const KNOWN_MANIFEST_VERSION = 31;
|
|
508
|
+
const NAG_TTL_MS = 24 * 60 * 60 * 1000; // #241: at most one nag per 24 h
|
|
306
509
|
let newsWarned = false;
|
|
510
|
+
|
|
511
|
+
// #241: a tiny per-install state cache (~/.config/pidge/state.json, per-agent
|
|
512
|
+
// when PIDGE_AGENT is set — same dir as the env file). Best-effort: a read-only
|
|
513
|
+
// fs just means the throttle falls back to once-per-process. Date is fine here
|
|
514
|
+
// (this is the CLI process, not a workflow script).
|
|
515
|
+
function stateFilePath() { return path.join(pidgeConfigDir(), 'state.json'); }
|
|
516
|
+
function readState() {
|
|
517
|
+
try { return JSON.parse(fs.readFileSync(stateFilePath(), 'utf8')) || {}; } catch { return {}; }
|
|
518
|
+
}
|
|
519
|
+
function writeState(patch) {
|
|
520
|
+
try {
|
|
521
|
+
const next = { ...readState(), ...patch };
|
|
522
|
+
fs.mkdirSync(pidgeConfigDir(), { recursive: true, mode: 0o700 });
|
|
523
|
+
fs.writeFileSync(stateFilePath(), JSON.stringify(next, null, 2) + '\n', { mode: 0o600 });
|
|
524
|
+
} catch { /* best-effort — the nag just won't persist its throttle */ }
|
|
525
|
+
}
|
|
526
|
+
|
|
307
527
|
function checkManifestNews(res) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
528
|
+
if (QUIET_NAG || newsWarned) return;
|
|
529
|
+
const ver = parseInt(res.headers.get('x-pidge-manifest-version') || '0', 10);
|
|
530
|
+
// (c) only when the server is ahead of what THIS CLI knows.
|
|
531
|
+
if (!(ver > KNOWN_MANIFEST_VERSION)) return;
|
|
532
|
+
// #241 throttle: nag at most once per 24 h, and after that window only when the
|
|
533
|
+
// server version actually CHANGED — so 5 calls in a row (or a steady server)
|
|
534
|
+
// don't re-spam. A recent OR unchanged record suppresses; the record's seenAt is
|
|
535
|
+
// stamped only on a real nag (suppressed runs don't roll the 24 h clock forward).
|
|
536
|
+
const last = readState().manifestVersion;
|
|
537
|
+
if (last && last.seenAt) {
|
|
538
|
+
const recent = (Date.now() - Date.parse(last.seenAt)) < NAG_TTL_MS; // (a)
|
|
539
|
+
const unchanged = last.value === ver; // (b)
|
|
540
|
+
if (recent || unchanged) { newsWarned = true; return; }
|
|
313
541
|
}
|
|
542
|
+
newsWarned = true;
|
|
543
|
+
writeState({ manifestVersion: { value: ver, seenAt: new Date().toISOString() } });
|
|
544
|
+
// #119: a pinned npx ref never updates itself — give the CONCRETE command.
|
|
545
|
+
// #243: show the AUTHENTICATED curl so re-reading the manifest doesn't 401.
|
|
546
|
+
console.error(`pidge: the server has NEW capabilities (manifest v${ver}; this CLI knows v${KNOWN_MANIFEST_VERSION}) — re-read the contract: curl -H "Authorization: Bearer $PIDGE_TOKEN" $PIDGE_URL/api/v1/manifest (see whats_new), then UPDATE the CLI: npm i -g pidge-cli@latest (npx users: run npx pidge-cli@latest, a pinned ref never self-updates). Silence this with --quiet-nag or PIDGE_QUIET_NAG=1.`);
|
|
314
547
|
}
|
|
315
548
|
|
|
316
549
|
// ---------------------------------------------------------------------------
|
|
@@ -487,8 +720,52 @@ function probeRealtime(base, token) {
|
|
|
487
720
|
});
|
|
488
721
|
}
|
|
489
722
|
|
|
490
|
-
//
|
|
491
|
-
|
|
723
|
+
// #242: a custom action id is lowercase letters, digits and underscore (≤40) —
|
|
724
|
+
// the same rule the server enforces, validated LOCALLY so a typo fails fast.
|
|
725
|
+
const CUSTOM_ACTION_ID = /^[a-z0-9_]{1,40}$/;
|
|
726
|
+
|
|
727
|
+
// --custom-action "id:label[:destructive][:confirm][:biometric][:terminal]"
|
|
728
|
+
function customActionFromSpec(spec) {
|
|
729
|
+
const [id, label, ...flags] = spec.split(':');
|
|
730
|
+
// #157 P2: fail fast locally — the rule is stable and the server 422 costs a
|
|
731
|
+
// round-trip an agent then has to interpret.
|
|
732
|
+
if (!CUSTOM_ACTION_ID.test(id || '')) {
|
|
733
|
+
die(`pidge: --custom-action id ${JSON.stringify(id)} is invalid — lowercase letters, digits and underscore only (^[a-z0-9_]{1,40}$)`, 1);
|
|
734
|
+
}
|
|
735
|
+
const ca = { id, label };
|
|
736
|
+
if (flags.includes('destructive')) ca.style = 'destructive';
|
|
737
|
+
if (flags.includes('confirm')) ca.confirm = true;
|
|
738
|
+
if (flags.includes('biometric')) ca.biometric = true;
|
|
739
|
+
if (flags.includes('terminal')) ca.terminal = true;
|
|
740
|
+
return ca;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// #242: one item of a JSON --actions array → a custom_actions spec. Validates
|
|
744
|
+
// {id,label} and passes the optional gating fields the server understands.
|
|
745
|
+
function customActionFromJson(item, i) {
|
|
746
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
747
|
+
die(`pidge: --actions[${i}] must be an object with "id" and "label" (e.g. {"id":"approve","label":"Aprovar agora"})`, 1);
|
|
748
|
+
}
|
|
749
|
+
if (typeof item.id !== 'string' || !CUSTOM_ACTION_ID.test(item.id)) {
|
|
750
|
+
die(`pidge: --actions[${i}].id ${JSON.stringify(item.id)} is invalid — lowercase letters, digits and underscore only (^[a-z0-9_]{1,40}$)`, 1);
|
|
751
|
+
}
|
|
752
|
+
if (typeof item.label !== 'string' || !item.label.trim()) {
|
|
753
|
+
die(`pidge: --actions[${i}].label is required — a non-empty string`, 1);
|
|
754
|
+
}
|
|
755
|
+
const ca = { id: item.id, label: item.label };
|
|
756
|
+
if (item.sf_symbol !== undefined) ca.sf_symbol = item.sf_symbol;
|
|
757
|
+
if (item.style !== undefined) ca.style = item.style;
|
|
758
|
+
if (item.destructive) ca.style = 'destructive';
|
|
759
|
+
if (item.confirm !== undefined) ca.confirm = !!item.confirm;
|
|
760
|
+
if (item.biometric !== undefined) ca.biometric = !!item.biometric;
|
|
761
|
+
if (item.terminal !== undefined) ca.terminal = !!item.terminal;
|
|
762
|
+
return ca;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Map CLI flags → the /notify JSON body, including only what was provided. `extra`
|
|
766
|
+
// carries subcommand-supplied raw fields (#246: the typed sends' template_kind and
|
|
767
|
+
// alert's escalate) — merged below, before the --param escape hatch.
|
|
768
|
+
function buildBody(extra = {}) {
|
|
492
769
|
if (!v.title) die('pidge: --title is required', 1);
|
|
493
770
|
const body = { title: v.title };
|
|
494
771
|
if (v.body !== undefined) body.body = v.body;
|
|
@@ -507,25 +784,31 @@ function buildBody() {
|
|
|
507
784
|
if (v.thread !== undefined) body.thread_id = v.thread;
|
|
508
785
|
if (v.after !== undefined) body.after = v.after;
|
|
509
786
|
if (v['collapse-key'] !== undefined) body.collapse_key = v['collapse-key'];
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
787
|
+
|
|
788
|
+
// --actions: the short comma form (built-in catalog ids → body.actions) OR a
|
|
789
|
+
// JSON array of custom {id,label,…} specs (#242 → body.custom_actions). A
|
|
790
|
+
// leading '[' selects JSON; bad JSON is a friendly LOCAL error (exit 1), never
|
|
791
|
+
// a silent fall-through that drops the labels and sends a plain notification.
|
|
792
|
+
// --custom-action specs APPEND to whatever the JSON form produced, so both can coexist.
|
|
793
|
+
const customActions = [];
|
|
794
|
+
if (v.actions !== undefined) {
|
|
795
|
+
const trimmed = v.actions.trim();
|
|
796
|
+
if (trimmed.startsWith('[')) {
|
|
797
|
+
let arr;
|
|
798
|
+
try { arr = JSON.parse(trimmed); }
|
|
799
|
+
catch (e) { die(`pidge: --actions looks like JSON but didn't parse (${e.message}). Use a JSON array of {"id","label"} objects, or the short form yes,no,reply`, 1); }
|
|
800
|
+
if (!Array.isArray(arr)) die('pidge: --actions JSON must be an ARRAY of {"id","label"} objects', 1);
|
|
801
|
+
arr.forEach((item, i) => customActions.push(customActionFromJson(item, i)));
|
|
802
|
+
} else {
|
|
803
|
+
body.actions = trimmed.split(',').filter(Boolean);
|
|
804
|
+
}
|
|
528
805
|
}
|
|
806
|
+
for (const spec of v['custom-action'] || []) customActions.push(customActionFromSpec(spec));
|
|
807
|
+
if (customActions.length) body.custom_actions = customActions;
|
|
808
|
+
|
|
809
|
+
// #246: subcommand-supplied raw fields (template_kind, alert's escalate). Applied
|
|
810
|
+
// before the --param loop so a raw --param can still override in a pinch.
|
|
811
|
+
Object.assign(body, extra);
|
|
529
812
|
|
|
530
813
|
// Escape hatch: any raw /notify field, so a NEW server field documented in the
|
|
531
814
|
// manifest works the day it ships — no CLI release needed. JSON values parse
|
|
@@ -597,8 +880,8 @@ async function resolveMedia(body) {
|
|
|
597
880
|
// POST /notify. Returns { ok, info, raw }. Emits to STDERR what an agent most
|
|
598
881
|
// needs to KNOW (0 devices / no banner buttons / an armed alarm / a policy
|
|
599
882
|
// degrade), so stdout stays free for machine output.
|
|
600
|
-
async function doNotify() {
|
|
601
|
-
const payload = buildBody();
|
|
883
|
+
async function doNotify(extra = {}) {
|
|
884
|
+
const payload = buildBody(extra);
|
|
602
885
|
await resolveMedia(payload);
|
|
603
886
|
let res, raw;
|
|
604
887
|
try {
|
|
@@ -637,6 +920,25 @@ async function doNotify() {
|
|
|
637
920
|
return { ok, info, raw };
|
|
638
921
|
}
|
|
639
922
|
|
|
923
|
+
// #246: the typed send subcommands (fyi/report/event/alert/live) share notify's
|
|
924
|
+
// fire-and-forget shape — stamp template_kind, POST, print the raw 201, exit
|
|
925
|
+
// (0 ok / 2 failed). `ask` is the one type that send+waits (it needs a decision)
|
|
926
|
+
// and so keeps its own case. `extra` carries alert's escalate:true.
|
|
927
|
+
async function doTypedNotify(kind, extra = {}) {
|
|
928
|
+
const { ok, info, raw } = await doNotify({ template_kind: kind, ...extra });
|
|
929
|
+
console.log(raw);
|
|
930
|
+
if (ok && info.correlation_id)
|
|
931
|
+
console.error(`pidge: correlation_id=${info.correlation_id} (use: pidge wait ${info.correlation_id})`);
|
|
932
|
+
process.exit(ok ? 0 : 2);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// #246: `pidge notify` / `pidge send` (no type) are deprecated for ONE minor
|
|
936
|
+
// (0.13.x) — they still send, and the server falls back to template_kind "fyi"
|
|
937
|
+
// (soft-rollout). 0.14 will 422 a typeless send. The warning is local (stderr).
|
|
938
|
+
function warnDeprecatedSend(name) {
|
|
939
|
+
console.error(`pidge: \`pidge ${name}\` is deprecated — use a TYPE instead: fyi · report · ask · event · alert · live (see \`pidge help\`). Server-side fallback to \`fyi\` continues in 0.13.x; will be removed in 0.14.`);
|
|
940
|
+
}
|
|
941
|
+
|
|
640
942
|
// Poll GET /notifications/:cid until a TERMINAL answer, print chosen_action JSON to
|
|
641
943
|
// stdout, exit 0. A snooze (snooze / reschedule-to-a-time) is non-terminal — it
|
|
642
944
|
// re-fires — so keep waiting through it. Exits 3 on timeout.
|
|
@@ -1262,6 +1564,24 @@ Generated from manifest v${m.manifest_version} of ${BASE} — re-run \`pidge ski
|
|
|
1262
1564
|
|
|
1263
1565
|
All commands: \`npx pidge-cli …\` (Node ≥18; reads ~/.config/pidge/env — no token in context). Not set up? \`pidge doctor\` tells you; onboard with \`pidge setup --claim <code>\` (the human copies the code from the Pidge app).
|
|
1264
1566
|
|
|
1567
|
+
## Choose the right type (REQUIRED in 0.14+)
|
|
1568
|
+
|
|
1569
|
+
Every send needs a type. Pick by intent:
|
|
1570
|
+
|
|
1571
|
+
| You want to... | Use | Example |
|
|
1572
|
+
|---|---|---|
|
|
1573
|
+
| Log something the human can read later, no action | \`pidge fyi\` | "Build completed in 2m12s" |
|
|
1574
|
+
| Deliver a curated result/digest worth reading now | \`pidge report\` | "Daily standup summary" |
|
|
1575
|
+
| Ask the human a yes/no/choice — block until they answer | \`pidge ask\` | "Approve deploy v3.2?" with \`--actions yes,no\` |
|
|
1576
|
+
| Surface a scheduled thing (with time) | \`pidge event\` | "Sprint review 14h" with \`--event-at ...\` |
|
|
1577
|
+
| Anomaly/error needing attention; add \`--escalate\` for AlarmKit | \`pidge alert\` | "API 503 errors spiked" |
|
|
1578
|
+
| Track an in-flight task with incremental updates | \`pidge live\` | "Deploy v3.2 — building..." |
|
|
1579
|
+
|
|
1580
|
+
If unsure: \`fyi\` for passive info, \`ask\` if you need a decision. NEVER use \`pidge send\`
|
|
1581
|
+
without a type — in 0.14 it'll 422. (In 0.13.x it warns locally + server falls back to fyi.)
|
|
1582
|
+
|
|
1583
|
+
Available CLI commands (typed sends): \`pidge fyi\` · \`pidge report\` · \`pidge ask\` · \`pidge event\` · \`pidge alert\` · \`pidge live\` (and \`pidge notify\`, deprecated). Run \`pidge <type> --help\` for each one's own flags.
|
|
1584
|
+
|
|
1265
1585
|
## Pick the right send (decision table)
|
|
1266
1586
|
|
|
1267
1587
|
${table.map((r) => `- ${r}`).join('\n')}
|
|
@@ -1280,6 +1600,25 @@ ${notes.map((n) => `- ${n}`).join('\n')}
|
|
|
1280
1600
|
- \`pidge listen\` blocks until the human MESSAGES you from the app (composer) — run it when idle.
|
|
1281
1601
|
- ${exits}
|
|
1282
1602
|
|
|
1603
|
+
## Stay "always-on" while you're turn-based (#244)
|
|
1604
|
+
|
|
1605
|
+
A turn-based agent (Claude Code, ChatGPT, anything that only runs when invoked) can still be COMMANDABLE by your human. Two ways, neither needs a daemon:
|
|
1606
|
+
|
|
1607
|
+
### Path 1 — an interactive listening window (active session)
|
|
1608
|
+
\`\`\`bash
|
|
1609
|
+
pidge listen --follow --timeout 300 # hold for 5 min (--timeout is SECONDS), printing messages as they arrive
|
|
1610
|
+
\`\`\`
|
|
1611
|
+
Good while you're actively working. You stay online until the window closes. \`--follow\` is supervisor-style — it traps the turn — so only use it when you intend to sit and wait.
|
|
1612
|
+
|
|
1613
|
+
### Path 2 — a supervisor that polls, no daemon (24/7)
|
|
1614
|
+
A \`cron\` job or \`systemd\` timer invokes you every N minutes; each tick runs ONE one-shot listen and exits:
|
|
1615
|
+
\`\`\`bash
|
|
1616
|
+
pidge listen --timeout 50 # block up to 50s for a message, print it, exit 0 (exit 3 = nothing this tick)
|
|
1617
|
+
\`\`\`
|
|
1618
|
+
Each poll is one of your turns: pick up the message, do the work, \`pidge ack --up-to <id>\`, then sleep until the next tick. Real always-on without being a daemon. With Claude Code, the built-in \`/loop\` (auto-wake every N min) drives the same loop.
|
|
1619
|
+
|
|
1620
|
+
> \`--timeout\` is always SECONDS (not "5m"). One-shot \`pidge listen\` is the polling primitive — loop it from your supervisor; do NOT background it with \`&\` (an orphaned listener eats the queue).
|
|
1621
|
+
|
|
1283
1622
|
## Full spec
|
|
1284
1623
|
|
|
1285
1624
|
\`curl $PIDGE_URL/api/v1/manifest -H "Authorization: Bearer $PIDGE_TOKEN"\` — the always-current contract (fields, templates, custom actions, media, threads, realtime).
|
|
@@ -1321,7 +1660,34 @@ ${notes.map((n) => `- ${n}`).join('\n')}
|
|
|
1321
1660
|
await runSkillInstall();
|
|
1322
1661
|
break;
|
|
1323
1662
|
}
|
|
1324
|
-
|
|
1663
|
+
// #246: typed sends — fyi/report/event/alert/live stamp template_kind and
|
|
1664
|
+
// fire-and-forget. ask is separate (it send+waits). notify/send are the
|
|
1665
|
+
// deprecated typeless path (server falls back to fyi during the soft-rollout).
|
|
1666
|
+
case 'fyi':
|
|
1667
|
+
case 'report':
|
|
1668
|
+
await doTypedNotify(command);
|
|
1669
|
+
break;
|
|
1670
|
+
case 'event': {
|
|
1671
|
+
// event needs a TIME — validate locally (ISO8601) so the agent fails fast
|
|
1672
|
+
// instead of taking the server's event_at_required 422 round-trip.
|
|
1673
|
+
if (v['event-at'] === undefined)
|
|
1674
|
+
die('pidge: --event-at required for event. Use ISO8601: --event-at 2026-06-26T14:00-03:00', 1);
|
|
1675
|
+
if (Number.isNaN(Date.parse(v['event-at'])))
|
|
1676
|
+
die(`pidge: --event-at ${JSON.stringify(v['event-at'])} is not a valid ISO8601 datetime. Use e.g. --event-at 2026-06-26T14:00-03:00`, 1);
|
|
1677
|
+
await doTypedNotify('event');
|
|
1678
|
+
break;
|
|
1679
|
+
}
|
|
1680
|
+
case 'alert':
|
|
1681
|
+
// --escalate ⇒ escalate:true (ask the channel's Urgente profile for an
|
|
1682
|
+
// AlarmKit alarm that breaks through silent/Focus; the human's profile decides).
|
|
1683
|
+
await doTypedNotify('alert', v.escalate ? { escalate: true } : {});
|
|
1684
|
+
break;
|
|
1685
|
+
case 'live':
|
|
1686
|
+
await doTypedNotify('live');
|
|
1687
|
+
break;
|
|
1688
|
+
case 'notify':
|
|
1689
|
+
case 'send': {
|
|
1690
|
+
warnDeprecatedSend(command);
|
|
1325
1691
|
const { ok, info, raw } = await doNotify();
|
|
1326
1692
|
console.log(raw);
|
|
1327
1693
|
if (ok && info.correlation_id)
|
|
@@ -1361,13 +1727,18 @@ ${notes.map((n) => `- ${n}`).join('\n')}
|
|
|
1361
1727
|
if (v.profile === 'tracking')
|
|
1362
1728
|
die('pidge: `ask --profile tracking` makes no sense — tracking never produces an answer (use the live_activities API; need a decision? send a real profile)', 1);
|
|
1363
1729
|
if (!v.title) die('pidge: --title is required', 1);
|
|
1730
|
+
// #246: an ask DECLARES a decision — it must say HOW the human answers.
|
|
1731
|
+
// --actions (catalog or JSON), --custom-action, or a --template that supplies
|
|
1732
|
+
// them all satisfy it; none ⇒ a local error (the spec's "no hidden default").
|
|
1733
|
+
if (v.actions === undefined && !(v['custom-action'] || []).length && v.template === undefined)
|
|
1734
|
+
die('pidge: --actions required for ask. Use --actions yes,no,reply or a JSON array.', 1);
|
|
1364
1735
|
// The cid is minted CLIENT-side when not given, and printed as the FIRST
|
|
1365
1736
|
// stderr line (greppable) — a killed/crashed ask always leaves the handle
|
|
1366
1737
|
// behind, so the agent can `pidge wait <cid>` instead of re-sending.
|
|
1367
1738
|
const cid = v['correlation-id'] || crypto.randomUUID();
|
|
1368
1739
|
v['correlation-id'] = cid;
|
|
1369
1740
|
console.error(`pidge: correlation_id=${cid}`);
|
|
1370
|
-
const { ok, info } = await doNotify();
|
|
1741
|
+
const { ok, info } = await doNotify({ template_kind: 'ask' });
|
|
1371
1742
|
if (!ok) process.exit(2);
|
|
1372
1743
|
console.error(`pidge: sent (${info.registered_devices} device(s)) — waiting on ${cid}`);
|
|
1373
1744
|
// #132: no --timeout ⇒ obey the template's suggestion from the 201 echo
|
|
@@ -1669,6 +2040,8 @@ ${notes.map((n) => `- ${n}`).join('\n')}
|
|
|
1669
2040
|
break;
|
|
1670
2041
|
}
|
|
1671
2042
|
default:
|
|
1672
|
-
|
|
2043
|
+
// #246: name the bad command and point at the type catalog (a friendlier
|
|
2044
|
+
// landing than dumping the whole USAGE on a typo).
|
|
2045
|
+
die(`pidge: unknown subcommand '${command}'. Try: fyi · report · ask · event · alert · live · notify (deprecated). pidge --help`, 1);
|
|
1673
2046
|
}
|
|
1674
2047
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pidge-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Send rich, actionable iPhone notifications to a human and block until they answer. Built for AI agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pidge",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"bin",
|
|
20
20
|
"README.md",
|
|
21
|
+
"CHANGELOG.md",
|
|
21
22
|
"LICENSE"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|