pidge-cli 0.13.0 → 0.14.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 +46 -0
- package/README.md +73 -26
- package/bin/pidge.js +296 -152
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.0 — 2026-06-28
|
|
4
|
+
|
|
5
|
+
The married vocabulary (perfis) — the CLI now speaks the SAME language as the server
|
|
6
|
+
(manifest v42) and the app: ONE list of 5 message types, with RESPONSE as a separate
|
|
7
|
+
axis. No scripts break — the old names keep working as aliases.
|
|
8
|
+
|
|
9
|
+
- **feat:** the typed sends are renamed to the canonical 5 — `pidge message` ·
|
|
10
|
+
`important` · `urgent` · `event` · `live` (message←fyi, important←report, urgent←alert;
|
|
11
|
+
event/live unchanged). `important` is the recommended default. The wire sends the new
|
|
12
|
+
`template_kind`. (perfis-S1)
|
|
13
|
+
- **feat (compat):** the OLD names still work as aliases — `pidge fyi`→message,
|
|
14
|
+
`report`→important, `alert`→urgent — mapped to the new type, with a one-line rename
|
|
15
|
+
note on stderr. Muscle-memory and existing scripts are untouched. (perfis-S1)
|
|
16
|
+
- **feat:** RESPONSE is now its own axis, composing on ANY type — `--actions`/
|
|
17
|
+
`--custom-action` (buttons) + the new **`--wait`** (block until the human answers,
|
|
18
|
+
then print `chosen_action` JSON; without it = fire-and-forget). This is the explicit
|
|
19
|
+
"send-and-go vs wait". (perfis-S2)
|
|
20
|
+
- **feat:** `pidge ask` is now the shortcut for `important --wait` (still REQUIRES a way
|
|
21
|
+
to answer; preserved behavior). There is no `ask` TYPE in the married catalog — asking
|
|
22
|
+
is a type + buttons + wait. (perfis-S2)
|
|
23
|
+
- **feat:** `pidge approval` — a new go/no-go RECIPE = `important` + Approve/Reject +
|
|
24
|
+
Face ID on Approve + `--wait`. Sent as `custom_actions` (only custom actions carry
|
|
25
|
+
`biometric`, and a custom id can't reuse a built-in like approve/reject — so the ids
|
|
26
|
+
are `grant`/`deny`). Pass your own `--actions`/`--custom-action` to override the
|
|
27
|
+
pair. (perfis-S2)
|
|
28
|
+
- **docs:** USAGE, per-command help and the generated `SKILL.md` rewritten around the two
|
|
29
|
+
axes (type + response) — mirrors the human's app, drops the dead fyi/report framing.
|
|
30
|
+
- **chore:** `KNOWN_MANIFEST_VERSION` 36 → 42 (the live server), silencing the news nag.
|
|
31
|
+
|
|
32
|
+
## 0.13.1 — 2026-06-26
|
|
33
|
+
|
|
34
|
+
Polish from an agent E2E (2026-06-26). No breaking changes.
|
|
35
|
+
|
|
36
|
+
- **fix:** the manifest-version nudge no longer scolds "your CLI is stale, UPDATE it".
|
|
37
|
+
pidge is a thin pipe — `--param KEY=VALUE` carries any new `/notify` field NOW, so a
|
|
38
|
+
server manifest bump almost never needs a CLI release. The nudge is reframed as "new
|
|
39
|
+
capabilities + how to use them today" and `KNOWN_MANIFEST_VERSION` is bumped 31 → 36
|
|
40
|
+
(the current server), silencing the false-positive on `@latest`. (#26)
|
|
41
|
+
- **fix:** the public manifest (#249-A) curl in that nudge drops the mandatory Bearer —
|
|
42
|
+
the catalog reads without a key; the Bearer is shown only as the optional way to also
|
|
43
|
+
see the channel's own config. (#26)
|
|
44
|
+
- **fix:** the realtime reconnect log no longer reads "realtime socket **socket** closed"
|
|
45
|
+
(doubled word) and the counter no longer sticks at "attempt 1/4" — it now shows a
|
|
46
|
+
monotonic "reconnect #N" so a connect→drop flap visibly advances instead of looking
|
|
47
|
+
like a stuck loop. (#25)
|
|
48
|
+
|
|
3
49
|
## 0.13.0 — 2026-06-25
|
|
4
50
|
|
|
5
51
|
Template system (#246) — the agent now declares an intent TYPE; the server maps it to
|
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.14.0** — the **married vocabulary** (perfis): the CLI now speaks the same
|
|
15
|
+
> language as the server (manifest v42) and the app. **One list of 5 types** —
|
|
16
|
+
> `pidge message · important · urgent · event · live` (message←fyi, important←report,
|
|
17
|
+
> urgent←alert; old names still work as aliases). **RESPONSE is a separate axis**:
|
|
18
|
+
> `--actions`/`--custom-action` add buttons on ANY type, and **`--wait`** blocks until
|
|
19
|
+
> the human answers — the explicit *send-and-go vs wait*. `pidge ask` is now the shortcut
|
|
20
|
+
> for `important --wait`; new **`pidge approval`** = important + Approve/Reject + Face
|
|
21
|
+
> ID + wait. `important` is the recommended default.
|
|
22
|
+
>
|
|
14
23
|
> **New in v0.12.0** — CLI bugs batch (all reported by an agent in real use): **`pidge
|
|
15
24
|
> <sub> --help`** now shows that subcommand's own help (its flags), not the global dump
|
|
16
25
|
> (#240); the **manifest-version nag is throttled to once / 24 h** (cached in
|
|
@@ -113,39 +122,73 @@ export PIDGE_TOKEN=hld_xxx # your channel's bearer key
|
|
|
113
122
|
# (or skip the exports: the CLI reads ~/.config/pidge/env — KEY=VALUE — so the
|
|
114
123
|
# key never has to appear in an agent's chat; explicit env vars win)
|
|
115
124
|
|
|
116
|
-
#
|
|
117
|
-
npx pidge-cli
|
|
118
|
-
|
|
125
|
+
# Just inform — fire-and-forget (clears when the human opens it):
|
|
126
|
+
npx pidge-cli message --title "Build green" --body "2m12s"
|
|
127
|
+
|
|
128
|
+
# A pendency they should resolve — the DEFAULT type ("waiting-for-you" card):
|
|
129
|
+
npx pidge-cli important --title "Review PR #42" --url https://github.com/…/pull/42
|
|
119
130
|
|
|
120
|
-
#
|
|
131
|
+
# Send AND wait for the answer (the one an agent wants) — = important + --wait:
|
|
121
132
|
npx pidge-cli ask \
|
|
122
|
-
--title "
|
|
133
|
+
--title "Approve deploy?" --actions yes,no,reply --timeout 600
|
|
134
|
+
|
|
135
|
+
# A go/no-go with Face ID — the approval RECIPE (= important + Approve/Reject + wait):
|
|
136
|
+
npx pidge-cli approval --title "Deploy to production?"
|
|
137
|
+
|
|
138
|
+
# Urgent — breaks through silent/Focus; --escalate forces an AlarmKit alarm:
|
|
139
|
+
npx pidge-cli urgent --title "Balance dropped below $5k" --escalate
|
|
123
140
|
|
|
124
141
|
# A thing with a known time — push at T−lead + a lock-screen countdown to the event:
|
|
125
|
-
npx pidge-cli
|
|
126
|
-
--title "
|
|
142
|
+
npx pidge-cli event \
|
|
143
|
+
--title "Team meeting" --event-at "2026-06-10T15:00:00"
|
|
127
144
|
|
|
128
145
|
# A chart you generated — uploaded for you, shown on the banner + feed:
|
|
129
|
-
npx pidge-cli
|
|
146
|
+
npx pidge-cli message --title "Chart ready" --image ./chart.png
|
|
130
147
|
|
|
131
148
|
# A real artifact — the human previews it on the phone, shares it, saves to Files:
|
|
132
|
-
npx pidge-cli
|
|
149
|
+
npx pidge-cli important --title "Report" --file ./report.xlsx
|
|
133
150
|
```
|
|
134
151
|
|
|
135
|
-
`ask`
|
|
152
|
+
`ask`/`approval` (and any `--wait` send) print the chosen action as JSON to
|
|
153
|
+
**stdout** and exit `0`:
|
|
136
154
|
|
|
137
155
|
```json
|
|
138
156
|
{ "kind": "acted", "action_id": "yes", "label": "Sim", "text": null,
|
|
139
157
|
"at": "2026-06-08T18:19:51Z", "snooze_until": null }
|
|
140
158
|
```
|
|
141
159
|
|
|
160
|
+
## Two axes: the TYPE + the RESPONSE
|
|
161
|
+
|
|
162
|
+
You pick **one type** (how much it may intrude — the human already configured how each
|
|
163
|
+
arrives), then ORTHOGONALLY decide the **response** (buttons? wait or not?).
|
|
164
|
+
|
|
165
|
+
**Axis 1 — type** (the married catalog of 5):
|
|
166
|
+
|
|
167
|
+
| Type | For | Clears when |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `pidge message` | just inform, no action | the human OPENS it |
|
|
170
|
+
| `pidge important` ⭐ | a pendency they should resolve (the DEFAULT) | **Feito** |
|
|
171
|
+
| `pidge urgent` | wake them now (rare, real); `--escalate` = AlarmKit alarm | Feito (cuts the alarm) |
|
|
172
|
+
| `pidge event --event-at <ISO>` | a thing with a known time (countdown LA) | passed / Feito |
|
|
173
|
+
| `pidge live` | track something live (Live Activity) | you end it |
|
|
174
|
+
|
|
175
|
+
**Axis 2 — response** (composes on ANY type): `--actions yes,no` / `--custom-action`
|
|
176
|
+
add buttons (free text is always available); `--wait` blocks until the human answers
|
|
177
|
+
(else fire-and-forget — the answer arrives later in `pidge listen --all`). Two shortcuts
|
|
178
|
+
bundle both: **`pidge ask`** = `important --wait` (needs `--actions`); **`pidge approval`**
|
|
179
|
+
= `important` + Approve/Reject + Face ID + `--wait`.
|
|
180
|
+
|
|
181
|
+
> Old names still work as **aliases**: `fyi`→message, `report`→important, `alert`→urgent.
|
|
182
|
+
|
|
142
183
|
## Commands
|
|
143
184
|
|
|
144
185
|
| Command | What it does |
|
|
145
186
|
|---|---|
|
|
187
|
+
| `message` / `important` / `urgent` / `event` / `live` | The 5 message types (axis 1). Fire-and-forget by default; add `--actions`/`--wait` (axis 2) to ask for a reply. `important` is the recommended default. |
|
|
188
|
+
| `ask` | `important --wait` shortcut: send **and block** until the human answers; prints the chosen action JSON. Requires a way to answer (`--actions`/`--custom-action`/`--template`). |
|
|
189
|
+
| `approval` | Go/no-go RECIPE: `important` + Approve (Face ID) / Reject + `--wait`. Pass your own `--actions` to override the pair. |
|
|
146
190
|
| `hello` | **v0.11.0 (#217):** your channel's **first-contact WOW** — send the onboarding handshake **and block** until the human confirms. The server narrates a 3-stage Live Activity on the lock screen (Conectando → toque para confirmar → Concluído ✓) so they *see* the agent→human→agent loop close. Run it as your **first** contact on a fresh channel. A thin `ask --template onboarding` wrapper with friendly default copy. |
|
|
147
|
-
| `
|
|
148
|
-
| `notify` | Send only. Prints the raw 201 JSON; the `correlation_id` + warnings go to stderr. |
|
|
191
|
+
| `notify` | **Deprecated** — send without a type (the server picks the channel default). Prefer a typed send. Prints the raw 201 JSON; the `correlation_id` + warnings go to stderr. |
|
|
149
192
|
| `wait <correlation_id>` | Block on an already-sent notification until it's answered. |
|
|
150
193
|
| `cancel <correlation_id>` | Cancel a **still-scheduled** notification before it fires (idempotent; 409 once it reached the phone). |
|
|
151
194
|
| `inbox` | What you sent: list, `--pending` slice, or `--summary` (counts + answer latency). |
|
|
@@ -188,11 +231,11 @@ WebSocket → ?wait= long-poll (capped 25 s server-side) → plain GETs ever
|
|
|
188
231
|
--body TEXT the message shown on the banner
|
|
189
232
|
--body-markdown MD rich body for the tap-through detail screen
|
|
190
233
|
--subtitle TEXT
|
|
191
|
-
--profile ID
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
--event-at ISO8601 WHEN the thing happens (a FACT; required by
|
|
234
|
+
--profile ID low-level alias of the TYPE — the HUMAN owns what each does:
|
|
235
|
+
message · important · urgent · event (needs --event-at) ·
|
|
236
|
+
live · the user's custom profiles. Prefer the typed
|
|
237
|
+
subcommands above; an explicit --profile still wins.
|
|
238
|
+
--event-at ISO8601 WHEN the thing happens (a FACT; required by event)
|
|
196
239
|
--lead-minutes N notify/start the countdown N min before event_at (5–240)
|
|
197
240
|
--urgency LEVEL normal | persistent | alarm (low-level — prefer --profile)
|
|
198
241
|
--image PATH_OR_URL image on the banner + feed: a local path is uploaded for you
|
|
@@ -207,13 +250,16 @@ WebSocket → ?wait= long-poll (capped 25 s server-side) → plain GETs ever
|
|
|
207
250
|
'[{"id":"approve","label":"Aprovar agora"},{"id":"defer","label":"Depois"}]'
|
|
208
251
|
--custom-action SPEC "id:label[:destructive][:confirm][:biometric][:terminal]"
|
|
209
252
|
(repeatable — your own buttons; composes with --actions JSON)
|
|
253
|
+
--wait RESPONSE axis: block until the human answers (ANY type), then
|
|
254
|
+
print chosen_action JSON. Without it: fire-and-forget (the
|
|
255
|
+
answer arrives later in `pidge listen --all`). ask/approval imply it.
|
|
210
256
|
--deliver-at ISO8601 schedule for later
|
|
211
257
|
--reply-to URL also POST the answer to your webhook (HMAC-signed)
|
|
212
258
|
--correlation-id ID idempotency + routing key (auto-generated if omitted)
|
|
213
259
|
--collapse-key KEY replace/update a prior notification
|
|
214
260
|
--param KEY=VALUE pass ANY raw /notify field (repeatable) — future server
|
|
215
261
|
fields work day-one, no CLI update needed
|
|
216
|
-
--timeout SECONDS ask:
|
|
262
|
+
--timeout SECONDS how long --wait blocks (ask/approval: template suggestion ~3600 · wait: 300)
|
|
217
263
|
--interval SECONDS FALLBACK poll cadence (default 30) — normally unused: WS or
|
|
218
264
|
the server-held long-poll (?wait=25) make answers ~instant
|
|
219
265
|
--realtime force the WebSocket (Node ≥22); --no-realtime = polling only
|
|
@@ -224,9 +270,10 @@ WebSocket → ?wait= long-poll (capped 25 s server-side) → plain GETs ever
|
|
|
224
270
|
- **`ask` prints `correlation_id=<cid>` as its FIRST stderr line** (minted client-side
|
|
225
271
|
when you don't pass one) — a killed `ask` always leaves the handle behind, so you
|
|
226
272
|
can `pidge wait <cid>` instead of re-sending.
|
|
227
|
-
- **stdout is always machine-readable.**
|
|
228
|
-
|
|
229
|
-
notices, armed-escalation and
|
|
273
|
+
- **stdout is always machine-readable.** A fire-and-forget send → the raw 201 JSON; a
|
|
274
|
+
`--wait` send / `ask` / `approval` / `wait` → the `chosen_action` JSON. Everything
|
|
275
|
+
human (warnings, the correlation_id, snooze notices, armed-escalation and
|
|
276
|
+
policy-degrade narration) goes to **stderr**.
|
|
230
277
|
- **Exit codes:** `0` answered · `3` timed out (= *no answer yet*, NOT a failure —
|
|
231
278
|
back off and retry later) · `4` timed out **without one healthy round-trip all
|
|
232
279
|
session** (the CHANNEL looks broken — server/network — tell your human) ·
|
|
@@ -234,11 +281,11 @@ WebSocket → ?wait= long-poll (capped 25 s server-side) → plain GETs ever
|
|
|
234
281
|
- **Responses are one-and-done.** Every answer closes the notification EXCEPT a
|
|
235
282
|
**snooze** (or a reschedule that set a new time), which re-fires later. `ask`/`wait`
|
|
236
283
|
keep polling through a snooze and print `snooze_until` so you can schedule a re-check.
|
|
237
|
-
- **
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
-
|
|
241
|
-
|
|
284
|
+
- **Types degrade, never reject.** An over-ceiling type is delivered at the channel's
|
|
285
|
+
allowed level — read `degraded`/`degrade_reason` in the 201 (narrated on stderr).
|
|
286
|
+
That's the human's policy working; don't retry harder.
|
|
287
|
+
- **`--wait` / `ask` on `live` is refused** — `live` is status-only and never produces
|
|
288
|
+
an answer.
|
|
242
289
|
- A genuine follow-up question is a **new** notification, never a second answer on
|
|
243
290
|
the same one.
|
|
244
291
|
|
package/bin/pidge.js
CHANGED
|
@@ -11,14 +11,25 @@
|
|
|
11
11
|
// ~/.config/pidge/env — KEY=VALUE — is read instead, so the key can live
|
|
12
12
|
// OUTSIDE the agent's chat/context entirely, #57)
|
|
13
13
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// TWO AXES (perfis, manifest v40+): (1) the TYPE — one married list of 5 the
|
|
15
|
+
// human configured how to receive: message · important · urgent · event · live;
|
|
16
|
+
// (2) the RESPONSE — buttons (--actions/--custom-action) + send-and-go vs wait
|
|
17
|
+
// (--wait blocks until the human answers). Response composes onto ANY type.
|
|
16
18
|
//
|
|
17
|
-
// #
|
|
18
|
-
// pidge
|
|
19
|
+
// # just inform — fire-and-forget (prints the raw 201)
|
|
20
|
+
// pidge message --title "Build green" --body "2m12s"
|
|
19
21
|
//
|
|
20
|
-
// # a
|
|
21
|
-
// pidge
|
|
22
|
+
// # a pendency the human should resolve (the DEFAULT type) + block on the answer
|
|
23
|
+
// pidge important --title "Approve deploy?" --actions yes,no,reply --wait
|
|
24
|
+
//
|
|
25
|
+
// # a go/no-go decision with Face ID — the approval RECIPE (= important + wait + gate)
|
|
26
|
+
// pidge approval --title "Deploy to production?"
|
|
27
|
+
//
|
|
28
|
+
// # urgent: breaks through silent/Focus, escalates to an AlarmKit alarm
|
|
29
|
+
// pidge urgent --title "Balance dropped below $5k" --escalate
|
|
30
|
+
//
|
|
31
|
+
// # a thing with a known time: push at T−lead + a lock-screen countdown
|
|
32
|
+
// pidge event --title "Team meeting" --event-at "2026-06-10T15:00:00"
|
|
22
33
|
//
|
|
23
34
|
// # block on an already-sent notification (by correlation_id)
|
|
24
35
|
// pidge wait order-7 --timeout 300
|
|
@@ -119,6 +130,9 @@ const OPTIONS = {
|
|
|
119
130
|
param: { type: 'string', multiple: true }, // key=value escape hatch → raw /notify field
|
|
120
131
|
timeout: { type: 'string' },
|
|
121
132
|
interval: { type: 'string' },
|
|
133
|
+
// perfis-S2 response axis: --wait blocks until the human answers (composes on
|
|
134
|
+
// ANY type — send-and-go vs wait). ask/approval imply it.
|
|
135
|
+
wait: { type: 'boolean' },
|
|
122
136
|
// inbox flags (#83)
|
|
123
137
|
pending: { type: 'boolean' },
|
|
124
138
|
summary: { type: 'boolean' },
|
|
@@ -163,14 +177,20 @@ USAGE
|
|
|
163
177
|
narrated LIVE on the lock screen by a 3-stage Live Activity
|
|
164
178
|
(Conectando → toque para confirmar → Concluído ✓). send + wait
|
|
165
179
|
in one — run it as your FIRST contact on a fresh channel.
|
|
166
|
-
|
|
167
|
-
pidge
|
|
168
|
-
pidge
|
|
169
|
-
pidge
|
|
170
|
-
pidge event
|
|
171
|
-
pidge
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
AXIS 1 — TYPE (the married list of 5; the human configured how each arrives):
|
|
181
|
+
pidge message [options] just inform, no action — clears when the human OPENS it
|
|
182
|
+
pidge important [options] ⭐DEFAULT a pendency the human should resolve ("waiting-for-you" card)
|
|
183
|
+
pidge urgent [options] breaks through silent/Focus; --escalate forces an AlarmKit alarm
|
|
184
|
+
pidge event [options] a scheduled thing — needs --event-at (countdown Live Activity)
|
|
185
|
+
pidge live [options] an in-flight task with incremental updates (Live Activity)
|
|
186
|
+
AXIS 2 — RESPONSE (composes on ANY type above): --actions/--custom-action add
|
|
187
|
+
buttons; text reply is ALWAYS available; --wait blocks until the human answers
|
|
188
|
+
(send-and-go vs --wait). Two shortcuts bundle both axes:
|
|
189
|
+
pidge ask [options] = important + --wait; needs --actions (prints chosen_action JSON)
|
|
190
|
+
pidge approval [options] = important + Approve/Reject + Face ID + --wait (a go/no-go)
|
|
191
|
+
COMPAT aliases (old names still work → mapped to the new type):
|
|
192
|
+
pidge fyi→message · report→important · alert→urgent (event/live unchanged)
|
|
193
|
+
pidge notify [options] DEPRECATED — send without a type; prefer a TYPE above
|
|
174
194
|
pidge wait <correlation_id> [options] block on an already-sent notification
|
|
175
195
|
pidge cancel <correlation_id> cancel a still-scheduled notification (#56)
|
|
176
196
|
pidge inbox [--pending|--summary|--all|--limit N] what you sent: list, pending slice, or counts+latency (#83)
|
|
@@ -220,10 +240,10 @@ OPTIONS (notify / ask)
|
|
|
220
240
|
--template ID content/action pattern — WHAT you're asking: context (FYI,
|
|
221
241
|
no buttons) · decision (yes/no/reply) · approval · reminder ·
|
|
222
242
|
nudge · sensitive (gated, Face ID). Composes with --profile.
|
|
223
|
-
--profile ID
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
243
|
+
--profile ID low-level alias of the TYPE axis (the HUMAN owns what it
|
|
244
|
+
does): message · important · urgent · event · live ·
|
|
245
|
+
the user's custom profiles. Prefer the typed subcommands
|
|
246
|
+
above; an explicit --profile still wins. See the manifest.
|
|
227
247
|
--event-at ISO8601 WHEN the thing happens (a FACT; required by profile event)
|
|
228
248
|
--lead-minutes N notify/start countdown N min before event_at (5–240)
|
|
229
249
|
--urgency LEVEL normal | persistent | alarm (low-level — prefer --profile)
|
|
@@ -233,9 +253,13 @@ OPTIONS (notify / ask)
|
|
|
233
253
|
shares and saves on the phone; uploaded automatically (≤25 MB)
|
|
234
254
|
--url URL deep link the app opens when the user taps (PR, dashboard, log)
|
|
235
255
|
--copy TEXT value offered as tap-to-copy on the detail (code, token)
|
|
236
|
-
--actions LIST comma list: yes,no,approve,reject,accept,
|
|
237
|
-
done,snooze,reschedule,reply,mute
|
|
256
|
+
--actions LIST RESPONSE axis — comma list: yes,no,approve,reject,accept,
|
|
257
|
+
decline,later,done,snooze,reschedule,reply,mute (or a JSON
|
|
258
|
+
array of custom {id,label} objects). Composes on ANY type.
|
|
238
259
|
--custom-action SPEC "id:label[:destructive][:confirm][:biometric][:terminal]" (repeatable)
|
|
260
|
+
--wait RESPONSE axis — block until the human answers (any type),
|
|
261
|
+
then print chosen_action JSON. Without it: fire-and-forget
|
|
262
|
+
(the answer arrives later in \`pidge listen --all\`). ask/approval imply it.
|
|
239
263
|
--deliver-at ISO8601 schedule for later
|
|
240
264
|
--reply-to URL also POST the answer to your webhook (HMAC-signed)
|
|
241
265
|
--correlation-id ID idempotency + routing key (auto-generated if omitted)
|
|
@@ -247,7 +271,8 @@ OPTIONS (notify / ask)
|
|
|
247
271
|
--collapse-key KEY replace/update a prior notification
|
|
248
272
|
--param KEY=VALUE pass ANY raw /notify field (repeatable) — future server
|
|
249
273
|
fields work without a CLI update; the manifest is the contract
|
|
250
|
-
--timeout SECONDS
|
|
274
|
+
--timeout SECONDS how long --wait blocks (ask/approval: template's suggestion,
|
|
275
|
+
~3600 for a decision · wait: 300) — explicit always wins
|
|
251
276
|
--interval SECONDS FALLBACK poll cadence (default 30) — normally unused: WS or
|
|
252
277
|
the server-held long-poll (?wait=25) make answers ~instant
|
|
253
278
|
|
|
@@ -262,17 +287,18 @@ ENV
|
|
|
262
287
|
shared ~/.config/pidge/env (single-agent only).
|
|
263
288
|
|
|
264
289
|
OUTPUT
|
|
265
|
-
stdout is machine-readable (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
290
|
+
stdout is machine-readable (a fire-and-forget send→the raw 201 JSON; a --wait
|
|
291
|
+
send / ask / approval / wait→chosen_action JSON); human notices go to stderr.
|
|
292
|
+
Exit: 0 answered · 3 timed out (no answer yet, not a failure) · 4 timed out
|
|
293
|
+
WITHOUT ONE healthy round-trip all session (the CHANNEL looks broken —
|
|
294
|
+
server/network — not the human ignoring you: surface it instead of retrying
|
|
295
|
+
blindly, #119) · 2 error · 1 usage.
|
|
296
|
+
|
|
297
|
+
Responses are one-and-done EXCEPT snooze/reschedule (they re-fire); a --wait send
|
|
298
|
+
keeps polling through a snooze and prints snooze_until. Follow-up = a NEW
|
|
299
|
+
notification. An over-ceiling type is delivered DEGRADED, never rejected — read
|
|
300
|
+
the 201's degraded/degrade_reason (narrated on stderr). \`live\` is status-only:
|
|
301
|
+
it never produces an answer, so --wait/ask refuse it.
|
|
276
302
|
|
|
277
303
|
Full spec (the contract — always current): GET $PIDGE_URL/api/v1/manifest`;
|
|
278
304
|
|
|
@@ -290,17 +316,18 @@ const OPTION_DOCS = {
|
|
|
290
316
|
'body-markdown': '--body-markdown MD rich body for the tap-through detail screen',
|
|
291
317
|
subtitle: '--subtitle TEXT a secondary line under the title',
|
|
292
318
|
template: '--template ID content/action pattern: context · decision · approval · reminder · nudge · sensitive',
|
|
293
|
-
profile: '--profile ID
|
|
294
|
-
'event-at': '--event-at ISO8601 WHEN the thing happens (required by
|
|
319
|
+
profile: '--profile ID low-level alias of the TYPE (the human owns it): message · important · urgent · event · live · custom',
|
|
320
|
+
'event-at': '--event-at ISO8601 WHEN the thing happens (required by event)',
|
|
295
321
|
'lead-minutes': '--lead-minutes N notify/countdown N min before event_at (5–240)',
|
|
296
|
-
urgency: '--urgency LEVEL normal | persistent | alarm (low-level — prefer
|
|
297
|
-
escalate: '--escalate
|
|
322
|
+
urgency: '--urgency LEVEL normal | persistent | alarm (low-level — prefer the typed subcommand)',
|
|
323
|
+
escalate: '--escalate urgent: force an AlarmKit alarm that breaks through silent/Focus',
|
|
298
324
|
image: '--image PATH_OR_URL banner+feed image: a local path is uploaded; an https URL is sent as-is',
|
|
299
325
|
file: '--file PATH a real artifact (xlsx/pdf/csv…) uploaded for the human (≤25 MB)',
|
|
300
326
|
url: '--url URL deep link the app opens on tap (PR, dashboard, log)',
|
|
301
327
|
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',
|
|
328
|
+
actions: '--actions LIST|JSON RESPONSE axis: comma list from the catalog (yes,no,reply) OR a JSON array of {"id","label"} custom actions — composes on ANY type',
|
|
303
329
|
'custom-action': '--custom-action SPEC "id:label[:destructive][:confirm][:biometric][:terminal]" (repeatable)',
|
|
330
|
+
wait: '--wait RESPONSE axis: block until the human answers (any type), then print chosen_action JSON (ask/approval imply it)',
|
|
304
331
|
'deliver-at': '--deliver-at ISO8601 schedule the send for later',
|
|
305
332
|
'reply-to': '--reply-to URL also POST the answer to your webhook (HMAC-signed)',
|
|
306
333
|
'correlation-id': '--correlation-id ID idempotency + routing key (auto-generated if omitted)',
|
|
@@ -308,7 +335,7 @@ const OPTION_DOCS = {
|
|
|
308
335
|
after: '--after CID decision queue (#157): held until that notification is answered',
|
|
309
336
|
'collapse-key': '--collapse-key KEY replace/update a prior notification',
|
|
310
337
|
param: '--param KEY=VALUE pass ANY raw /notify field (repeatable) — the manifest is the contract',
|
|
311
|
-
timeout: '--timeout SECONDS how long
|
|
338
|
+
timeout: '--timeout SECONDS how long --wait blocks (ask/approval: template suggestion ~3600 · wait: 300 · listen: 600)',
|
|
312
339
|
interval: '--interval SECONDS FALLBACK poll cadence (default 30) — normally unused (WS/long-poll)',
|
|
313
340
|
realtime: '--realtime force the realtime WebSocket (warn + fall back to polling if unavailable)',
|
|
314
341
|
'no-realtime': '--no-realtime polling only (skip the WebSocket)',
|
|
@@ -330,11 +357,14 @@ const OPTION_DOCS = {
|
|
|
330
357
|
window: '--window N reachability window in seconds (default 30)',
|
|
331
358
|
'quiet-nag': '--quiet-nag silence the "server has new capabilities" nag for this run',
|
|
332
359
|
};
|
|
333
|
-
// Content flags shared by
|
|
360
|
+
// Content flags shared by every send.
|
|
334
361
|
const CONTENT_OPTS = ['title', 'body', 'body-markdown', 'subtitle', 'template', 'profile',
|
|
335
362
|
'event-at', 'lead-minutes', 'urgency', 'image', 'file', 'url', 'copy', 'actions',
|
|
336
363
|
'custom-action', 'deliver-at', 'reply-to', 'correlation-id', 'thread', 'after',
|
|
337
364
|
'collapse-key', 'param'];
|
|
365
|
+
// Typed sends also carry the RESPONSE axis: --wait (block on the answer) + the
|
|
366
|
+
// blocking knobs. (`live` is status-only — it never answers, so it skips these.)
|
|
367
|
+
const SEND_OPTS = [...CONTENT_OPTS, 'wait', 'timeout', 'interval', 'realtime', 'no-realtime'];
|
|
338
368
|
|
|
339
369
|
const HELP = {
|
|
340
370
|
setup: {
|
|
@@ -359,47 +389,72 @@ const HELP = {
|
|
|
359
389
|
body: 'A thin wrapper over `ask --template onboarding` with friendly default copy. Run it as your FIRST contact on a fresh channel.',
|
|
360
390
|
opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
|
|
361
391
|
},
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
392
|
+
// AXIS 1 — the married catalog of 5 (perfis-S1/S2). The TYPE you pick IS how the
|
|
393
|
+
// human configured it to arrive. RESPONSE (--actions/--wait) composes on any of them.
|
|
394
|
+
message: {
|
|
395
|
+
summary: 'just inform — passive info the human reads when they want; no action (clears when they OPEN it).',
|
|
396
|
+
usage: 'pidge message --title TEXT [--body TEXT | --body-markdown MD] [--image PATH] [--url URL]',
|
|
397
|
+
body: 'Fire-and-forget by default (stdout is the raw 201). Use it for logs, registros and neutral summaries. Need a decision? add --actions + --wait, or use `pidge important`/`pidge approval`. (Replaces the old `fyi`.)',
|
|
398
|
+
opts: [...SEND_OPTS],
|
|
367
399
|
},
|
|
368
|
-
|
|
369
|
-
summary: '
|
|
370
|
-
usage: 'pidge
|
|
371
|
-
body: 'Fire-and-forget
|
|
372
|
-
opts: [...
|
|
400
|
+
important: {
|
|
401
|
+
summary: '⭐ the DEFAULT — a pendency the human should resolve ("waiting-for-you" card; clears on Done).',
|
|
402
|
+
usage: 'pidge important --title TEXT [--actions yes,no,reply] [--wait] [--body-markdown MD]',
|
|
403
|
+
body: 'Fire-and-forget by default; add --actions/--custom-action for quick-tap buttons and --wait to block until the human answers (prints chosen_action JSON). The most-used type — on the fence between informing and asking, pick this. (Replaces the old `report`.)',
|
|
404
|
+
opts: [...SEND_OPTS],
|
|
373
405
|
},
|
|
374
|
-
|
|
375
|
-
summary: '
|
|
376
|
-
usage: 'pidge
|
|
377
|
-
body: '
|
|
378
|
-
opts: [...
|
|
406
|
+
urgent: {
|
|
407
|
+
summary: 'breaks through silent/Focus; --escalate forces an AlarmKit alarm. Use for the real and inadiável (<1/day).',
|
|
408
|
+
usage: 'pidge urgent --title TEXT [--escalate] [--actions yes,no] [--wait]',
|
|
409
|
+
body: 'A contract of trust: reserve it for what truly can\'t wait. --escalate asks for an AlarmKit alarm that rings through silent + Focus (the human\'s settings still decide). Once DELIVERED an urgent only stops when answered — you can\'t abort it. (Replaces the old `alert`.)',
|
|
410
|
+
opts: [...SEND_OPTS, 'escalate'],
|
|
379
411
|
},
|
|
380
412
|
event: {
|
|
381
|
-
summary: '
|
|
413
|
+
summary: 'a scheduled thing with a known time — countdown Live Activity (needs --event-at).',
|
|
382
414
|
usage: 'pidge event --title TEXT --event-at ISO8601 [--lead-minutes N] [--body-markdown MD]',
|
|
383
415
|
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: [...
|
|
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'],
|
|
416
|
+
opts: [...SEND_OPTS],
|
|
391
417
|
},
|
|
392
418
|
live: {
|
|
393
|
-
summary: 'track an in-flight task (deploy/build/trip) with incremental updates (
|
|
419
|
+
summary: 'track an in-flight task (deploy/build/trip) with incremental updates (Live Activity). Status-only — never answers.',
|
|
394
420
|
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.',
|
|
421
|
+
body: 'Fire-and-forget. Records the live type; the LA-as-primitive is being built — today the send is delivered as a normal notification. Use judgement, not a recipe: show what the human WANTS to watch evolve.',
|
|
396
422
|
opts: [...CONTENT_OPTS],
|
|
397
423
|
},
|
|
424
|
+
// AXIS 2 — the two response shortcuts (bundle a type + buttons + --wait).
|
|
425
|
+
ask: {
|
|
426
|
+
summary: 'a DECISION — = important + --wait; needs --actions. Blocks until the human answers (prints chosen_action JSON).',
|
|
427
|
+
usage: 'pidge ask --title TEXT --actions yes,no,reply [--reply-to URL] [options]',
|
|
428
|
+
body: 'Shorthand for `important --wait` that REQUIRES a way to answer — --actions (catalog or JSON), --custom-action, or a --template that supplies them. Holds a WebSocket (or polls) until a TERMINAL answer; a snooze/reschedule re-fires (ask keeps waiting, prints snooze_until). `live` is refused (it never answers).',
|
|
429
|
+
opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
|
|
430
|
+
},
|
|
431
|
+
approval: {
|
|
432
|
+
summary: 'a go/no-go RECIPE — = important + Approve/Reject + Face ID on Approve + --wait.',
|
|
433
|
+
usage: 'pidge approval --title TEXT [--body-markdown MD] [options]',
|
|
434
|
+
body: 'The easy shortcut for an explicit approval: injects an Approve (Face-ID gated) / Reject pair and blocks on the answer. Pass your own --actions/--custom-action to override the default pair. A gated action is detail-screen only (the banner shows no quick buttons by design — gotcha #19).',
|
|
435
|
+
opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
|
|
436
|
+
},
|
|
437
|
+
// COMPAT aliases — old names map to the new type (kept so scripts don't break).
|
|
438
|
+
fyi: {
|
|
439
|
+
summary: 'COMPAT alias of `pidge message` (renamed in 0.14 — the married catalog). Still works; prefer `message`.',
|
|
440
|
+
usage: 'pidge fyi … (→ pidge message …)',
|
|
441
|
+
opts: [...SEND_OPTS],
|
|
442
|
+
},
|
|
443
|
+
report: {
|
|
444
|
+
summary: 'COMPAT alias of `pidge important` (renamed in 0.14). Still works; prefer `important`.',
|
|
445
|
+
usage: 'pidge report … (→ pidge important …)',
|
|
446
|
+
opts: [...SEND_OPTS],
|
|
447
|
+
},
|
|
448
|
+
alert: {
|
|
449
|
+
summary: 'COMPAT alias of `pidge urgent` (renamed in 0.14). Still works; prefer `urgent`.',
|
|
450
|
+
usage: 'pidge alert … (→ pidge urgent …)',
|
|
451
|
+
opts: [...SEND_OPTS, 'escalate'],
|
|
452
|
+
},
|
|
398
453
|
notify: {
|
|
399
|
-
summary: 'DEPRECATED
|
|
454
|
+
summary: 'DEPRECATED — send WITHOUT a type; the server falls back to its default. Use a typed send instead.',
|
|
400
455
|
usage: 'pidge notify [options]',
|
|
401
|
-
body: 'Kept for
|
|
402
|
-
opts: [...
|
|
456
|
+
body: 'Kept for compat — it warns and still sends (no template_kind; the server picks the channel default). Prefer `pidge message/important/urgent/event/live` (or the `ask`/`approval` shortcuts).',
|
|
457
|
+
opts: [...SEND_OPTS],
|
|
403
458
|
},
|
|
404
459
|
wait: {
|
|
405
460
|
summary: 'block on an already-sent notification until it is answered (prints chosen_action JSON).',
|
|
@@ -504,7 +559,7 @@ function fetchT(url, opts = {}, timeoutMs = 30000) {
|
|
|
504
559
|
// The server advertises its manifest version on every response. When it's newer
|
|
505
560
|
// than what this CLI shipped knowing, nudge on stderr — the agent re-reads the
|
|
506
561
|
// manifest (whats_new) and learns the new capabilities without polling.
|
|
507
|
-
const KNOWN_MANIFEST_VERSION =
|
|
562
|
+
const KNOWN_MANIFEST_VERSION = 42;
|
|
508
563
|
const NAG_TTL_MS = 24 * 60 * 60 * 1000; // #241: at most one nag per 24 h
|
|
509
564
|
let newsWarned = false;
|
|
510
565
|
|
|
@@ -541,9 +596,13 @@ function checkManifestNews(res) {
|
|
|
541
596
|
}
|
|
542
597
|
newsWarned = true;
|
|
543
598
|
writeState({ manifestVersion: { value: ver, seenAt: new Date().toISOString() } });
|
|
544
|
-
// #
|
|
545
|
-
//
|
|
546
|
-
|
|
599
|
+
// #26: pidge is a THIN PIPE — a server manifest bump almost never needs a CLI
|
|
600
|
+
// release, because --param carries any new /notify field NOW. So the nudge is
|
|
601
|
+
// "new capabilities + how to use them today", NOT "your CLI is stale, update it".
|
|
602
|
+
// #249-A: the manifest is PUBLIC — the curl reads the catalog without a key
|
|
603
|
+
// (a key only adds your channel's own config). Updating the CLI is the LAST,
|
|
604
|
+
// optional step (only to gain native flags), never the headline.
|
|
605
|
+
console.error(`pidge: the server has NEW capabilities (manifest v${ver}; this CLI knows v${KNOWN_MANIFEST_VERSION}) — pidge is a thin pipe, so you can use any new /notify field RIGHT NOW via --param KEY=VALUE. Read the catalog (whats_new) in the public manifest: curl $PIDGE_URL/api/v1/manifest (public; add -H "Authorization: Bearer $PIDGE_TOKEN" to also see your channel's config). Updating the CLI only matters to gain native flags: npx pidge-cli@latest (a pinned ref never self-updates). Silence this with --quiet-nag or PIDGE_QUIET_NAG=1.`);
|
|
547
606
|
}
|
|
548
607
|
|
|
549
608
|
// ---------------------------------------------------------------------------
|
|
@@ -641,7 +700,9 @@ function cableSubscribe({ channel, onUp, onFrame, onDown, base = BASE, token = T
|
|
|
641
700
|
if (f.identifier === identifier && f.message) onFrame(f.message);
|
|
642
701
|
};
|
|
643
702
|
ws.onerror = () => { /* onclose follows with the code */ };
|
|
644
|
-
|
|
703
|
+
// #25: the reconnect log prefixes "realtime socket …", so the reason must NOT
|
|
704
|
+
// start with "socket" again (was "socket socket closed (1006)").
|
|
705
|
+
ws.onclose = (e) => die(`closed (${e.code})`);
|
|
645
706
|
return { close: () => { closed = true; clearInterval(beatCheck); try { ws.close(); } catch { /* noop */ } } };
|
|
646
707
|
}
|
|
647
708
|
|
|
@@ -651,7 +712,8 @@ function cableSubscribe({ channel, onUp, onFrame, onDown, base = BASE, token = T
|
|
|
651
712
|
// `finish(reason)` to end the session (e.g. when the answer landed over HTTP).
|
|
652
713
|
// Resolves 'deadline' | 'ws-unavailable'.
|
|
653
714
|
async function cableSession({ channel, deadline, onUp, onFrame }) {
|
|
654
|
-
let wsFails = 0;
|
|
715
|
+
let wsFails = 0; // consecutive drops SINCE the last healthy connect — the degrade gate
|
|
716
|
+
let wsReconnects = 0; // monotonic total this session — what we DISPLAY (never reset)
|
|
655
717
|
while (Date.now() < deadline) {
|
|
656
718
|
const outcome = await new Promise((resolve) => {
|
|
657
719
|
let sub = null;
|
|
@@ -674,12 +736,17 @@ async function cableSession({ channel, deadline, onUp, onFrame }) {
|
|
|
674
736
|
if (outcome === 'deadline') return 'deadline';
|
|
675
737
|
if (!outcome.startsWith('down: ')) return outcome; // caller-driven finish (e.g. 'answered')
|
|
676
738
|
wsFails++;
|
|
739
|
+
wsReconnects++;
|
|
677
740
|
const MAX_WS_FAILS = 4; // then fall back to polling for the rest of the session
|
|
678
741
|
if (wsFails >= MAX_WS_FAILS) return 'ws-unavailable';
|
|
679
742
|
// env override = a test/ops hook (keeps the forced-1006 degrade test fast)
|
|
680
743
|
const base = parseInt(process.env.PIDGE_WS_BACKOFF_MS || '2000', 10) || 2000;
|
|
681
744
|
const backoff = Math.min(base * wsFails, base * 5);
|
|
682
|
-
|
|
745
|
+
// #25: show the MONOTONIC reconnect count, not the consecutive-fail counter —
|
|
746
|
+
// a connect→drop FLAP resets wsFails (onUp forgives a healthy connect), so the
|
|
747
|
+
// old "attempt 1/4" repeated forever and looked like a stuck loop. The cumulative
|
|
748
|
+
// "#N" visibly advances; the polling fallback is spelled out so the ceiling is clear.
|
|
749
|
+
console.error(`pidge: realtime socket ${outcome.replace('down: ', '')} — reconnecting in ${Math.round(backoff / 1000)}s (reconnect #${wsReconnects}; falls back to polling after ${MAX_WS_FAILS} consecutive failures)`);
|
|
683
750
|
await sleep(backoff);
|
|
684
751
|
}
|
|
685
752
|
return 'deadline';
|
|
@@ -920,23 +987,80 @@ async function doNotify(extra = {}) {
|
|
|
920
987
|
return { ok, info, raw };
|
|
921
988
|
}
|
|
922
989
|
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
990
|
+
// The RESPONSE axis (perfis-S2): true when the send carries SOME way for the human
|
|
991
|
+
// to answer with a tap — built-in actions, custom actions, or a content --template
|
|
992
|
+
// that supplies them. Free-text reply is ALWAYS available, so this is only about
|
|
993
|
+
// buttons. `ask` requires it; `approval` injects a default pair when it's absent.
|
|
994
|
+
const hasAnswerAffordance = () =>
|
|
995
|
+
v.actions !== undefined || (v['custom-action'] || []).length > 0 || v.template !== undefined;
|
|
996
|
+
|
|
997
|
+
// The `approval` recipe's default button pair (perfis-S2 follow-up). Sent as
|
|
998
|
+
// CUSTOM actions, NOT built-ins: only custom_actions can carry `biometric` (Face
|
|
999
|
+
// ID), and a custom id may NOT reuse a built-in id like approve/reject (the server
|
|
1000
|
+
// 422s "collides with a built-in") — so the ids are grant/deny. Face ID gates the
|
|
1001
|
+
// consequential "Approve"; "Reject" is the safe (destructive-styled) out. A gated
|
|
1002
|
+
// action is detail-screen only (no banner buttons — gotcha #19), by design.
|
|
1003
|
+
const APPROVAL_ACTIONS = [
|
|
1004
|
+
{ id: 'grant', label: 'Approve', biometric: true, terminal: true },
|
|
1005
|
+
{ id: 'deny', label: 'Reject', style: 'destructive', terminal: true },
|
|
1006
|
+
];
|
|
1007
|
+
|
|
1008
|
+
// The married catalog of 5 (perfis-S1): one send, stamped with the canonical
|
|
1009
|
+
// `template_kind` (message/important/urgent/event/live). The RESPONSE axis is
|
|
1010
|
+
// orthogonal: with `wait:false` it's fire-and-forget (print the raw 201, exit);
|
|
1011
|
+
// with `wait:true` it mints a cid, sends, and BLOCKS until a terminal answer
|
|
1012
|
+
// (print chosen_action JSON). `requireAnswerable` gates `ask`. `extra` carries
|
|
1013
|
+
// raw fields (urgent's escalate:true, approval's injected custom_actions).
|
|
1014
|
+
async function doTypedSend(kind, { wait = false, extra = {}, requireAnswerable = false, label = kind } = {}) {
|
|
1015
|
+
if (!v.title) die('pidge: --title is required', 1);
|
|
1016
|
+
// `live` is status-only — it never produces an answer, so --wait would block the
|
|
1017
|
+
// full timeout believing the human is deciding. Refuse it (mirror the old ask guard).
|
|
1018
|
+
if (wait && (kind === 'live' || v.profile === 'tracking'))
|
|
1019
|
+
die(`pidge: \`${label}\`${kind === 'live' ? '' : ' --profile tracking'} can't --wait — ${kind === 'live' ? '`live` is' : 'tracking is'} status-only and never produces an answer (drop --wait, or ask with a real type)`, 1);
|
|
1020
|
+
if (requireAnswerable && !hasAnswerAffordance())
|
|
1021
|
+
die(`pidge: --actions required for ${label}. Use --actions yes,no (or approve,reject), --custom-action, or a --template that supplies them.`, 1);
|
|
1022
|
+
|
|
1023
|
+
if (!wait) {
|
|
1024
|
+
const { ok, info, raw } = await doNotify({ template_kind: kind, ...extra });
|
|
1025
|
+
console.log(raw);
|
|
1026
|
+
if (ok && info.correlation_id)
|
|
1027
|
+
console.error(`pidge: correlation_id=${info.correlation_id} (use: pidge wait ${info.correlation_id})`);
|
|
1028
|
+
process.exit(ok ? 0 : 2);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// --wait: the cid is minted CLIENT-side when not given, and printed as the FIRST
|
|
1032
|
+
// stderr line (greppable) — a killed/crashed wait always leaves the handle behind,
|
|
1033
|
+
// so the agent can `pidge wait <cid>` instead of re-sending.
|
|
1034
|
+
const cid = v['correlation-id'] || crypto.randomUUID();
|
|
1035
|
+
v['correlation-id'] = cid;
|
|
1036
|
+
console.error(`pidge: correlation_id=${cid}`);
|
|
1037
|
+
const { ok, info } = await doNotify({ template_kind: kind, ...extra });
|
|
1038
|
+
if (!ok) process.exit(2);
|
|
1039
|
+
console.error(`pidge: sent (${info.registered_devices} device(s)) — waiting on ${cid}`);
|
|
1040
|
+
// #132: no --timeout ⇒ obey the template's suggestion from the 201 echo (human
|
|
1041
|
+
// decisions take 30-40 min; a 600 s default misreads them as silence). Explicit wins.
|
|
1042
|
+
let timeout = num(v.timeout, NaN);
|
|
1043
|
+
if (!Number.isFinite(timeout)) {
|
|
1044
|
+
if (info.suggested_ask_timeout) {
|
|
1045
|
+
timeout = info.suggested_ask_timeout;
|
|
1046
|
+
console.error(`pidge: timeout ${Math.round(timeout / 60)} min — suggested by template ${info.template || v.template} (override with --timeout)`);
|
|
1047
|
+
} else {
|
|
1048
|
+
timeout = 600;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
await waitForAnswer(cid, { timeout, interval: num(v.interval, 30) });
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// A compat alias (perfis-S1): the OLD type name still works, mapped to the new
|
|
1055
|
+
// canonical one — a one-line note points at the rename so muscle-memory migrates.
|
|
1056
|
+
function warnRenamed(oldName, newName) {
|
|
1057
|
+
console.error(`pidge: \`pidge ${oldName}\` was renamed → use \`pidge ${newName}\` (the married catalog of 5; the alias keeps working).`);
|
|
933
1058
|
}
|
|
934
1059
|
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
// (soft-rollout). 0.14 will 422 a typeless send. The warning is local (stderr).
|
|
1060
|
+
// `pidge notify` / `pidge send` (no type) are deprecated — they still send, and the
|
|
1061
|
+
// server falls back to the channel default. Prefer a typed send. Warning is local.
|
|
938
1062
|
function warnDeprecatedSend(name) {
|
|
939
|
-
console.error(`pidge: \`pidge ${name}\` is deprecated — use a TYPE instead:
|
|
1063
|
+
console.error(`pidge: \`pidge ${name}\` is deprecated — use a TYPE instead: message · important · urgent · event · live (or the ask/approval shortcuts; see \`pidge help\`). It still sends (no template_kind ⇒ the server picks the channel default).`);
|
|
940
1064
|
}
|
|
941
1065
|
|
|
942
1066
|
// Poll GET /notifications/:cid until a TERMINAL answer, print chosen_action JSON to
|
|
@@ -1555,7 +1679,7 @@ async function runSkillInstall() {
|
|
|
1555
1679
|
const exits = (m.cli && m.cli.output) || '';
|
|
1556
1680
|
const skill = `---
|
|
1557
1681
|
name: pidge
|
|
1558
|
-
description: Send rich, actionable iPhone notifications to your human and get their decision back (Pidge). Use when finishing long tasks
|
|
1682
|
+
description: Send rich, actionable iPhone notifications to your human and get their decision back (Pidge). Pick a type (message/important/urgent/event/live) and, orthogonally, a response (buttons + send-and-go vs wait). Use when finishing long tasks, needing a decision/approval, sending updates with substance, or anything time-anchored. Also covers reading the human's replies/messages back.
|
|
1559
1683
|
---
|
|
1560
1684
|
|
|
1561
1685
|
# Pidge — notify your human, get answers back
|
|
@@ -1564,23 +1688,43 @@ Generated from manifest v${m.manifest_version} of ${BASE} — re-run \`pidge ski
|
|
|
1564
1688
|
|
|
1565
1689
|
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).
|
|
1566
1690
|
|
|
1567
|
-
##
|
|
1568
|
-
|
|
1569
|
-
Every send needs a type. Pick by intent:
|
|
1691
|
+
## Two axes: the TYPE + the RESPONSE
|
|
1570
1692
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
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..." |
|
|
1693
|
+
You and your human speak the SAME language. You pick ONE **type** (how much it may
|
|
1694
|
+
intrude — the human already configured how each arrives); then, ORTHOGONALLY, you
|
|
1695
|
+
decide the **response** (buttons? wait or not?).
|
|
1579
1696
|
|
|
1580
|
-
|
|
1581
|
-
without a type — in 0.14 it'll 422. (In 0.13.x it warns locally + server falls back to fyi.)
|
|
1697
|
+
### Axis 1 — the type (one married list of 5)
|
|
1582
1698
|
|
|
1583
|
-
|
|
1699
|
+
| You want to... | Use | The human sees / clears when |
|
|
1700
|
+
|---|---|---|
|
|
1701
|
+
| just inform, no action | \`pidge message\` | quiet banner; clears when they OPEN it |
|
|
1702
|
+
| a pendency they should resolve ⭐ DEFAULT | \`pidge important\` | "waiting-for-you" card; clears on **Done** |
|
|
1703
|
+
| a go/no-go DECISION (approve/choose) | \`pidge approval\` | Approve/Reject + **Face ID**; clears when they decide |
|
|
1704
|
+
| a thing with a known TIME | \`pidge event --event-at <ISO>\` | countdown + reminder; passed / Done |
|
|
1705
|
+
| TRACK something live | \`pidge live\` | Live Activity on the lock; you end it |
|
|
1706
|
+
| WAKE them now (rare, real) | \`pidge urgent\` | **alarm** through silent/Focus; Done cuts it |
|
|
1707
|
+
|
|
1708
|
+
⭐ \`important\` is the default — on the fence between informing and asking, pick it.
|
|
1709
|
+
(Forget \`fyi\`/\`report\` — they're gone; every send is title + markdown, only the
|
|
1710
|
+
DELIVERY differs. The old names still work as aliases → message/important/urgent.)
|
|
1711
|
+
|
|
1712
|
+
### Axis 2 — the response (composes on ANY type)
|
|
1713
|
+
|
|
1714
|
+
"Asking for a reply" is separate from the type — you don't need \`approval\` to get a button:
|
|
1715
|
+
- **Free text** → ALWAYS available; the human can write back on any notification.
|
|
1716
|
+
- **Buttons** → optional, any type: \`--actions yes,no\` (catalog) or \`--custom-action\` (e.g. \`confirm/postpone\`).
|
|
1717
|
+
- **Face ID** → \`:biometric\` locks a sensitive button (\`approval\` turns it on by default). A flag, not a type.
|
|
1718
|
+
- **send-and-go vs wait** — the choice that decides how YOU work:
|
|
1719
|
+
- **send-and-go** (fire and continue): the answer arrives later in \`pidge listen --all\`. For a turn-based agent.
|
|
1720
|
+
- **wait** (block until they tap): \`--wait\` (or \`pidge ask\`). For when you can't proceed without the decision.
|
|
1721
|
+
- \`approval\` is a RECIPE, not magic: = \`important\` + Approve/Reject + Face ID + \`--wait\`.
|
|
1722
|
+
|
|
1723
|
+
Need a TYPED reply (a time/value/name)? \`--actions reply\` ALONE — never yes/no+reply
|
|
1724
|
+
together (the human taps the easy button and you get a useless "Yes"). ONE question per send.
|
|
1725
|
+
|
|
1726
|
+
Available subcommands: \`pidge message · important · urgent · event · live\` (+ the
|
|
1727
|
+
\`ask\`/\`approval\` shortcuts; \`fyi/report/alert\` aliases; \`notify\` deprecated). Run \`pidge <type> --help\` for each one's flags.
|
|
1584
1728
|
|
|
1585
1729
|
## Pick the right send (decision table)
|
|
1586
1730
|
|
|
@@ -1660,12 +1804,21 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
1660
1804
|
await runSkillInstall();
|
|
1661
1805
|
break;
|
|
1662
1806
|
}
|
|
1663
|
-
//
|
|
1664
|
-
//
|
|
1665
|
-
//
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1807
|
+
// === AXIS 1 — the married catalog of 5 (perfis-S1/S2). Each stamps the
|
|
1808
|
+
// canonical template_kind. AXIS 2 (response) is orthogonal: --actions/
|
|
1809
|
+
// --custom-action add buttons, --wait blocks on the answer (else fire-and-
|
|
1810
|
+
// forget). notify/send = the deprecated typeless path; ask/approval = the
|
|
1811
|
+
// two shortcuts that bundle a type + response. ===
|
|
1812
|
+
case 'message':
|
|
1813
|
+
await doTypedSend('message', { wait: !!v.wait });
|
|
1814
|
+
break;
|
|
1815
|
+
case 'important':
|
|
1816
|
+
await doTypedSend('important', { wait: !!v.wait });
|
|
1817
|
+
break;
|
|
1818
|
+
case 'urgent':
|
|
1819
|
+
// --escalate ⇒ escalate:true (ask the Urgente profile for an AlarmKit alarm
|
|
1820
|
+
// that breaks through silent/Focus; the human's profile still decides).
|
|
1821
|
+
await doTypedSend('urgent', { wait: !!v.wait, extra: v.escalate ? { escalate: true } : {} });
|
|
1669
1822
|
break;
|
|
1670
1823
|
case 'event': {
|
|
1671
1824
|
// event needs a TIME — validate locally (ISO8601) so the agent fails fast
|
|
@@ -1674,17 +1827,37 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
1674
1827
|
die('pidge: --event-at required for event. Use ISO8601: --event-at 2026-06-26T14:00-03:00', 1);
|
|
1675
1828
|
if (Number.isNaN(Date.parse(v['event-at'])))
|
|
1676
1829
|
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
|
|
1830
|
+
await doTypedSend('event', { wait: !!v.wait });
|
|
1678
1831
|
break;
|
|
1679
1832
|
}
|
|
1833
|
+
case 'live':
|
|
1834
|
+
// status-only — pass --wait through so doTypedSend REFUSES it loudly (it
|
|
1835
|
+
// never produces an answer); without --wait it's fire-and-forget.
|
|
1836
|
+
await doTypedSend('live', { wait: !!v.wait });
|
|
1837
|
+
break;
|
|
1838
|
+
// --- compat aliases (perfis-S1): old type names → the new canonical 5. They
|
|
1839
|
+
// map to the new template_kind and still honor --wait/--actions, so scripts
|
|
1840
|
+
// and muscle-memory keep working; a one-line note points at the new name.
|
|
1841
|
+
case 'fyi':
|
|
1842
|
+
warnRenamed('fyi', 'message');
|
|
1843
|
+
await doTypedSend('message', { wait: !!v.wait, label: 'fyi' });
|
|
1844
|
+
break;
|
|
1845
|
+
case 'report':
|
|
1846
|
+
warnRenamed('report', 'important');
|
|
1847
|
+
await doTypedSend('important', { wait: !!v.wait, label: 'report' });
|
|
1848
|
+
break;
|
|
1680
1849
|
case 'alert':
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
await doTypedNotify('alert', v.escalate ? { escalate: true } : {});
|
|
1850
|
+
warnRenamed('alert', 'urgent');
|
|
1851
|
+
await doTypedSend('urgent', { wait: !!v.wait, extra: v.escalate ? { escalate: true } : {}, label: 'alert' });
|
|
1684
1852
|
break;
|
|
1685
|
-
|
|
1686
|
-
|
|
1853
|
+
// `approval` = the RECIPE (perfis-S2 follow-up): important + Approve/Reject
|
|
1854
|
+
// (Face ID on Approve) + --wait. A shortcut for an explicit go/no-go; the human
|
|
1855
|
+
// can override the pair with their own --actions/--custom-action.
|
|
1856
|
+
case 'approval': {
|
|
1857
|
+
const extra = hasAnswerAffordance() ? {} : { custom_actions: APPROVAL_ACTIONS };
|
|
1858
|
+
await doTypedSend('important', { wait: true, extra, label: 'approval' });
|
|
1687
1859
|
break;
|
|
1860
|
+
}
|
|
1688
1861
|
case 'notify':
|
|
1689
1862
|
case 'send': {
|
|
1690
1863
|
warnDeprecatedSend(command);
|
|
@@ -1721,40 +1894,11 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
1721
1894
|
break;
|
|
1722
1895
|
}
|
|
1723
1896
|
case 'ask': {
|
|
1724
|
-
//
|
|
1725
|
-
//
|
|
1726
|
-
//
|
|
1727
|
-
|
|
1728
|
-
|
|
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);
|
|
1735
|
-
// The cid is minted CLIENT-side when not given, and printed as the FIRST
|
|
1736
|
-
// stderr line (greppable) — a killed/crashed ask always leaves the handle
|
|
1737
|
-
// behind, so the agent can `pidge wait <cid>` instead of re-sending.
|
|
1738
|
-
const cid = v['correlation-id'] || crypto.randomUUID();
|
|
1739
|
-
v['correlation-id'] = cid;
|
|
1740
|
-
console.error(`pidge: correlation_id=${cid}`);
|
|
1741
|
-
const { ok, info } = await doNotify({ template_kind: 'ask' });
|
|
1742
|
-
if (!ok) process.exit(2);
|
|
1743
|
-
console.error(`pidge: sent (${info.registered_devices} device(s)) — waiting on ${cid}`);
|
|
1744
|
-
// #132: no --timeout ⇒ obey the template's suggestion from the 201 echo
|
|
1745
|
-
// (human decisions take 30-40 min in the wild; a 600 s default misread
|
|
1746
|
-
// them as silence). Explicit --timeout always wins.
|
|
1747
|
-
let timeout = num(v.timeout, NaN);
|
|
1748
|
-
if (!Number.isFinite(timeout)) {
|
|
1749
|
-
if (info.suggested_ask_timeout) {
|
|
1750
|
-
timeout = info.suggested_ask_timeout;
|
|
1751
|
-
const mins = Math.round(timeout / 60);
|
|
1752
|
-
console.error(`pidge: timeout ${mins} min — suggested by template ${info.template || v.template} (override with --timeout)`);
|
|
1753
|
-
} else {
|
|
1754
|
-
timeout = 600;
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
await waitForAnswer(cid, { timeout, interval: num(v.interval, 30) });
|
|
1897
|
+
// `ask` = the preserved shortcut: important + --wait + REQUIRES a way to
|
|
1898
|
+
// answer. There is no `ask` TYPE in the married catalog (manifest v40+) —
|
|
1899
|
+
// asking is "a type + buttons + wait". The legacy alias keeps working because
|
|
1900
|
+
// it always ships with buttons. `live`/tracking is refused (it never answers).
|
|
1901
|
+
await doTypedSend('important', { wait: true, requireAnswerable: true, label: 'ask' });
|
|
1758
1902
|
break;
|
|
1759
1903
|
}
|
|
1760
1904
|
case 'wait': {
|
|
@@ -2040,8 +2184,8 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
2040
2184
|
break;
|
|
2041
2185
|
}
|
|
2042
2186
|
default:
|
|
2043
|
-
//
|
|
2044
|
-
// landing than dumping the whole USAGE on a typo).
|
|
2045
|
-
die(`pidge: unknown subcommand '${command}'.
|
|
2187
|
+
// Name the bad command and point at the married catalog + the two response
|
|
2188
|
+
// shortcuts (a friendlier landing than dumping the whole USAGE on a typo).
|
|
2189
|
+
die(`pidge: unknown subcommand '${command}'. Types: message · important · urgent · event · live (response: --actions/--wait, or the ask/approval shortcuts). pidge --help`, 1);
|
|
2046
2190
|
}
|
|
2047
2191
|
})();
|