pidge-cli 0.13.1 → 0.15.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 +35 -0
- package/README.md +73 -26
- package/bin/pidge.js +408 -194
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.0 — #274 CLI redesign (F1)
|
|
4
|
+
|
|
5
|
+
-m/--body-markdown-file input chain, --gated, English hello, --template off the help menu (still accepted), nag knows v46, --wait defaults to 60 min for decisions.
|
|
6
|
+
|
|
7
|
+
F3/F4: skill rewritten (two approval paths, English gold examples, no content_template menu, appendix from v46); setup → skill → hello fuse with graceful-degrade.
|
|
8
|
+
|
|
9
|
+
## 0.14.0 — 2026-06-28
|
|
10
|
+
|
|
11
|
+
The married vocabulary (perfis) — the CLI now speaks the SAME language as the server
|
|
12
|
+
(manifest v42) and the app: ONE list of 5 message types, with RESPONSE as a separate
|
|
13
|
+
axis. No scripts break — the old names keep working as aliases.
|
|
14
|
+
|
|
15
|
+
- **feat:** the typed sends are renamed to the canonical 5 — `pidge message` ·
|
|
16
|
+
`important` · `urgent` · `event` · `live` (message←fyi, important←report, urgent←alert;
|
|
17
|
+
event/live unchanged). `important` is the recommended default. The wire sends the new
|
|
18
|
+
`template_kind`. (perfis-S1)
|
|
19
|
+
- **feat (compat):** the OLD names still work as aliases — `pidge fyi`→message,
|
|
20
|
+
`report`→important, `alert`→urgent — mapped to the new type, with a one-line rename
|
|
21
|
+
note on stderr. Muscle-memory and existing scripts are untouched. (perfis-S1)
|
|
22
|
+
- **feat:** RESPONSE is now its own axis, composing on ANY type — `--actions`/
|
|
23
|
+
`--custom-action` (buttons) + the new **`--wait`** (block until the human answers,
|
|
24
|
+
then print `chosen_action` JSON; without it = fire-and-forget). This is the explicit
|
|
25
|
+
"send-and-go vs wait". (perfis-S2)
|
|
26
|
+
- **feat:** `pidge ask` is now the shortcut for `important --wait` (still REQUIRES a way
|
|
27
|
+
to answer; preserved behavior). There is no `ask` TYPE in the married catalog — asking
|
|
28
|
+
is a type + buttons + wait. (perfis-S2)
|
|
29
|
+
- **feat:** `pidge approval` — a new go/no-go RECIPE = `important` + Approve/Reject +
|
|
30
|
+
Face ID on Approve + `--wait`. Sent as `custom_actions` (only custom actions carry
|
|
31
|
+
`biometric`, and a custom id can't reuse a built-in like approve/reject — so the ids
|
|
32
|
+
are `grant`/`deny`). Pass your own `--actions`/`--custom-action` to override the
|
|
33
|
+
pair. (perfis-S2)
|
|
34
|
+
- **docs:** USAGE, per-command help and the generated `SKILL.md` rewritten around the two
|
|
35
|
+
axes (type + response) — mirrors the human's app, drops the dead fyi/report framing.
|
|
36
|
+
- **chore:** `KNOWN_MANIFEST_VERSION` 36 → 42 (the live server), silencing the news nag.
|
|
37
|
+
|
|
3
38
|
## 0.13.1 — 2026-06-26
|
|
4
39
|
|
|
5
40
|
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
|
|
@@ -96,7 +107,8 @@ const OPTIONS = {
|
|
|
96
107
|
help: { type: 'boolean', short: 'h' },
|
|
97
108
|
title: { type: 'string' },
|
|
98
109
|
body: { type: 'string' },
|
|
99
|
-
'body-markdown': { type: 'string' },
|
|
110
|
+
'body-markdown': { type: 'string', short: 'm' },
|
|
111
|
+
'body-markdown-file': { type: 'string' }, // a path, or "-" to read stdin (#274)
|
|
100
112
|
subtitle: { type: 'string' },
|
|
101
113
|
template: { type: 'string' }, // content/action pattern (manifest `templates`)
|
|
102
114
|
profile: { type: 'string' }, // delivery profile id (manifest `profiles`)
|
|
@@ -104,6 +116,7 @@ const OPTIONS = {
|
|
|
104
116
|
'lead-minutes': { type: 'string' }, // notify/countdown lead before event_at
|
|
105
117
|
urgency: { type: 'string' }, // normal | persistent | alarm (low-level — prefer --profile)
|
|
106
118
|
escalate: { type: 'boolean' }, // #246: alert type — force an AlarmKit alarm (escalate:true)
|
|
119
|
+
gated: { type: 'boolean' }, // #274: one Face-ID confirm action (replaces content_template:sensitive)
|
|
107
120
|
image: { type: 'string' }, // banner+feed image: local path → uploaded; URL → as-is
|
|
108
121
|
file: { type: 'string' }, // real artifact (xlsx/pdf/csv…): local path → uploaded
|
|
109
122
|
url: { type: 'string' }, // deep link the app opens on tap (#45)
|
|
@@ -119,6 +132,9 @@ const OPTIONS = {
|
|
|
119
132
|
param: { type: 'string', multiple: true }, // key=value escape hatch → raw /notify field
|
|
120
133
|
timeout: { type: 'string' },
|
|
121
134
|
interval: { type: 'string' },
|
|
135
|
+
// perfis-S2 response axis: --wait blocks until the human answers (composes on
|
|
136
|
+
// ANY type — send-and-go vs wait). ask/approval imply it.
|
|
137
|
+
wait: { type: 'boolean' },
|
|
122
138
|
// inbox flags (#83)
|
|
123
139
|
pending: { type: 'boolean' },
|
|
124
140
|
summary: { type: 'boolean' },
|
|
@@ -163,14 +179,20 @@ USAGE
|
|
|
163
179
|
narrated LIVE on the lock screen by a 3-stage Live Activity
|
|
164
180
|
(Conectando → toque para confirmar → Concluído ✓). send + wait
|
|
165
181
|
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
|
-
|
|
182
|
+
AXIS 1 — TYPE (the married list of 5; the human configured how each arrives):
|
|
183
|
+
pidge message [options] just inform, no action — clears when the human OPENS it
|
|
184
|
+
pidge important [options] ⭐DEFAULT a pendency the human should resolve ("waiting-for-you" card)
|
|
185
|
+
pidge urgent [options] breaks through silent/Focus; --escalate forces an AlarmKit alarm
|
|
186
|
+
pidge event [options] a scheduled thing — needs --event-at (countdown Live Activity)
|
|
187
|
+
pidge live [options] an in-flight task with incremental updates (Live Activity)
|
|
188
|
+
AXIS 2 — RESPONSE (composes on ANY type above): --actions/--custom-action add
|
|
189
|
+
buttons; text reply is ALWAYS available; --wait blocks until the human answers
|
|
190
|
+
(send-and-go vs --wait). Two shortcuts bundle both axes:
|
|
191
|
+
pidge ask [options] = important + --wait; needs --actions (prints chosen_action JSON)
|
|
192
|
+
pidge approval [options] = important + Approve/Reject + Face ID + --wait (a go/no-go)
|
|
193
|
+
COMPAT aliases (old names still work → mapped to the new type):
|
|
194
|
+
pidge fyi→message · report→important · alert→urgent (event/live unchanged)
|
|
195
|
+
pidge notify [options] DEPRECATED — send without a type; prefer a TYPE above
|
|
174
196
|
pidge wait <correlation_id> [options] block on an already-sent notification
|
|
175
197
|
pidge cancel <correlation_id> cancel a still-scheduled notification (#56)
|
|
176
198
|
pidge inbox [--pending|--summary|--all|--limit N] what you sent: list, pending slice, or counts+latency (#83)
|
|
@@ -217,13 +239,12 @@ OPTIONS (notify / ask)
|
|
|
217
239
|
--body TEXT message shown on the banner
|
|
218
240
|
--body-markdown MD rich body for the tap-through detail screen
|
|
219
241
|
--subtitle TEXT
|
|
220
|
-
--
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
the user's custom profiles. See the manifest's \`profiles\`.
|
|
242
|
+
--gated add a Face-ID confirm on the consequential action (money/deletion)
|
|
243
|
+
--body-markdown-file F read the markdown body from a file (or "-" for stdin)
|
|
244
|
+
--profile ID low-level alias of the TYPE axis (the HUMAN owns what it
|
|
245
|
+
does): message · important · urgent · event · live ·
|
|
246
|
+
the user's custom profiles. Prefer the typed subcommands
|
|
247
|
+
above; an explicit --profile still wins. See the manifest.
|
|
227
248
|
--event-at ISO8601 WHEN the thing happens (a FACT; required by profile event)
|
|
228
249
|
--lead-minutes N notify/start countdown N min before event_at (5–240)
|
|
229
250
|
--urgency LEVEL normal | persistent | alarm (low-level — prefer --profile)
|
|
@@ -233,9 +254,13 @@ OPTIONS (notify / ask)
|
|
|
233
254
|
shares and saves on the phone; uploaded automatically (≤25 MB)
|
|
234
255
|
--url URL deep link the app opens when the user taps (PR, dashboard, log)
|
|
235
256
|
--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
|
|
257
|
+
--actions LIST RESPONSE axis — comma list: yes,no,approve,reject,accept,
|
|
258
|
+
decline,later,done,snooze,reschedule,reply,mute (or a JSON
|
|
259
|
+
array of custom {id,label} objects). Composes on ANY type.
|
|
238
260
|
--custom-action SPEC "id:label[:destructive][:confirm][:biometric][:terminal]" (repeatable)
|
|
261
|
+
--wait RESPONSE axis — block until the human answers (any type),
|
|
262
|
+
then print chosen_action JSON. Without it: fire-and-forget
|
|
263
|
+
(the answer arrives later in \`pidge listen --all\`). ask/approval imply it.
|
|
239
264
|
--deliver-at ISO8601 schedule for later
|
|
240
265
|
--reply-to URL also POST the answer to your webhook (HMAC-signed)
|
|
241
266
|
--correlation-id ID idempotency + routing key (auto-generated if omitted)
|
|
@@ -247,7 +272,8 @@ OPTIONS (notify / ask)
|
|
|
247
272
|
--collapse-key KEY replace/update a prior notification
|
|
248
273
|
--param KEY=VALUE pass ANY raw /notify field (repeatable) — future server
|
|
249
274
|
fields work without a CLI update; the manifest is the contract
|
|
250
|
-
--timeout SECONDS
|
|
275
|
+
--timeout SECONDS how long --wait blocks (ask/approval: template's suggestion,
|
|
276
|
+
~3600 for a decision · wait: 300) — explicit always wins
|
|
251
277
|
--interval SECONDS FALLBACK poll cadence (default 30) — normally unused: WS or
|
|
252
278
|
the server-held long-poll (?wait=25) make answers ~instant
|
|
253
279
|
|
|
@@ -262,17 +288,18 @@ ENV
|
|
|
262
288
|
shared ~/.config/pidge/env (single-agent only).
|
|
263
289
|
|
|
264
290
|
OUTPUT
|
|
265
|
-
stdout is machine-readable (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
291
|
+
stdout is machine-readable (a fire-and-forget send→the raw 201 JSON; a --wait
|
|
292
|
+
send / ask / approval / wait→chosen_action JSON); human notices go to stderr.
|
|
293
|
+
Exit: 0 answered · 3 timed out (no answer yet, not a failure) · 4 timed out
|
|
294
|
+
WITHOUT ONE healthy round-trip all session (the CHANNEL looks broken —
|
|
295
|
+
server/network — not the human ignoring you: surface it instead of retrying
|
|
296
|
+
blindly, #119) · 2 error · 1 usage.
|
|
297
|
+
|
|
298
|
+
Responses are one-and-done EXCEPT snooze/reschedule (they re-fire); a --wait send
|
|
299
|
+
keeps polling through a snooze and prints snooze_until. Follow-up = a NEW
|
|
300
|
+
notification. An over-ceiling type is delivered DEGRADED, never rejected — read
|
|
301
|
+
the 201's degraded/degrade_reason (narrated on stderr). \`live\` is status-only:
|
|
302
|
+
it never produces an answer, so --wait/ask refuse it.
|
|
276
303
|
|
|
277
304
|
Full spec (the contract — always current): GET $PIDGE_URL/api/v1/manifest`;
|
|
278
305
|
|
|
@@ -288,19 +315,21 @@ const OPTION_DOCS = {
|
|
|
288
315
|
title: '--title TEXT (required) the headline',
|
|
289
316
|
body: '--body TEXT the message shown on the banner',
|
|
290
317
|
'body-markdown': '--body-markdown MD rich body for the tap-through detail screen',
|
|
318
|
+
'body-markdown-file': '--body-markdown-file F read the markdown body from a file (or "-" for stdin) — avoids shell-quoting long markdown',
|
|
291
319
|
subtitle: '--subtitle TEXT a secondary line under the title',
|
|
292
|
-
|
|
293
|
-
profile: '--profile ID
|
|
294
|
-
'event-at': '--event-at ISO8601 WHEN the thing happens (required by
|
|
320
|
+
gated: '--gated add a Face-ID confirm on the consequential action (money/deletion). Pair with a louder profile if it must also be loud.',
|
|
321
|
+
profile: '--profile ID low-level alias of the TYPE (the human owns it): message · important · urgent · event · live · custom',
|
|
322
|
+
'event-at': '--event-at ISO8601 WHEN the thing happens (required by event)',
|
|
295
323
|
'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
|
|
324
|
+
urgency: '--urgency LEVEL normal | persistent | alarm (low-level — prefer the typed subcommand)',
|
|
325
|
+
escalate: '--escalate urgent: force an AlarmKit alarm that breaks through silent/Focus',
|
|
298
326
|
image: '--image PATH_OR_URL banner+feed image: a local path is uploaded; an https URL is sent as-is',
|
|
299
327
|
file: '--file PATH a real artifact (xlsx/pdf/csv…) uploaded for the human (≤25 MB)',
|
|
300
328
|
url: '--url URL deep link the app opens on tap (PR, dashboard, log)',
|
|
301
329
|
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',
|
|
330
|
+
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
331
|
'custom-action': '--custom-action SPEC "id:label[:destructive][:confirm][:biometric][:terminal]" (repeatable)',
|
|
332
|
+
wait: '--wait RESPONSE axis: block until the human answers (any type), then print chosen_action JSON (ask/approval imply it)',
|
|
304
333
|
'deliver-at': '--deliver-at ISO8601 schedule the send for later',
|
|
305
334
|
'reply-to': '--reply-to URL also POST the answer to your webhook (HMAC-signed)',
|
|
306
335
|
'correlation-id': '--correlation-id ID idempotency + routing key (auto-generated if omitted)',
|
|
@@ -308,7 +337,7 @@ const OPTION_DOCS = {
|
|
|
308
337
|
after: '--after CID decision queue (#157): held until that notification is answered',
|
|
309
338
|
'collapse-key': '--collapse-key KEY replace/update a prior notification',
|
|
310
339
|
param: '--param KEY=VALUE pass ANY raw /notify field (repeatable) — the manifest is the contract',
|
|
311
|
-
timeout: '--timeout SECONDS how long
|
|
340
|
+
timeout: '--timeout SECONDS how long --wait blocks (ask/approval: template suggestion ~3600 · wait: 300 · listen: 600)',
|
|
312
341
|
interval: '--interval SECONDS FALLBACK poll cadence (default 30) — normally unused (WS/long-poll)',
|
|
313
342
|
realtime: '--realtime force the realtime WebSocket (warn + fall back to polling if unavailable)',
|
|
314
343
|
'no-realtime': '--no-realtime polling only (skip the WebSocket)',
|
|
@@ -330,11 +359,14 @@ const OPTION_DOCS = {
|
|
|
330
359
|
window: '--window N reachability window in seconds (default 30)',
|
|
331
360
|
'quiet-nag': '--quiet-nag silence the "server has new capabilities" nag for this run',
|
|
332
361
|
};
|
|
333
|
-
// Content flags shared by
|
|
334
|
-
const CONTENT_OPTS = ['title', 'body', 'body-markdown', 'subtitle', 'template', 'profile',
|
|
362
|
+
// Content flags shared by every send.
|
|
363
|
+
const CONTENT_OPTS = ['title', 'body', 'body-markdown', 'body-markdown-file', 'subtitle', 'template', 'profile',
|
|
335
364
|
'event-at', 'lead-minutes', 'urgency', 'image', 'file', 'url', 'copy', 'actions',
|
|
336
365
|
'custom-action', 'deliver-at', 'reply-to', 'correlation-id', 'thread', 'after',
|
|
337
366
|
'collapse-key', 'param'];
|
|
367
|
+
// Typed sends also carry the RESPONSE axis: --wait (block on the answer) + the
|
|
368
|
+
// blocking knobs. (`live` is status-only — it never answers, so it skips these.)
|
|
369
|
+
const SEND_OPTS = [...CONTENT_OPTS, 'gated', 'wait', 'timeout', 'interval', 'realtime', 'no-realtime'];
|
|
338
370
|
|
|
339
371
|
const HELP = {
|
|
340
372
|
setup: {
|
|
@@ -356,50 +388,75 @@ const HELP = {
|
|
|
356
388
|
hello: {
|
|
357
389
|
summary: 'first-contact WOW (#217): your channel\'s debut handshake, narrated live by a 3-stage Live Activity. send + wait in one.',
|
|
358
390
|
usage: 'pidge hello [options]',
|
|
359
|
-
body: '
|
|
391
|
+
body: 'First contact on a fresh channel: send the debut handshake and block until your human confirms. The server narrates a 3-stage Live Activity.',
|
|
360
392
|
opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
|
|
361
393
|
},
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
394
|
+
// AXIS 1 — the married catalog of 5 (perfis-S1/S2). The TYPE you pick IS how the
|
|
395
|
+
// human configured it to arrive. RESPONSE (--actions/--wait) composes on any of them.
|
|
396
|
+
message: {
|
|
397
|
+
summary: 'just inform — passive info the human reads when they want; no action (clears when they OPEN it).',
|
|
398
|
+
usage: 'pidge message --title TEXT [--body TEXT | --body-markdown MD] [--image PATH] [--url URL]',
|
|
399
|
+
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`.)',
|
|
400
|
+
opts: [...SEND_OPTS],
|
|
367
401
|
},
|
|
368
|
-
|
|
369
|
-
summary: '
|
|
370
|
-
usage: 'pidge
|
|
371
|
-
body: 'Fire-and-forget
|
|
372
|
-
opts: [...
|
|
402
|
+
important: {
|
|
403
|
+
summary: '⭐ the DEFAULT — a pendency the human should resolve ("waiting-for-you" card; clears on Done).',
|
|
404
|
+
usage: 'pidge important --title TEXT [--actions yes,no,reply] [--wait] [--body-markdown MD]',
|
|
405
|
+
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`.)',
|
|
406
|
+
opts: [...SEND_OPTS],
|
|
373
407
|
},
|
|
374
|
-
|
|
375
|
-
summary: '
|
|
376
|
-
usage: 'pidge
|
|
377
|
-
body: '
|
|
378
|
-
opts: [...
|
|
408
|
+
urgent: {
|
|
409
|
+
summary: 'breaks through silent/Focus; --escalate forces an AlarmKit alarm. Use for the real and inadiável (<1/day).',
|
|
410
|
+
usage: 'pidge urgent --title TEXT [--escalate] [--actions yes,no] [--wait]',
|
|
411
|
+
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`.)',
|
|
412
|
+
opts: [...SEND_OPTS, 'escalate'],
|
|
379
413
|
},
|
|
380
414
|
event: {
|
|
381
|
-
summary: '
|
|
415
|
+
summary: 'a scheduled thing with a known time — countdown Live Activity (needs --event-at).',
|
|
382
416
|
usage: 'pidge event --title TEXT --event-at ISO8601 [--lead-minutes N] [--body-markdown MD]',
|
|
383
417
|
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'],
|
|
418
|
+
opts: [...SEND_OPTS],
|
|
391
419
|
},
|
|
392
420
|
live: {
|
|
393
|
-
summary: 'track an in-flight task (deploy/build/trip) with incremental updates (
|
|
421
|
+
summary: 'track an in-flight task (deploy/build/trip) with incremental updates (Live Activity). Status-only — never answers.',
|
|
394
422
|
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.',
|
|
423
|
+
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
424
|
opts: [...CONTENT_OPTS],
|
|
397
425
|
},
|
|
426
|
+
// AXIS 2 — the two response shortcuts (bundle a type + buttons + --wait).
|
|
427
|
+
ask: {
|
|
428
|
+
summary: 'a DECISION — = important + --wait; needs --actions. Blocks until the human answers (prints chosen_action JSON).',
|
|
429
|
+
usage: 'pidge ask --title TEXT --actions yes,no,reply [--reply-to URL] [options]',
|
|
430
|
+
body: 'Shorthand for important --wait that REQUIRES a way to answer — --actions (catalog or JSON) or --custom-action. Holds a WebSocket (or polls) until a TERMINAL answer; a snooze/reschedule re-fires.',
|
|
431
|
+
opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
|
|
432
|
+
},
|
|
433
|
+
approval: {
|
|
434
|
+
summary: 'a go/no-go RECIPE — = important + Approve/Reject + Face ID on Approve + --wait.',
|
|
435
|
+
usage: 'pidge approval --title TEXT [--body-markdown MD] [options]',
|
|
436
|
+
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).',
|
|
437
|
+
opts: [...CONTENT_OPTS, 'timeout', 'interval', 'realtime', 'no-realtime'],
|
|
438
|
+
},
|
|
439
|
+
// COMPAT aliases — old names map to the new type (kept so scripts don't break).
|
|
440
|
+
fyi: {
|
|
441
|
+
summary: 'COMPAT alias of `pidge message` (renamed in 0.14 — the married catalog). Still works; prefer `message`.',
|
|
442
|
+
usage: 'pidge fyi … (→ pidge message …)',
|
|
443
|
+
opts: [...SEND_OPTS],
|
|
444
|
+
},
|
|
445
|
+
report: {
|
|
446
|
+
summary: 'COMPAT alias of `pidge important` (renamed in 0.14). Still works; prefer `important`.',
|
|
447
|
+
usage: 'pidge report … (→ pidge important …)',
|
|
448
|
+
opts: [...SEND_OPTS],
|
|
449
|
+
},
|
|
450
|
+
alert: {
|
|
451
|
+
summary: 'COMPAT alias of `pidge urgent` (renamed in 0.14). Still works; prefer `urgent`.',
|
|
452
|
+
usage: 'pidge alert … (→ pidge urgent …)',
|
|
453
|
+
opts: [...SEND_OPTS, 'escalate'],
|
|
454
|
+
},
|
|
398
455
|
notify: {
|
|
399
|
-
summary: 'DEPRECATED
|
|
456
|
+
summary: 'DEPRECATED — send WITHOUT a type; the server falls back to its default. Use a typed send instead.',
|
|
400
457
|
usage: 'pidge notify [options]',
|
|
401
|
-
body: 'Kept for
|
|
402
|
-
opts: [...
|
|
458
|
+
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).',
|
|
459
|
+
opts: [...SEND_OPTS],
|
|
403
460
|
},
|
|
404
461
|
wait: {
|
|
405
462
|
summary: 'block on an already-sent notification until it is answered (prints chosen_action JSON).',
|
|
@@ -504,7 +561,7 @@ function fetchT(url, opts = {}, timeoutMs = 30000) {
|
|
|
504
561
|
// The server advertises its manifest version on every response. When it's newer
|
|
505
562
|
// than what this CLI shipped knowing, nudge on stderr — the agent re-reads the
|
|
506
563
|
// manifest (whats_new) and learns the new capabilities without polling.
|
|
507
|
-
const KNOWN_MANIFEST_VERSION =
|
|
564
|
+
const KNOWN_MANIFEST_VERSION = 46;
|
|
508
565
|
const NAG_TTL_MS = 24 * 60 * 60 * 1000; // #241: at most one nag per 24 h
|
|
509
566
|
let newsWarned = false;
|
|
510
567
|
|
|
@@ -781,7 +838,13 @@ function buildBody(extra = {}) {
|
|
|
781
838
|
if (!v.title) die('pidge: --title is required', 1);
|
|
782
839
|
const body = { title: v.title };
|
|
783
840
|
if (v.body !== undefined) body.body = v.body;
|
|
784
|
-
if (v['body-markdown'] !== undefined)
|
|
841
|
+
if (v['body-markdown-file'] !== undefined) {
|
|
842
|
+
body.body_markdown = v['body-markdown-file'] === '-'
|
|
843
|
+
? fs.readFileSync(0, 'utf8')
|
|
844
|
+
: fs.readFileSync(v['body-markdown-file'], 'utf8');
|
|
845
|
+
} else if (v['body-markdown'] !== undefined) {
|
|
846
|
+
body.body_markdown = v['body-markdown'];
|
|
847
|
+
}
|
|
785
848
|
if (v.subtitle !== undefined) body.subtitle = v.subtitle;
|
|
786
849
|
if (v.template !== undefined) body.template = v.template;
|
|
787
850
|
if (v.profile !== undefined) body.profile = v.profile;
|
|
@@ -818,6 +881,15 @@ function buildBody(extra = {}) {
|
|
|
818
881
|
for (const spec of v['custom-action'] || []) customActions.push(customActionFromSpec(spec));
|
|
819
882
|
if (customActions.length) body.custom_actions = customActions;
|
|
820
883
|
|
|
884
|
+
// #274: --gated synthesizes ONE Face-ID confirm on the consequential action
|
|
885
|
+
// (money/deletion) — the replacement for the retired content_template:sensitive.
|
|
886
|
+
// Skip if the agent already supplied a biometric action (don't double-gate).
|
|
887
|
+
if (v.gated && !(body.custom_actions || []).some((c) => c.biometric)) {
|
|
888
|
+
body.custom_actions = (body.custom_actions || []).concat([
|
|
889
|
+
{ id: 'confirm_action', label: 'Confirm', style: 'destructive', confirm: true, biometric: true, terminal: true },
|
|
890
|
+
]);
|
|
891
|
+
}
|
|
892
|
+
|
|
821
893
|
// #246: subcommand-supplied raw fields (template_kind, alert's escalate). Applied
|
|
822
894
|
// before the --param loop so a raw --param can still override in a pinch.
|
|
823
895
|
Object.assign(body, extra);
|
|
@@ -932,23 +1004,83 @@ async function doNotify(extra = {}) {
|
|
|
932
1004
|
return { ok, info, raw };
|
|
933
1005
|
}
|
|
934
1006
|
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1007
|
+
// The RESPONSE axis (perfis-S2): true when the send carries SOME way for the human
|
|
1008
|
+
// to answer with a tap — built-in actions, custom actions, or a content --template
|
|
1009
|
+
// that supplies them. Free-text reply is ALWAYS available, so this is only about
|
|
1010
|
+
// buttons. `ask` requires it; `approval` injects a default pair when it's absent.
|
|
1011
|
+
const hasAnswerAffordance = () =>
|
|
1012
|
+
v.actions !== undefined || (v['custom-action'] || []).length > 0 || v.template !== undefined;
|
|
1013
|
+
|
|
1014
|
+
// The `approval` recipe's default button pair (perfis-S2 follow-up). Sent as
|
|
1015
|
+
// CUSTOM actions, NOT built-ins: only custom_actions can carry `biometric` (Face
|
|
1016
|
+
// ID), and a custom id may NOT reuse a built-in id like approve/reject (the server
|
|
1017
|
+
// 422s "collides with a built-in") — so the ids are grant/deny. Face ID gates the
|
|
1018
|
+
// consequential "Approve"; "Reject" is the safe (destructive-styled) out. A gated
|
|
1019
|
+
// action is detail-screen only (no banner buttons — gotcha #19), by design.
|
|
1020
|
+
const APPROVAL_ACTIONS = [
|
|
1021
|
+
{ id: 'grant', label: 'Approve', biometric: true, terminal: true },
|
|
1022
|
+
{ id: 'deny', label: 'Reject', style: 'destructive', terminal: true },
|
|
1023
|
+
];
|
|
1024
|
+
|
|
1025
|
+
// The married catalog of 5 (perfis-S1): one send, stamped with the canonical
|
|
1026
|
+
// `template_kind` (message/important/urgent/event/live). The RESPONSE axis is
|
|
1027
|
+
// orthogonal: with `wait:false` it's fire-and-forget (print the raw 201, exit);
|
|
1028
|
+
// with `wait:true` it mints a cid, sends, and BLOCKS until a terminal answer
|
|
1029
|
+
// (print chosen_action JSON). `requireAnswerable` gates `ask`. `extra` carries
|
|
1030
|
+
// raw fields (urgent's escalate:true, approval's injected custom_actions).
|
|
1031
|
+
async function doTypedSend(kind, { wait = false, extra = {}, requireAnswerable = false, label = kind } = {}) {
|
|
1032
|
+
if (!v.title) die('pidge: --title is required', 1);
|
|
1033
|
+
// `live` is status-only — it never produces an answer, so --wait would block the
|
|
1034
|
+
// full timeout believing the human is deciding. Refuse it (mirror the old ask guard).
|
|
1035
|
+
if (wait && (kind === 'live' || v.profile === 'tracking'))
|
|
1036
|
+
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);
|
|
1037
|
+
if (requireAnswerable && !hasAnswerAffordance())
|
|
1038
|
+
die(`pidge: --actions required for ${label}. Add buttons with --actions yes,no (or approve,reject) or --custom-action id:label.`, 1);
|
|
1039
|
+
|
|
1040
|
+
if (!wait) {
|
|
1041
|
+
const { ok, info, raw } = await doNotify({ template_kind: kind, ...extra });
|
|
1042
|
+
console.log(raw);
|
|
1043
|
+
if (ok && info.correlation_id)
|
|
1044
|
+
console.error(`pidge: correlation_id=${info.correlation_id} (use: pidge wait ${info.correlation_id})`);
|
|
1045
|
+
process.exit(ok ? 0 : 2);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// --wait: the cid is minted CLIENT-side when not given, and printed as the FIRST
|
|
1049
|
+
// stderr line (greppable) — a killed/crashed wait always leaves the handle behind,
|
|
1050
|
+
// so the agent can `pidge wait <cid>` instead of re-sending.
|
|
1051
|
+
const cid = v['correlation-id'] || crypto.randomUUID();
|
|
1052
|
+
v['correlation-id'] = cid;
|
|
1053
|
+
console.error(`pidge: correlation_id=${cid}`);
|
|
1054
|
+
const { ok, info } = await doNotify({ template_kind: kind, ...extra });
|
|
1055
|
+
if (!ok) process.exit(2);
|
|
1056
|
+
console.error(`pidge: sent (${info.registered_devices} device(s)) — waiting on ${cid}`);
|
|
1057
|
+
// #132: no --timeout ⇒ obey the template's suggestion from the 201 echo (human
|
|
1058
|
+
// decisions take 30-40 min; a 600 s default misreads them as silence). Explicit wins.
|
|
1059
|
+
let timeout = num(v.timeout, NaN);
|
|
1060
|
+
if (!Number.isFinite(timeout)) {
|
|
1061
|
+
if (info.suggested_ask_timeout) {
|
|
1062
|
+
timeout = info.suggested_ask_timeout;
|
|
1063
|
+
console.error(`pidge: timeout ${Math.round(timeout / 60)} min — suggested by template ${info.template || v.template} (override with --timeout)`);
|
|
1064
|
+
} else if (info.requires_action) {
|
|
1065
|
+
timeout = 3600; // #274/#132: a human decision (buttons present) takes 30-40 min, not 600 s of "silence"
|
|
1066
|
+
console.error(`pidge: no template suggestion — defaulting --wait to 60 min for a decision (override with --timeout)`);
|
|
1067
|
+
} else {
|
|
1068
|
+
timeout = 600;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
await waitForAnswer(cid, { timeout, interval: num(v.interval, 30) });
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// A compat alias (perfis-S1): the OLD type name still works, mapped to the new
|
|
1075
|
+
// canonical one — a one-line note points at the rename so muscle-memory migrates.
|
|
1076
|
+
function warnRenamed(oldName, newName) {
|
|
1077
|
+
console.error(`pidge: \`pidge ${oldName}\` was renamed → use \`pidge ${newName}\` (the married catalog of 5; the alias keeps working).`);
|
|
945
1078
|
}
|
|
946
1079
|
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
// (soft-rollout). 0.14 will 422 a typeless send. The warning is local (stderr).
|
|
1080
|
+
// `pidge notify` / `pidge send` (no type) are deprecated — they still send, and the
|
|
1081
|
+
// server falls back to the channel default. Prefer a typed send. Warning is local.
|
|
950
1082
|
function warnDeprecatedSend(name) {
|
|
951
|
-
console.error(`pidge: \`pidge ${name}\` is deprecated — use a TYPE instead:
|
|
1083
|
+
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
1084
|
}
|
|
953
1085
|
|
|
954
1086
|
// Poll GET /notifications/:cid until a TERMINAL answer, print chosen_action JSON to
|
|
@@ -1457,9 +1589,9 @@ async function runDoctor(base = BASE, token = TOKEN, sourceLabel = null) {
|
|
|
1457
1589
|
console.error(`pidge doctor: realtime: INDISPONÍVEL — ${rt.reason}. O \`listen\` degrada pra polling (funciona, menos instantâneo); use --no-realtime pra fixar o piso.`);
|
|
1458
1590
|
}
|
|
1459
1591
|
// #229: lead with `pidge hello` — the first-contact WOW (send + wait in one),
|
|
1460
|
-
// the same debut the /agent-setup guide leads with.
|
|
1461
|
-
// `
|
|
1462
|
-
console.error('pidge doctor: all good — try: pidge hello (first-contact WOW — send + wait in one
|
|
1592
|
+
// the same debut the /agent-setup guide leads with. (#274: no --template hint —
|
|
1593
|
+
// `pidge hello` IS the entry point; the content_template surface is off the menu.)
|
|
1594
|
+
console.error('pidge doctor: all good — try: pidge hello (first-contact WOW — send + wait in one)');
|
|
1463
1595
|
console.log(JSON.stringify({ ok: true, base_url: base, channel: data.channel, devices, manifest_version: data.manifest_version, realtime }));
|
|
1464
1596
|
process.exit(0);
|
|
1465
1597
|
}
|
|
@@ -1526,6 +1658,7 @@ async function runSetup() {
|
|
|
1526
1658
|
console.log(`export PIDGE_URL=${finalBase}`);
|
|
1527
1659
|
console.log(`export PIDGE_TOKEN=${data.key}`);
|
|
1528
1660
|
console.error(`pidge: canal "${channelName}" — modo POR-AGENTE (nada gravado em disco). Cole as duas linhas no ambiente de lançamento DESTE agente (systemd/launcher/cron/profile). Cada agente tem a SUA chave; perdeu, é só pegar outro código no app e re-rodar (a chave do canal é a MESMA). NÃO rode --print de dentro de um agente — a chave apareceria no contexto dele.`);
|
|
1661
|
+
await fuseSkillAndHello(finalBase, data.key);
|
|
1529
1662
|
await runDoctor(finalBase, data.key, 'fresh claim (per-agent env — not stored on disk)');
|
|
1530
1663
|
return;
|
|
1531
1664
|
}
|
|
@@ -1546,102 +1679,180 @@ async function runSetup() {
|
|
|
1546
1679
|
}
|
|
1547
1680
|
if (!AGENT_ID)
|
|
1548
1681
|
console.error('pidge: este é o arquivo COMPARTILHADO (single-agent). Vai rodar 2+ agentes nesta máquina? Dê a cada um PIDGE_AGENT=<id> no launch (arquivo isolado por agente) — senão eles enviam como o mesmo canal.');
|
|
1682
|
+
await fuseSkillAndHello(finalBase, data.key);
|
|
1549
1683
|
await runDoctor(finalBase, data.key, CONFIG_FILE);
|
|
1550
1684
|
}
|
|
1551
1685
|
|
|
1552
|
-
//
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1555
|
-
|
|
1686
|
+
// #274 F4: setup → skill → hello. Best-effort, run right BEFORE the post-setup
|
|
1687
|
+
// doctor (runDoctor process.exit()s, so this can't trail it). A skill-install
|
|
1688
|
+
// failure is ONE stderr line — NEVER a `--help`/USAGE dump (the graceful-degrade
|
|
1689
|
+
// invariant). `pidge hello` stays a printed NEXT step: we don't auto-fire a push
|
|
1690
|
+
// the human didn't ask for. base+key are the freshly-claimed ones (the manifest
|
|
1691
|
+
// is public, so this works even on the --print path where no token is on disk).
|
|
1692
|
+
async function fuseSkillAndHello(base, token) {
|
|
1693
|
+
try {
|
|
1694
|
+
const r = await installSkill(base, token);
|
|
1695
|
+
console.error(`pidge: skill written to ${r.file} (manifest v${r.manifest_version}) — your future sessions in this project know Pidge now`);
|
|
1696
|
+
} catch (e) {
|
|
1697
|
+
console.error(`pidge: skill install skipped (${e.message}) — run \`pidge skill install\` later.`);
|
|
1698
|
+
}
|
|
1699
|
+
console.error('pidge: next → `pidge hello` to send your first handshake and watch it confirm on the lock screen.');
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
// skill install (#110e; rewritten #274 F3): persistent Pidge knowledge for AI
|
|
1703
|
+
// agents — the live manifest's APPENDIX (profiles / notes / exits) wrapped around
|
|
1704
|
+
// a HAND-AUTHORED, failure-mode-first spine. The dead content_template
|
|
1705
|
+
// `decision_table` is NEVER pulled again, so even an old manifest can't reinject
|
|
1706
|
+
// the v46 collision. Non-exiting: RETURNS {file, manifest_version} and THROWS on
|
|
1707
|
+
// failure, so callers (`skill install` AND the setup fuse) choose die-vs-degrade.
|
|
1708
|
+
async function installSkill(base = BASE, token = TOKEN) {
|
|
1709
|
+
const hdrs = { authorization: `Bearer ${token}`, 'content-type': 'application/json' };
|
|
1556
1710
|
let res, m;
|
|
1557
1711
|
try {
|
|
1558
|
-
res = await fetchT(`${
|
|
1712
|
+
res = await fetchT(`${base}/api/v1/manifest`, { headers: hdrs });
|
|
1559
1713
|
m = await res.json();
|
|
1560
1714
|
} catch (e) {
|
|
1561
|
-
|
|
1715
|
+
throw new Error(`could not read the manifest: ${e.message}`);
|
|
1562
1716
|
}
|
|
1563
|
-
if (res.status !== 200)
|
|
1564
|
-
|
|
1717
|
+
if (res.status !== 200) throw new Error(`manifest read failed (${res.status})`);
|
|
1718
|
+
// The ONLY generated parts (the appendix). m.templates.* is deliberately UNREAD.
|
|
1565
1719
|
const profileTable = (m.profiles && m.profiles.decision_table) || [];
|
|
1566
1720
|
const notes = m.notes || [];
|
|
1567
1721
|
const exits = (m.cli && m.cli.output) || '';
|
|
1568
1722
|
const skill = `---
|
|
1569
1723
|
name: pidge
|
|
1570
|
-
description: Send rich, actionable iPhone notifications to your human and get their decision back (Pidge). Use when finishing long tasks
|
|
1724
|
+
description: Send rich, actionable iPhone notifications to your human and get their decision back (Pidge). Every send is a TYPE (message/important/urgent/event/live) plus an OPTIONAL 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 back.
|
|
1571
1725
|
---
|
|
1572
1726
|
|
|
1573
1727
|
# Pidge — notify your human, get answers back
|
|
1574
1728
|
|
|
1575
|
-
Generated from manifest v${m.manifest_version} of ${BASE} — re-run \`pidge skill install\` to update (any API response header X-Pidge-Manifest-Version > ${m.manifest_version} means there's news).
|
|
1729
|
+
Generated from manifest v${m.manifest_version} of ${BASE} — re-run \`pidge skill install\` to update (any API response header \`X-Pidge-Manifest-Version\` > ${m.manifest_version} means there's news).
|
|
1576
1730
|
|
|
1577
|
-
All commands: \`npx pidge-cli …\` (Node ≥18; reads
|
|
1731
|
+
All commands: \`npx pidge-cli …\` (Node ≥18; reads \`~/.config/pidge/env\` — no token in your context). Not set up? Run \`pidge doctor\`. Onboard with \`pidge setup --claim <code>\` (the human copies the code from the Pidge app), then \`pidge hello\`.
|
|
1578
1732
|
|
|
1579
|
-
##
|
|
1733
|
+
## One breath
|
|
1580
1734
|
|
|
1581
|
-
Every send
|
|
1735
|
+
Every send is **a TYPE + a markdown body + an OPTIONAL response**. The TYPE (one of five) decides how much it may intrude — the human already configured how each arrives. The RESPONSE (buttons? wait or not?) is a second, orthogonal axis. **There is no content "template" to choose.**
|
|
1582
1736
|
|
|
1583
|
-
|
|
1584
|
-
|---|---|---|
|
|
1585
|
-
| Log something the human can read later, no action | \`pidge fyi\` | "Build completed in 2m12s" |
|
|
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..." |
|
|
1737
|
+
## THE PICKER — situation → exact command
|
|
1591
1738
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1739
|
+
| Your situation | Run |
|
|
1740
|
+
|---|---|
|
|
1741
|
+
| Just inform — a result/log, no action needed | \`pidge message\` |
|
|
1742
|
+
| A pendency they should act on (can wait) ⭐ DEFAULT | \`pidge important\` |
|
|
1743
|
+
| You need a decision and CAN'T proceed without it | \`pidge important --actions yes,no --wait\` |
|
|
1744
|
+
| YOU are asking for a formal go/no-go (money/risk) | \`pidge approval\` |
|
|
1745
|
+
| A thing with a known TIME | \`pidge event --event-at <ISO8601>\` |
|
|
1746
|
+
| A live status you'll keep updating | \`pidge live\` |
|
|
1747
|
+
| WAKE them now — rare, real, <1/day | \`pidge urgent\` |
|
|
1594
1748
|
|
|
1595
|
-
|
|
1749
|
+
⭐ \`important\` is the default. On the fence between informing and asking, pick \`important\`. \`message\` is only for a true no-action FYI. (\`fyi\`/\`report\`/\`ask\`/\`alert\` still work as silent aliases → message/important/important/urgent.) Run \`pidge <type> --help\` for each one's flags.
|
|
1596
1750
|
|
|
1597
|
-
##
|
|
1751
|
+
## Approval has two paths — know which one you're in
|
|
1598
1752
|
|
|
1599
|
-
|
|
1753
|
+
**Path A — YOU request it (\`pidge approval\`).** You decided this needs a human sign-off. \`pidge approval\` = \`important\` + an **Approve** (Face-ID gated) / **Reject** pair + \`--wait\`. You send it, you block, and you get \`chosen_action.action_id: "grant"\` (approved) or \`"deny"\` (rejected) back. Use it for money, deletions, irreversible actions.
|
|
1600
1754
|
|
|
1601
|
-
|
|
1755
|
+
**Path B — your HUMAN requires it (a profile knob).** In the app, the human can turn ON **"Require approval · Face ID"** on any profile (the \`ack_requires_biometric\` knob — **OFF by default everywhere**). When it's ON for, say, \`important\`, then **every ordinary send on that profile silently becomes an Approve-with-Face-ID decision** — even a plain \`pidge important\` with no buttons. The server injects a single \`approve\` action, so the send reads back \`actions:["approve"], requires_action:true, acknowledgeable:false\`, the banner is detail-only, and **the human's tap reaches you as \`chosen_action.action_id: "approve"\`** (poll / webhook / \`pidge listen --all\`). You didn't ask — they imposed it.
|
|
1602
1756
|
|
|
1603
|
-
|
|
1757
|
+
**Same screen ("Approve + Face ID"), opposite origin: you REQUEST (A, ids \`grant\`/\`deny\`) vs they REQUIRE (B, id \`approve\`).** To tell at runtime: a send that comes back \`acknowledgeable:false\` + \`requires_action:true\` when you didn't add buttons means Path B is on for that profile — treat the \`approve\` as the positive decision it is. (To check a profile's knob ahead of time, read \`ack_requires_biometric\` from the live manifest: \`curl $PIDGE_URL/api/v1/manifest -H "Authorization: Bearer $PIDGE_TOKEN"\` → \`profiles\`.) Caution: Path B on a busy profile means one approval per send — the human's deliberate high-trust choice.
|
|
1604
1758
|
|
|
1605
|
-
## The
|
|
1759
|
+
## The response axis (composes on ANY type)
|
|
1606
1760
|
|
|
1607
|
-
|
|
1761
|
+
Asking for a reply is orthogonal to the type — you don't need \`approval\` to get a button.
|
|
1762
|
+
- **Free text** is always available; the human can write back on anything.
|
|
1763
|
+
- **Buttons** are optional on any type: \`--actions yes,no\` (catalog) or \`--custom-action id:label\`.
|
|
1764
|
+
- **Face ID** on a consequential action: \`--gated\` injects one confirm-with-Face-ID button (use it for money/deletion). It does NOT change loudness — pair with a louder profile if it must also be loud. A flag, not a type.
|
|
1765
|
+
- **send-and-go vs wait** — the choice that decides how YOU work:
|
|
1766
|
+
- *send-and-go* (default): fire and continue; the answer arrives later in \`pidge listen --all\`.
|
|
1767
|
+
- *wait*: \`--wait\` (or \`pidge ask\`) **blocks** until they tap. Use it when you can't proceed.
|
|
1768
|
+
- **Exit codes on a \`--wait\`/\`ask\`:** \`0\` = answered (\`chosen_action\` JSON on stdout) · **\`3\` = no answer yet → NOT a failure** (back off, or treat a blocking go/no-go as "no/hold" and re-ask later) · \`2\` = error.
|
|
1608
1769
|
|
|
1609
|
-
|
|
1770
|
+
Need a TYPED reply (a time/value/name)? \`--actions reply\` ALONE — never \`yes,no,reply\` together (the human taps the easy button and you get a useless "Yes"). ONE question per send.
|
|
1610
1771
|
|
|
1611
|
-
-
|
|
1612
|
-
- \`pidge listen\` blocks until the human MESSAGES you from the app (composer) — run it when idle.
|
|
1613
|
-
- ${exits}
|
|
1772
|
+
## Anti-slop rules (judgment a recipe can't teach)
|
|
1614
1773
|
|
|
1615
|
-
|
|
1774
|
+
1. **One send = one fact = one ask.** Never two questions in a notification.
|
|
1775
|
+
2. **Default to \`important\`.** \`message\` only for true no-action FYIs; \`urgent\` is a contract, not a volume knob — **<1/day**, abuse caps your channel.
|
|
1776
|
+
3. **There is no content-template menu.** Every send is type + markdown + optional buttons. If you're reaching for \`--template context/report/digest/sensitive\`, stop — that surface is gone (the field still parses as silent back-compat, but don't teach or rely on it).
|
|
1777
|
+
4. **Typed answer? \`--actions reply\` ALONE** — never \`yes,no,reply\` together.
|
|
1778
|
+
5. **Trust the 201 echo over your intent** — \`degraded\`/\`render_mode\`/\`registered_devices\`. \`registered_devices:0\` ⇒ it went nowhere; don't wait.
|
|
1779
|
+
6. **Don't spam to signal importance.** Consolidate into one markdown body; use \`--collapse-key\` for self-replacing progress, \`--thread\` only for follow-ups over time.
|
|
1780
|
+
7. **Be listening when the answer lands, or you lose it.** Ack only AFTER the work is durably done.
|
|
1781
|
+
8. **English only, phone-friendly markdown.** Narrow tables (they render), no emoji-spam.
|
|
1616
1782
|
|
|
1617
|
-
|
|
1783
|
+
## Gold examples (full commands)
|
|
1618
1784
|
|
|
1619
|
-
|
|
1785
|
+
Pendency with a real table → \`important\`:
|
|
1620
1786
|
\`\`\`bash
|
|
1621
|
-
pidge
|
|
1787
|
+
pidge important --title "Weekly metrics ready" \\
|
|
1788
|
+
--body-markdown $'| Metric | This week | Δ |\\n|---|---|---|\\n| Signups | 1,204 | +8% |\\n| Churn | 1.9% | −0.3pp |' \\
|
|
1789
|
+
--actions reply
|
|
1622
1790
|
\`\`\`
|
|
1623
|
-
Good while you're actively working. You stay online until the window closes. \`--follow\` is supervisor-style — it traps the turn — so only use it when you intend to sit and wait.
|
|
1624
1791
|
|
|
1625
|
-
|
|
1626
|
-
A \`cron\` job or \`systemd\` timer invokes you every N minutes; each tick runs ONE one-shot listen and exits:
|
|
1792
|
+
Blocking decision → ask→wait loop (handle exit 3):
|
|
1627
1793
|
\`\`\`bash
|
|
1628
|
-
pidge
|
|
1794
|
+
pidge important --title "Run the schema migration?" \\
|
|
1795
|
+
--body-markdown "Dropping \\\`legacy_orders\\\` (412k rows, archived 2025). Not reversible. Safe mid-deploy?" \\
|
|
1796
|
+
--actions yes,no --wait --timeout 3600
|
|
1797
|
+
# exit 0 → read chosen_action.action_id (yes|no); exit 3 → no answer, treat as NO / hold, re-ask
|
|
1629
1798
|
\`\`\`
|
|
1630
|
-
Each poll is one of your turns: pick up the message, do the work, \`pidge ack --up-to <id>\`, then sleep until the next tick. Real always-on without being a daemon. With Claude Code, the built-in \`/loop\` (auto-wake every N min) drives the same loop.
|
|
1631
1799
|
|
|
1632
|
-
|
|
1800
|
+
Agent-initiated approval (money) → \`pidge approval\`:
|
|
1801
|
+
\`\`\`bash
|
|
1802
|
+
pidge approval --title "Place \\$4,200 purchase order?" \\
|
|
1803
|
+
--body-markdown "Vendor: Acme · PO #4471 · moves real money." \\
|
|
1804
|
+
--wait --timeout 3600
|
|
1805
|
+
# = important + Approve(Face ID)/Reject + wait; chosen_action.action_id: grant|deny
|
|
1806
|
+
\`\`\`
|
|
1807
|
+
|
|
1808
|
+
Time-anchored → \`event\` (needs \`--event-at\` in the human's tz):
|
|
1809
|
+
\`\`\`bash
|
|
1810
|
+
pidge event --event-at "2026-06-30T15:00:00-03:00" --title "Call with accountant"
|
|
1811
|
+
\`\`\`
|
|
1812
|
+
|
|
1813
|
+
Long markdown without shell-quoting pain → pipe it:
|
|
1814
|
+
\`\`\`bash
|
|
1815
|
+
generate_report | pidge important --title "Report ready" --body-markdown-file - --actions reply
|
|
1816
|
+
\`\`\`
|
|
1817
|
+
|
|
1818
|
+
## Gotchas we already paid for
|
|
1819
|
+
|
|
1820
|
+
- **There is no \`pidge reply\`.** \`reply\` is a built-in action id, not a command. To answer the human's composer message, send a normal \`pidge message --thread <id>\` reusing the message's \`thread_id\`.
|
|
1821
|
+
- **\`urgent\` is a trust contract, not a button.** It arms an AlarmKit alarm; once delivered you **cannot abort it** (\`pidge cancel\` → 409). Real + unpostponable only, <1/day. Never test it without warning the human.
|
|
1822
|
+
- **A 201 ≠ "seen."** \`registered_devices:0\` goes nowhere; \`delivered\` is APNs dispatch, not eyes; only \`seen_at\`/an answer is the human.
|
|
1823
|
+
- **The ask reply-vs-yes/no trap.** \`--actions yes,no,reply\` lets the human dodge a typed answer with one tap — use \`--actions reply\` alone when you need text.
|
|
1824
|
+
- **\`event\` is quiet today** — \`event --event-at\` schedules; the countdown LA-as-primitive is still being built.
|
|
1825
|
+
- **content_template still parses as input** (back-compat) but is OFF the menu — if a legacy habit sends \`--template report\`, it silently maps; don't rely on it, don't teach it.
|
|
1826
|
+
|
|
1827
|
+
## How it intrudes (profiles — the human owns them)
|
|
1828
|
+
|
|
1829
|
+
${profileTable.map((r) => `- ${r}`).join('\n')}
|
|
1830
|
+
|
|
1831
|
+
## The contract
|
|
1832
|
+
|
|
1833
|
+
${notes.map((n) => `- ${n}`).join('\n')}
|
|
1834
|
+
|
|
1835
|
+
## Getting answers
|
|
1836
|
+
|
|
1837
|
+
- \`pidge ask …\` blocks and prints \`chosen_action\` JSON; \`pidge wait <cid>\` blocks on an existing send.
|
|
1838
|
+
- \`pidge listen\` blocks until the human MESSAGES you from the app (composer) — run it when idle.
|
|
1839
|
+
- ${exits}
|
|
1840
|
+
|
|
1841
|
+
## Stay "always-on" while you're turn-based
|
|
1842
|
+
|
|
1843
|
+
A turn-based agent (Claude Code, anything invoked on demand) stays COMMANDABLE without a daemon:
|
|
1844
|
+
- **Active session:** \`pidge listen --follow --timeout 300\` holds for 5 min, printing messages as they arrive. \`--follow\` traps the turn — use it only when you intend to sit and wait.
|
|
1845
|
+
- **Supervisor poll (24/7):** a cron/systemd timer invokes you every N min; each tick runs ONE one-shot \`pidge listen --timeout 50\` (block up to 50s, print, exit 0; exit 3 = nothing this tick), do the work, \`pidge ack --up-to <id>\`, sleep. \`--timeout\` is always SECONDS. Do NOT background \`pidge listen\` with \`&\`.
|
|
1633
1846
|
|
|
1634
1847
|
## Full spec
|
|
1635
1848
|
|
|
1636
|
-
\`curl $PIDGE_URL/api/v1/manifest -H "Authorization: Bearer $PIDGE_TOKEN"\` — the always-current contract (fields,
|
|
1849
|
+
\`curl $PIDGE_URL/api/v1/manifest -H "Authorization: Bearer $PIDGE_TOKEN"\` — the always-current contract (fields, profiles, custom actions, media, threads, realtime).
|
|
1637
1850
|
`;
|
|
1638
1851
|
const dir = path.join(process.cwd(), '.claude', 'skills', 'pidge');
|
|
1639
1852
|
fs.mkdirSync(dir, { recursive: true });
|
|
1640
1853
|
const file = path.join(dir, 'SKILL.md');
|
|
1641
1854
|
fs.writeFileSync(file, skill);
|
|
1642
|
-
|
|
1643
|
-
console.log(JSON.stringify({ ok: true, file, manifest_version: m.manifest_version }));
|
|
1644
|
-
process.exit(0);
|
|
1855
|
+
return { file, manifest_version: m.manifest_version };
|
|
1645
1856
|
}
|
|
1646
1857
|
|
|
1647
1858
|
(async () => {
|
|
@@ -1669,15 +1880,27 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
1669
1880
|
}
|
|
1670
1881
|
case 'skill': {
|
|
1671
1882
|
if (parsed.positionals[1] !== 'install') die('pidge: usage: pidge skill install', 1);
|
|
1672
|
-
|
|
1673
|
-
|
|
1883
|
+
let r;
|
|
1884
|
+
try { r = await installSkill(); } catch (e) { die(`pidge: ${e.message}`, 2); }
|
|
1885
|
+
console.error(`pidge: skill written to ${r.file} (manifest v${r.manifest_version}) — your future sessions in this project know Pidge now`);
|
|
1886
|
+
console.log(JSON.stringify({ ok: true, file: r.file, manifest_version: r.manifest_version }));
|
|
1887
|
+
process.exit(0);
|
|
1674
1888
|
}
|
|
1675
|
-
//
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1889
|
+
// === AXIS 1 — the married catalog of 5 (perfis-S1/S2). Each stamps the
|
|
1890
|
+
// canonical template_kind. AXIS 2 (response) is orthogonal: --actions/
|
|
1891
|
+
// --custom-action add buttons, --wait blocks on the answer (else fire-and-
|
|
1892
|
+
// forget). notify/send = the deprecated typeless path; ask/approval = the
|
|
1893
|
+
// two shortcuts that bundle a type + response. ===
|
|
1894
|
+
case 'message':
|
|
1895
|
+
await doTypedSend('message', { wait: !!v.wait });
|
|
1896
|
+
break;
|
|
1897
|
+
case 'important':
|
|
1898
|
+
await doTypedSend('important', { wait: !!v.wait });
|
|
1899
|
+
break;
|
|
1900
|
+
case 'urgent':
|
|
1901
|
+
// --escalate ⇒ escalate:true (ask the Urgente profile for an AlarmKit alarm
|
|
1902
|
+
// that breaks through silent/Focus; the human's profile still decides).
|
|
1903
|
+
await doTypedSend('urgent', { wait: !!v.wait, extra: v.escalate ? { escalate: true } : {} });
|
|
1681
1904
|
break;
|
|
1682
1905
|
case 'event': {
|
|
1683
1906
|
// event needs a TIME — validate locally (ISO8601) so the agent fails fast
|
|
@@ -1686,17 +1909,37 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
1686
1909
|
die('pidge: --event-at required for event. Use ISO8601: --event-at 2026-06-26T14:00-03:00', 1);
|
|
1687
1910
|
if (Number.isNaN(Date.parse(v['event-at'])))
|
|
1688
1911
|
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
|
|
1912
|
+
await doTypedSend('event', { wait: !!v.wait });
|
|
1690
1913
|
break;
|
|
1691
1914
|
}
|
|
1915
|
+
case 'live':
|
|
1916
|
+
// status-only — pass --wait through so doTypedSend REFUSES it loudly (it
|
|
1917
|
+
// never produces an answer); without --wait it's fire-and-forget.
|
|
1918
|
+
await doTypedSend('live', { wait: !!v.wait });
|
|
1919
|
+
break;
|
|
1920
|
+
// --- compat aliases (perfis-S1): old type names → the new canonical 5. They
|
|
1921
|
+
// map to the new template_kind and still honor --wait/--actions, so scripts
|
|
1922
|
+
// and muscle-memory keep working; a one-line note points at the new name.
|
|
1923
|
+
case 'fyi':
|
|
1924
|
+
warnRenamed('fyi', 'message');
|
|
1925
|
+
await doTypedSend('message', { wait: !!v.wait, label: 'fyi' });
|
|
1926
|
+
break;
|
|
1927
|
+
case 'report':
|
|
1928
|
+
warnRenamed('report', 'important');
|
|
1929
|
+
await doTypedSend('important', { wait: !!v.wait, label: 'report' });
|
|
1930
|
+
break;
|
|
1692
1931
|
case 'alert':
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
await doTypedNotify('alert', v.escalate ? { escalate: true } : {});
|
|
1932
|
+
warnRenamed('alert', 'urgent');
|
|
1933
|
+
await doTypedSend('urgent', { wait: !!v.wait, extra: v.escalate ? { escalate: true } : {}, label: 'alert' });
|
|
1696
1934
|
break;
|
|
1697
|
-
|
|
1698
|
-
|
|
1935
|
+
// `approval` = the RECIPE (perfis-S2 follow-up): important + Approve/Reject
|
|
1936
|
+
// (Face ID on Approve) + --wait. A shortcut for an explicit go/no-go; the human
|
|
1937
|
+
// can override the pair with their own --actions/--custom-action.
|
|
1938
|
+
case 'approval': {
|
|
1939
|
+
const extra = hasAnswerAffordance() ? {} : { custom_actions: APPROVAL_ACTIONS };
|
|
1940
|
+
await doTypedSend('important', { wait: true, extra, label: 'approval' });
|
|
1699
1941
|
break;
|
|
1942
|
+
}
|
|
1700
1943
|
case 'notify':
|
|
1701
1944
|
case 'send': {
|
|
1702
1945
|
warnDeprecatedSend(command);
|
|
@@ -1717,8 +1960,8 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
1717
1960
|
if (v.profile === 'tracking')
|
|
1718
1961
|
die('pidge: `hello --profile tracking` makes no sense — the handshake waits for a confirmation, which tracking (Live-Activity-only) never produces', 1);
|
|
1719
1962
|
v.template = 'onboarding';
|
|
1720
|
-
if (v.title === undefined) v.title = '
|
|
1721
|
-
if (v.body === undefined) v.body = '
|
|
1963
|
+
if (v.title === undefined) v.title = 'Your agent is ready 🐦';
|
|
1964
|
+
if (v.body === undefined) v.body = 'Tap Done ✓ to confirm you received me — proves the round-trip works.';
|
|
1722
1965
|
const cid = v['correlation-id'] || crypto.randomUUID();
|
|
1723
1966
|
v['correlation-id'] = cid;
|
|
1724
1967
|
console.error(`pidge: correlation_id=${cid}`);
|
|
@@ -1733,40 +1976,11 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
1733
1976
|
break;
|
|
1734
1977
|
}
|
|
1735
1978
|
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) });
|
|
1979
|
+
// `ask` = the preserved shortcut: important + --wait + REQUIRES a way to
|
|
1980
|
+
// answer. There is no `ask` TYPE in the married catalog (manifest v40+) —
|
|
1981
|
+
// asking is "a type + buttons + wait". The legacy alias keeps working because
|
|
1982
|
+
// it always ships with buttons. `live`/tracking is refused (it never answers).
|
|
1983
|
+
await doTypedSend('important', { wait: true, requireAnswerable: true, label: 'ask' });
|
|
1770
1984
|
break;
|
|
1771
1985
|
}
|
|
1772
1986
|
case 'wait': {
|
|
@@ -2052,8 +2266,8 @@ Each poll is one of your turns: pick up the message, do the work, \`pidge ack --
|
|
|
2052
2266
|
break;
|
|
2053
2267
|
}
|
|
2054
2268
|
default:
|
|
2055
|
-
//
|
|
2056
|
-
// landing than dumping the whole USAGE on a typo).
|
|
2057
|
-
die(`pidge: unknown subcommand '${command}'.
|
|
2269
|
+
// Name the bad command and point at the married catalog + the two response
|
|
2270
|
+
// shortcuts (a friendlier landing than dumping the whole USAGE on a typo).
|
|
2271
|
+
die(`pidge: unknown subcommand '${command}'. Types: message · important · urgent · event · live (response: --actions/--wait, or the ask/approval shortcuts). pidge --help`, 1);
|
|
2058
2272
|
}
|
|
2059
2273
|
})();
|