pidge-cli 0.6.1 → 0.7.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.
Files changed (3) hide show
  1. package/README.md +17 -0
  2. package/bin/pidge.js +281 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -11,6 +11,19 @@ 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
+ ## Setup in one command (v0.7.0 — the claim flow)
15
+
16
+ ```bash
17
+ # The human copies a setup prompt from the Pidge app (Canais → the channel) —
18
+ # it carries a SINGLE-USE claim code (15 min TTL), never the key:
19
+ npx pidge-cli setup --claim <code> --url https://pidge.sh
20
+ # → exchanges the code for the real key, writes ~/.config/pidge/env (chmod 600)
21
+ # and runs `pidge doctor`. The secret never appears on screen or in any chat.
22
+
23
+ npx pidge-cli doctor # validate anytime: env source, server, key, "canal X · N devices"
24
+ npx pidge-cli whoami # which channel does this key speak for (JSON)
25
+ ```
26
+
14
27
  ## Use it (no install — via npx)
15
28
 
16
29
  ```bash
@@ -55,6 +68,10 @@ npx pidge-cli notify --title "Relatório" --file ./relatorio.xlsx
55
68
  | `cancel <correlation_id>` | Cancel a **still-scheduled** notification before it fires (idempotent; 409 once it reached the phone). |
56
69
  | `inbox` | What you sent: list, `--pending` slice, or `--summary` (counts + answer latency). |
57
70
  | `listen` | Block until the human **messages you** from the app; prints the messages, ACKs them, exits `0`. One-shot — loop it. |
71
+ | `setup --claim <code>` | One-shot onboarding (v0.7.0): exchange the single-use code for the key, store it in `~/.config/pidge/env` (600), run doctor. |
72
+ | `doctor` | Validate the setup **without exposing secrets**: env source, server reachable, key valid, channel + device count. Exit 0/2. |
73
+ | `whoami` | Which channel does this key speak for (JSON). |
74
+ | `skill install` | Write `.claude/skills/pidge/SKILL.md` generated from the live manifest — persistent Pidge knowledge for Claude Code agents; re-run to update. |
58
75
 
59
76
  ## Realtime (v0.6.0)
60
77
 
package/bin/pidge.js CHANGED
@@ -90,6 +90,7 @@ const OPTIONS = {
90
90
  'reply-to': { type: 'string' },
91
91
  'correlation-id': { type: 'string' },
92
92
  thread: { type: 'string' }, // conversation handle (#49) — same id ⇒ one strand on the phone
93
+ after: { type: 'string' }, // decision queue (#157): held until this cid resolves
93
94
  'collapse-key': { type: 'string' },
94
95
  param: { type: 'string', multiple: true }, // key=value escape hatch → raw /notify field
95
96
  timeout: { type: 'string' },
@@ -102,17 +103,35 @@ const OPTIONS = {
102
103
  // realtime (#118): WS by default when the runtime has a WebSocket (Node ≥22)
103
104
  realtime: { type: 'boolean' }, // force WS (warn+fallback if unavailable)
104
105
  'no-realtime': { type: 'boolean' }, // polling only
106
+ // onboarding v2 (#110)
107
+ claim: { type: 'string' }, // setup --claim <single-use code>
108
+ // #157 P2: listen keeps going after a batch (supervisor loop, one process)
109
+ follow: { type: 'boolean' },
105
110
  };
106
111
 
107
112
  const USAGE = `pidge — send an iPhone notification to a human and block until they answer.
108
113
 
109
114
  USAGE
115
+ pidge setup --claim CODE [--url BASE] one-shot onboarding (#110): exchange the single-use
116
+ code for the channel key, store it in
117
+ ~/.config/pidge/env (chmod 600), run doctor
118
+ pidge doctor validate the setup WITHOUT exposing secrets:
119
+ env source, server, key, "canal X · N devices"
120
+ pidge whoami which channel does this key speak for (JSON)
110
121
  pidge ask [options] send AND wait for the answer (prints chosen_action JSON)
111
122
  pidge notify [options] send only (prints the 201 JSON)
112
123
  pidge wait <correlation_id> [options] block on an already-sent notification
113
124
  pidge cancel <correlation_id> cancel a still-scheduled notification (#56)
114
125
  pidge inbox [--pending|--summary|--all|--limit N] what you sent: list, pending slice, or counts+latency (#83)
115
- pidge listen [--timeout N] block until the human MESSAGES you from the app, print + ack + exit (#48)
126
+ pidge listen [--timeout N] [--all] [--follow]
127
+ block until the human MESSAGES you from the app, print + ack + exit (#48)
128
+ --follow = print+ack and KEEP listening until --timeout
129
+ (exit 0 if any batch landed; one-shot stays the default)
130
+ --all (#131) = the SINGLE EAR: also hear notification ANSWERS
131
+ (kind notification_reply + self-contained ref) — nothing the human
132
+ says can be missed by a looped listen --all
133
+ pidge skill install write .claude/skills/pidge/SKILL.md generated from the
134
+ live manifest (persistent Pidge knowledge for Claude Code)
116
135
  pidge --help
117
136
 
118
137
  REALTIME (#118)
@@ -157,6 +176,9 @@ OPTIONS (notify / ask)
157
176
  --correlation-id ID idempotency + routing key (auto-generated if omitted)
158
177
  --thread ID conversation handle (#49): sends sharing it group as ONE
159
178
  strand on the phone — use it for follow-ups
179
+ --after CID decision queue (#157): HELD until that notification is
180
+ answered — chain N decisions so the human sees one at a
181
+ time ("Decisão 2/3" --after <cid-da-1>); snooze doesn't advance
160
182
  --collapse-key KEY replace/update a prior notification
161
183
  --param KEY=VALUE pass ANY raw /notify field (repeatable) — future server
162
184
  fields work without a CLI update; the manifest is the contract
@@ -197,7 +219,9 @@ const command = parsed.positionals[0];
197
219
  // `pidge --help` / `-h` / `help` → full help on stdout, exit 0. No command → stderr, exit 1.
198
220
  if (v.help || command === 'help') { console.log(USAGE); process.exit(0); }
199
221
  if (!command) { console.error(USAGE); process.exit(1); }
200
- if (!TOKEN) die('pidge: set PIDGE_TOKEN (env var, or put PIDGE_TOKEN=… in ~/.config/pidge/env)');
222
+ // `setup` is the command that CREATES the token config it must run without one.
223
+ if (!TOKEN && command !== 'setup')
224
+ die('pidge: set PIDGE_TOKEN (env var, or put PIDGE_TOKEN=… in ~/.config/pidge/env) — or onboard with: pidge setup --claim <code> (ask your human for the code: Pidge app → Canais → o canal → copiar prompt de setup)');
201
225
 
202
226
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
203
227
  const headers = { authorization: `Bearer ${TOKEN}`, 'content-type': 'application/json' };
@@ -217,7 +241,7 @@ function fetchT(url, opts = {}, timeoutMs = 30000) {
217
241
  // The server advertises its manifest version on every response. When it's newer
218
242
  // than what this CLI shipped knowing, nudge ONCE on stderr — the agent re-reads
219
243
  // the manifest (whats_new) and learns the new capabilities without polling.
220
- const KNOWN_MANIFEST_VERSION = 16;
244
+ const KNOWN_MANIFEST_VERSION = 24;
221
245
  let newsWarned = false;
222
246
  function checkManifestNews(res) {
223
247
  const v = parseInt(res.headers.get('x-pidge-manifest-version') || '0', 10);
@@ -372,6 +396,7 @@ function buildBody() {
372
396
  if (v['reply-to'] !== undefined) body.reply_to = v['reply-to'];
373
397
  if (v['correlation-id'] !== undefined) body.correlation_id = v['correlation-id'];
374
398
  if (v.thread !== undefined) body.thread_id = v.thread;
399
+ if (v.after !== undefined) body.after = v.after;
375
400
  if (v['collapse-key'] !== undefined) body.collapse_key = v['collapse-key'];
376
401
  if (v.actions !== undefined) body.actions = v.actions.split(',').filter(Boolean);
377
402
 
@@ -379,6 +404,11 @@ function buildBody() {
379
404
  if (customs.length) {
380
405
  body.custom_actions = customs.map((spec) => {
381
406
  const [id, label, ...flags] = spec.split(':');
407
+ // #157 P2: fail fast locally — the rule is stable and the server 422
408
+ // costs a round-trip an agent then has to interpret.
409
+ if (!/^[a-z0-9_]{1,40}$/.test(id || '')) {
410
+ die(`pidge: --custom-action id ${JSON.stringify(id)} is invalid — lowercase letters, digits and underscore only (^[a-z0-9_]{1,40}$)`, 1);
411
+ }
382
412
  const ca = { id, label };
383
413
  if (flags.includes('destructive')) ca.style = 'destructive';
384
414
  if (flags.includes('confirm')) ca.confirm = true;
@@ -613,8 +643,189 @@ async function waitForAnswer(cid, { timeout, interval }) {
613
643
 
614
644
  const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback);
615
645
 
646
+ // ---------------------------------------------------------------------------
647
+ // Onboarding v2 (#110): setup --claim / doctor / whoami / skill install.
648
+ // ---------------------------------------------------------------------------
649
+
650
+ const CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'pidge');
651
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'env');
652
+
653
+ // Where the token came from — doctor narrates it, setup respects precedence.
654
+ function tokenSource() {
655
+ if (process.env.PIDGE_TOKEN || process.env.HERALD_TOKEN) return 'env var';
656
+ if (FILE_ENV.PIDGE_TOKEN) return CONFIG_FILE;
657
+ return null;
658
+ }
659
+
660
+ // GET /whoami — which channel does this key speak for. Returns {res, data}.
661
+ async function fetchWhoami(base = BASE, token = TOKEN) {
662
+ const res = await fetchT(`${base}/api/v1/whoami`, {
663
+ headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' },
664
+ });
665
+ let data = {};
666
+ try { data = await res.json(); } catch { /* leave {} */ }
667
+ return { res, data };
668
+ }
669
+
670
+ // doctor: validate the setup WITHOUT exposing secrets. Narration on stderr,
671
+ // a compact machine-readable line on stdout. Exit 0 healthy / 2 broken.
672
+ async function runDoctor(base = BASE, token = TOKEN) {
673
+ const source = token === TOKEN ? tokenSource() : CONFIG_FILE; // post-setup call passes the fresh token
674
+ if (!token) {
675
+ console.error('pidge doctor: NO TOKEN — set PIDGE_TOKEN, or onboard with `pidge setup --claim <code>` (the human copies the code from the Pidge app)');
676
+ process.exit(2);
677
+ }
678
+ console.error(`pidge doctor: token found (${source || 'passed in'}) — never displayed`);
679
+ console.error(`pidge doctor: server ${base}`);
680
+ let out;
681
+ try {
682
+ out = await fetchWhoami(base, token);
683
+ } catch (e) {
684
+ console.error(`pidge doctor: server UNREACHABLE — ${e.message} (check the URL; is it ${base}?)`);
685
+ process.exit(2);
686
+ }
687
+ const { res, data } = out;
688
+ checkManifestNews(res);
689
+ if (res.status === 401) {
690
+ console.error('pidge doctor: server reachable but the key is INVALID/REVOKED — re-onboard: ask your human for a fresh claim code (Pidge app → Canais → o canal → copiar prompt de setup)');
691
+ process.exit(2);
692
+ }
693
+ if (res.status === 404) {
694
+ // pre-#110 server: no /whoami yet — the key may still be fine; prove it on the manifest.
695
+ const m = await fetchT(`${base}/api/v1/manifest`, { headers: { authorization: `Bearer ${token}` } }).catch(() => null);
696
+ if (m && m.status === 200) {
697
+ console.error('pidge doctor: key VALID (server predates /whoami — channel/device detail unavailable; update the server to see it)');
698
+ console.log(JSON.stringify({ ok: true, base_url: base, channel: null, devices: null }));
699
+ process.exit(0);
700
+ }
701
+ console.error(`pidge doctor: unexpected ${m ? m.status : 'network error'} on the manifest — server looks broken`);
702
+ process.exit(2);
703
+ }
704
+ if (res.status !== 200) {
705
+ console.error(`pidge doctor: unexpected ${res.status} from /whoami — ${JSON.stringify(data)}`);
706
+ process.exit(2);
707
+ }
708
+ const devices = data.devices ?? 0;
709
+ console.error(`pidge doctor: key valid — canal "${data.channel && data.channel.name}" · ${devices} device(s)`);
710
+ if (devices === 0)
711
+ console.error('pidge doctor: WARNING — 0 devices: sends will reach NOBODY until the human installs/opens the Pidge app on their iPhone');
712
+ console.error('pidge doctor: all good — try: pidge ask --template decision --title "Pidge funcionando?"');
713
+ console.log(JSON.stringify({ ok: true, base_url: base, channel: data.channel, devices, manifest_version: data.manifest_version }));
714
+ process.exit(0);
715
+ }
716
+
717
+ // setup --claim: exchange the single-use code for the key, store it ourselves
718
+ // (the secret never appears on screen or in the chat the prompt was pasted in),
719
+ // then prove the loop with doctor.
720
+ async function runSetup() {
721
+ const code = v.claim;
722
+ if (!code) die('pidge: usage: pidge setup --claim <code> [--url <base>] (the human copies the code from the Pidge app)', 1);
723
+ const base = (v.url || process.env.PIDGE_URL || FILE_ENV.PIDGE_URL || 'https://pidge.sh').replace(/\/+$/, '');
724
+ let res, data = {};
725
+ try {
726
+ res = await fetchT(`${base}/api/v1/claim`, {
727
+ method: 'POST', headers: { 'content-type': 'application/json' },
728
+ body: JSON.stringify({ code }),
729
+ });
730
+ try { data = await res.json(); } catch { /* leave {} */ }
731
+ } catch (e) {
732
+ die(`pidge: claim failed (network): ${e.message} — is the server URL right? (${base})`, 2);
733
+ }
734
+ if (res.status === 404)
735
+ die('pidge: claim code unknown, EXPIRED (15 min TTL) or already used — ask your human for a fresh one (Pidge app → Canais → o canal → copiar prompt de setup)', 2);
736
+ if (!(res.status >= 200 && res.status < 300) || !data.key)
737
+ die(`pidge: claim failed (${res.status}): ${JSON.stringify(data)}`, 2);
738
+
739
+ const finalBase = (data.base_url || base).replace(/\/+$/, '');
740
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
741
+ fs.writeFileSync(CONFIG_FILE, `PIDGE_URL=${finalBase}\nPIDGE_TOKEN=${data.key}\n`, { mode: 0o600 });
742
+ try { fs.chmodSync(CONFIG_FILE, 0o600); } catch { /* mode set on create */ }
743
+ console.error(`pidge: configured for canal "${data.channel && data.channel.name}" — key stored in ${CONFIG_FILE} (chmod 600, never displayed)`);
744
+ await runDoctor(finalBase, data.key);
745
+ }
746
+
747
+ // skill install (#110e): persistent Pidge knowledge for Claude Code agents —
748
+ // a skill generated FROM the live manifest (so it can't drift), versioned with
749
+ // manifest_version (re-run to update; whats_new is the changelog).
750
+ async function runSkillInstall() {
751
+ let res, m;
752
+ try {
753
+ res = await fetchT(`${BASE}/api/v1/manifest`, { headers });
754
+ m = await res.json();
755
+ } catch (e) {
756
+ die(`pidge: could not read the manifest: ${e.message}`, 2);
757
+ }
758
+ if (res.status !== 200) die(`pidge: manifest read failed (${res.status})`, 2);
759
+ const table = (m.templates && m.templates.decision_table) || [];
760
+ const profileTable = (m.profiles && m.profiles.decision_table) || [];
761
+ const notes = m.notes || [];
762
+ const exits = (m.cli && m.cli.output) || '';
763
+ const skill = `---
764
+ name: pidge
765
+ description: Send rich, actionable iPhone notifications to your human and get their decision back (Pidge). Use when finishing long tasks (report), needing a decision/approval, sending FYIs with substance, or anything time-anchored. Also covers reading the human's replies/messages back.
766
+ ---
767
+
768
+ # Pidge — notify your human, get answers back
769
+
770
+ 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).
771
+
772
+ 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).
773
+
774
+ ## Pick the right send (decision table)
775
+
776
+ ${table.map((r) => `- ${r}`).join('\n')}
777
+
778
+ ## How it intrudes (profiles — the human owns them)
779
+
780
+ ${profileTable.map((r) => `- ${r}`).join('\n')}
781
+
782
+ ## The contract
783
+
784
+ ${notes.map((n) => `- ${n}`).join('\n')}
785
+
786
+ ## Getting answers
787
+
788
+ - \`pidge ask …\` blocks and prints chosen_action JSON; \`pidge wait <cid>\` blocks on an existing send.
789
+ - \`pidge listen\` blocks until the human MESSAGES you from the app (composer) — run it when idle.
790
+ - ${exits}
791
+
792
+ ## Full spec
793
+
794
+ \`curl $PIDGE_URL/api/v1/manifest -H "Authorization: Bearer $PIDGE_TOKEN"\` — the always-current contract (fields, templates, custom actions, media, threads, realtime).
795
+ `;
796
+ const dir = path.join(process.cwd(), '.claude', 'skills', 'pidge');
797
+ fs.mkdirSync(dir, { recursive: true });
798
+ const file = path.join(dir, 'SKILL.md');
799
+ fs.writeFileSync(file, skill);
800
+ console.error(`pidge: skill written to ${file} (manifest v${m.manifest_version}) — your future sessions in this project know Pidge now`);
801
+ console.log(JSON.stringify({ ok: true, file, manifest_version: m.manifest_version }));
802
+ process.exit(0);
803
+ }
804
+
616
805
  (async () => {
617
806
  switch (command) {
807
+ case 'setup': {
808
+ await runSetup(); // exits via runDoctor
809
+ break;
810
+ }
811
+ case 'doctor': {
812
+ await runDoctor();
813
+ break;
814
+ }
815
+ case 'whoami': {
816
+ const { res, data } = await fetchWhoami().catch((e) => { die(`pidge: whoami failed (network): ${e.message}`, 2); });
817
+ checkManifestNews(res);
818
+ if (res.status !== 200) die(`pidge: whoami failed (${res.status}): ${JSON.stringify(data)}`, 2);
819
+ console.log(JSON.stringify(data, null, 2));
820
+ console.error(`pidge: you are canal "${data.channel && data.channel.name}" · ${data.devices ?? '?'} device(s)`);
821
+ process.exit(0);
822
+ break;
823
+ }
824
+ case 'skill': {
825
+ if (parsed.positionals[1] !== 'install') die('pidge: usage: pidge skill install', 1);
826
+ await runSkillInstall();
827
+ break;
828
+ }
618
829
  case 'notify': {
619
830
  const { ok, info, raw } = await doNotify();
620
831
  console.log(raw);
@@ -639,7 +850,20 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
639
850
  const { ok, info } = await doNotify();
640
851
  if (!ok) process.exit(2);
641
852
  console.error(`pidge: sent (${info.registered_devices} device(s)) — waiting on ${cid}`);
642
- await waitForAnswer(cid, { timeout: num(v.timeout, 600), interval: num(v.interval, 30) });
853
+ // #132: no --timeout obey the template's suggestion from the 201 echo
854
+ // (human decisions take 30-40 min in the wild; a 600 s default misread
855
+ // them as silence). Explicit --timeout always wins.
856
+ let timeout = num(v.timeout, NaN);
857
+ if (!Number.isFinite(timeout)) {
858
+ if (info.suggested_ask_timeout) {
859
+ timeout = info.suggested_ask_timeout;
860
+ const mins = Math.round(timeout / 60);
861
+ console.error(`pidge: timeout ${mins} min — suggested by template ${info.template || v.template} (override with --timeout)`);
862
+ } else {
863
+ timeout = 600;
864
+ }
865
+ }
866
+ await waitForAnswer(cid, { timeout, interval: num(v.interval, 30) });
643
867
  break;
644
868
  }
645
869
  case 'wait': {
@@ -715,12 +939,35 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
715
939
  // the whole session never had a healthy round-trip (#119).
716
940
  // At-least-once: the ack happens AFTER the print — a crash re-serves them;
717
941
  // dedupe by id if you've seen one before.
942
+ // --all (#131): the SINGLE EAR — the queue also serves notification
943
+ // ANSWERS (kind notification_reply, with a self-contained ref), so a
944
+ // fire-and-forget notify can't lose its reply. Without --all the original
945
+ // composer-only contract stands (no double-consumption for ask/wait users).
718
946
  const timeout = num(v.timeout, 600);
719
947
  let deadline = Date.now() + timeout * 1000;
948
+ const queueQs = v.all ? '?all=true' : '';
949
+ // #157 P2 --follow: print+ack a batch and KEEP listening until the
950
+ // timeout — the supervisor loop without re-spawning a process per batch.
951
+ let gotAny = false;
952
+ const followEnd = () => {
953
+ if (v.follow && gotAny) {
954
+ console.error(`pidge: --follow window ended after ${timeout}s — batches were delivered`);
955
+ process.exit(0);
956
+ }
957
+ return false;
958
+ };
720
959
 
721
- // Print + ack + exit 0 — shared by the WS and polling paths.
960
+ // Print + ack (+ exit 0 unless --follow) — shared by the WS and polling paths.
722
961
  const printAndAck = async (msgs) => {
723
962
  console.log(JSON.stringify(msgs, null, 2));
963
+ // #131: narrate answers so the agent knows WHICH notification spoke back.
964
+ for (const m of msgs) {
965
+ if (m.kind === 'notification_reply' && m.ref) {
966
+ const said = m.text ? `: ${String(m.text).slice(0, 120)}` : '';
967
+ console.error(`pidge: reply to your notification ${m.ref.correlation_id} ("${m.ref.title}") — ${m.action_id || m.ref.event_kind}${said}`);
968
+ if (m.truncated) console.error('pidge: that reply hit the server cap (truncated:true) — tell your human the tail was lost');
969
+ }
970
+ }
724
971
  const upTo = Math.max(...msgs.map((m) => m.id));
725
972
  try {
726
973
  // fetchT, not fetch: a wedged proxy stalling this ack would otherwise
@@ -738,7 +985,9 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
738
985
  } catch (e) {
739
986
  console.error(`pidge: WARNING — ack failed (network: ${e.message}); these messages will be re-served next listen`);
740
987
  }
741
- process.exit(0);
988
+ gotAny = true;
989
+ if (!v.follow) process.exit(0);
990
+ console.error('pidge: --follow — still listening');
742
991
  };
743
992
 
744
993
  // Realtime path (#118): hold ConversationChannel — the human sees "ouvindo
@@ -750,12 +999,15 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
750
999
  if (draining) return;
751
1000
  draining = true;
752
1001
  try {
753
- const res = await fetchT(`${BASE}/api/v1/messages`, { headers });
1002
+ const res = await fetchT(`${BASE}/api/v1/messages${queueQs}`, { headers });
754
1003
  checkManifestNews(res);
755
1004
  if (res.status === 200) {
756
1005
  health.ok();
757
1006
  const msgs = (await res.json().catch(() => ({}))).messages || [];
758
- if (msgs.length) { finish('got-messages'); await printAndAck(msgs); }
1007
+ if (msgs.length) {
1008
+ if (!v.follow) finish('got-messages');
1009
+ await printAndAck(msgs);
1010
+ }
759
1011
  } else if (res.status >= 500) {
760
1012
  health.fail(`backlog read ${res.status}`);
761
1013
  }
@@ -766,16 +1018,29 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
766
1018
  }
767
1019
  };
768
1020
  let announced = false;
769
- const outcome = await cableSession({
1021
+ const sessions = [cableSession({
770
1022
  channel: 'ConversationChannel',
771
1023
  deadline,
772
1024
  onUp: (finish) => {
773
- if (!announced) { announced = true; console.error('pidge: listening over the realtime socket (the human sees "ouvindo agora")'); }
1025
+ if (!announced) { announced = true; console.error(`pidge: listening over the realtime socket${v.all ? ' — single ear: composer + notification answers (#131)' : ''} (the human sees "ouvindo agora")`); }
774
1026
  drain(finish);
775
1027
  },
776
1028
  onFrame: (m, finish) => { if (m.type === 'message') drain(finish); },
777
- });
1029
+ })];
1030
+ // --all (#131): answers broadcast on InboxChannel, not Conversation — a
1031
+ // second subscription wakes the same HTTP drain (the queue is the ledger;
1032
+ // the loser session leaks until exit, harmless in a one-shot process).
1033
+ if (v.all) {
1034
+ sessions.push(cableSession({
1035
+ channel: 'InboxChannel',
1036
+ deadline,
1037
+ onUp: (finish) => drain(finish),
1038
+ onFrame: (m, finish) => { if (m.type === 'event' && m.responded) drain(finish); },
1039
+ }));
1040
+ }
1041
+ const outcome = await Promise.race(sessions);
778
1042
  if (outcome === 'deadline') {
1043
+ followEnd();
779
1044
  health.exitTimeout(`timed out after ${timeout}s — no message from the human`);
780
1045
  }
781
1046
  if (outcome === 'got-messages') {
@@ -788,7 +1053,10 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
788
1053
  const waitS = health.degraded ? 0 : Math.max(0, Math.min(25, Math.ceil((deadline - Date.now()) / 1000)));
789
1054
  const askedAt = Date.now();
790
1055
  try {
791
- const res = await fetchT(`${BASE}/api/v1/messages${waitS > 0 ? `?wait=${waitS}` : ''}`, { headers }, (waitS + 10) * 1000);
1056
+ const qs = new URLSearchParams();
1057
+ if (waitS > 0) qs.set('wait', String(waitS));
1058
+ if (v.all) qs.set('all', 'true');
1059
+ const res = await fetchT(`${BASE}/api/v1/messages${qs.size ? `?${qs}` : ''}`, { headers }, (waitS + 10) * 1000);
792
1060
  checkManifestNews(res);
793
1061
  if (res.status === 200) {
794
1062
  health.ok();
@@ -805,6 +1073,7 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
805
1073
  health.fail(`network: ${e.message}`);
806
1074
  }
807
1075
  if (Date.now() >= deadline) {
1076
+ followEnd();
808
1077
  health.exitTimeout(`timed out after ${timeout}s — no message from the human`);
809
1078
  }
810
1079
  const pace = health.degraded ? DEGRADED_INTERVAL_S : num(v.interval, 5);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pidge-cli",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Send rich, actionable iPhone notifications to a human and block until they answer. Built for AI agents.",
5
5
  "keywords": [
6
6
  "pidge",