pidge-cli 0.13.1 → 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 +29 -0
- package/README.md +73 -26
- package/bin/pidge.js +278 -146
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
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
|
+
|
|
3
32
|
## 0.13.1 — 2026-06-26
|
|
4
33
|
|
|
5
34
|
Polish from an agent E2E (2026-06-26). No breaking changes.
|
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
|
|
|
@@ -932,23 +987,80 @@ async function doNotify(extra = {}) {
|
|
|
932
987
|
return { ok, info, raw };
|
|
933
988
|
}
|
|
934
989
|
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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).`);
|
|
945
1058
|
}
|
|
946
1059
|
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
// (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.
|
|
950
1062
|
function warnDeprecatedSend(name) {
|
|
951
|
-
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).`);
|
|
952
1064
|
}
|
|
953
1065
|
|
|
954
1066
|
// Poll GET /notifications/:cid until a TERMINAL answer, print chosen_action JSON to
|
|
@@ -1567,7 +1679,7 @@ async function runSkillInstall() {
|
|
|
1567
1679
|
const exits = (m.cli && m.cli.output) || '';
|
|
1568
1680
|
const skill = `---
|
|
1569
1681
|
name: pidge
|
|
1570
|
-
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.
|
|
1571
1683
|
---
|
|
1572
1684
|
|
|
1573
1685
|
# Pidge — notify your human, get answers back
|
|
@@ -1576,23 +1688,43 @@ Generated from manifest v${m.manifest_version} of ${BASE} — re-run \`pidge ski
|
|
|
1576
1688
|
|
|
1577
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).
|
|
1578
1690
|
|
|
1579
|
-
##
|
|
1580
|
-
|
|
1581
|
-
Every send needs a type. Pick by intent:
|
|
1691
|
+
## Two axes: the TYPE + the RESPONSE
|
|
1582
1692
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
| Deliver a curated result/digest worth reading now | \`pidge report\` | "Daily standup summary" |
|
|
1587
|
-
| Ask the human a yes/no/choice — block until they answer | \`pidge ask\` | "Approve deploy v3.2?" with \`--actions yes,no\` |
|
|
1588
|
-
| Surface a scheduled thing (with time) | \`pidge event\` | "Sprint review 14h" with \`--event-at ...\` |
|
|
1589
|
-
| Anomaly/error needing attention; add \`--escalate\` for AlarmKit | \`pidge alert\` | "API 503 errors spiked" |
|
|
1590
|
-
| 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?).
|
|
1591
1696
|
|
|
1592
|
-
|
|
1593
|
-
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)
|
|
1594
1698
|
|
|
1595
|
-
|
|
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.
|
|
1596
1728
|
|
|
1597
1729
|
## Pick the right send (decision table)
|
|
1598
1730
|
|
|
@@ -1672,12 +1804,21 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
1672
1804
|
await runSkillInstall();
|
|
1673
1805
|
break;
|
|
1674
1806
|
}
|
|
1675
|
-
//
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
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 } : {} });
|
|
1681
1822
|
break;
|
|
1682
1823
|
case 'event': {
|
|
1683
1824
|
// event needs a TIME — validate locally (ISO8601) so the agent fails fast
|
|
@@ -1686,17 +1827,37 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
1686
1827
|
die('pidge: --event-at required for event. Use ISO8601: --event-at 2026-06-26T14:00-03:00', 1);
|
|
1687
1828
|
if (Number.isNaN(Date.parse(v['event-at'])))
|
|
1688
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);
|
|
1689
|
-
await
|
|
1830
|
+
await doTypedSend('event', { wait: !!v.wait });
|
|
1690
1831
|
break;
|
|
1691
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;
|
|
1692
1849
|
case 'alert':
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
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' });
|
|
1696
1852
|
break;
|
|
1697
|
-
|
|
1698
|
-
|
|
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' });
|
|
1699
1859
|
break;
|
|
1860
|
+
}
|
|
1700
1861
|
case 'notify':
|
|
1701
1862
|
case 'send': {
|
|
1702
1863
|
warnDeprecatedSend(command);
|
|
@@ -1733,40 +1894,11 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
1733
1894
|
break;
|
|
1734
1895
|
}
|
|
1735
1896
|
case 'ask': {
|
|
1736
|
-
//
|
|
1737
|
-
//
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
if (!v.title) die('pidge: --title is required', 1);
|
|
1742
|
-
// #246: an ask DECLARES a decision — it must say HOW the human answers.
|
|
1743
|
-
// --actions (catalog or JSON), --custom-action, or a --template that supplies
|
|
1744
|
-
// them all satisfy it; none ⇒ a local error (the spec's "no hidden default").
|
|
1745
|
-
if (v.actions === undefined && !(v['custom-action'] || []).length && v.template === undefined)
|
|
1746
|
-
die('pidge: --actions required for ask. Use --actions yes,no,reply or a JSON array.', 1);
|
|
1747
|
-
// The cid is minted CLIENT-side when not given, and printed as the FIRST
|
|
1748
|
-
// stderr line (greppable) — a killed/crashed ask always leaves the handle
|
|
1749
|
-
// behind, so the agent can `pidge wait <cid>` instead of re-sending.
|
|
1750
|
-
const cid = v['correlation-id'] || crypto.randomUUID();
|
|
1751
|
-
v['correlation-id'] = cid;
|
|
1752
|
-
console.error(`pidge: correlation_id=${cid}`);
|
|
1753
|
-
const { ok, info } = await doNotify({ template_kind: 'ask' });
|
|
1754
|
-
if (!ok) process.exit(2);
|
|
1755
|
-
console.error(`pidge: sent (${info.registered_devices} device(s)) — waiting on ${cid}`);
|
|
1756
|
-
// #132: no --timeout ⇒ obey the template's suggestion from the 201 echo
|
|
1757
|
-
// (human decisions take 30-40 min in the wild; a 600 s default misread
|
|
1758
|
-
// them as silence). Explicit --timeout always wins.
|
|
1759
|
-
let timeout = num(v.timeout, NaN);
|
|
1760
|
-
if (!Number.isFinite(timeout)) {
|
|
1761
|
-
if (info.suggested_ask_timeout) {
|
|
1762
|
-
timeout = info.suggested_ask_timeout;
|
|
1763
|
-
const mins = Math.round(timeout / 60);
|
|
1764
|
-
console.error(`pidge: timeout ${mins} min — suggested by template ${info.template || v.template} (override with --timeout)`);
|
|
1765
|
-
} else {
|
|
1766
|
-
timeout = 600;
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
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' });
|
|
1770
1902
|
break;
|
|
1771
1903
|
}
|
|
1772
1904
|
case 'wait': {
|
|
@@ -2052,8 +2184,8 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
2052
2184
|
break;
|
|
2053
2185
|
}
|
|
2054
2186
|
default:
|
|
2055
|
-
//
|
|
2056
|
-
// landing than dumping the whole USAGE on a typo).
|
|
2057
|
-
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);
|
|
2058
2190
|
}
|
|
2059
2191
|
})();
|