pidge-cli 0.6.0 → 0.6.1

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 (2) hide show
  1. package/bin/pidge.js +27 -8
  2. package/package.json +1 -1
package/bin/pidge.js CHANGED
@@ -124,7 +124,9 @@ REALTIME (#118)
124
124
  --realtime force WS (warns + falls back to polling if unavailable)
125
125
  --no-realtime polling only (the ?wait= long-poll, capped 25 s server-side)
126
126
  Degrade ladder, narrated on stderr: WS → ?wait= long-poll → plain GETs every
127
- ~45 s after 3 consecutive failures on held polls (#119).
127
+ ~45 s after 3 consecutive failures on held polls (#119). Degrade is STICKY for
128
+ the session (we can't probe held-poll health without re-paying the failure) —
129
+ re-invoke the command to retry the fast path.
128
130
 
129
131
  OPTIONS (notify / ask)
130
132
  --title TEXT (required) the headline
@@ -200,6 +202,18 @@ if (!TOKEN) die('pidge: set PIDGE_TOKEN (env var, or put PIDGE_TOKEN=… in ~/.c
200
202
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
201
203
  const headers = { authorization: `Bearer ${TOKEN}`, 'content-type': 'application/json' };
202
204
 
205
+ // fetch with a hard timeout (#119 review): a wedged edge proxy can stall even a
206
+ // short POST forever, and a hung ack on the realtime listen path would pin the
207
+ // process past its deadline — worse than going deaf. NOTHING in this CLI should
208
+ // await a fetch that can't time out. A held long-poll passes its own (larger)
209
+ // timeout; everything else uses the 30 s default.
210
+ function fetchT(url, opts = {}, timeoutMs = 30000) {
211
+ const ms = parseInt(process.env.PIDGE_FETCH_TIMEOUT || '', 10) || timeoutMs; // test/ops hook
212
+ const ctl = new AbortController();
213
+ const t = setTimeout(() => ctl.abort(new Error(`timeout after ${ms}ms`)), ms);
214
+ return fetch(url, { ...opts, signal: ctl.signal }).finally(() => clearTimeout(t));
215
+ }
216
+
203
217
  // The server advertises its manifest version on every response. When it's newer
204
218
  // than what this CLI shipped knowing, nudge ONCE on stderr — the agent re-reads
205
219
  // the manifest (whats_new) and learns the new capabilities without polling.
@@ -331,9 +345,10 @@ async function cableSession({ channel, deadline, onUp, onFrame }) {
331
345
  if (outcome === 'deadline') return 'deadline';
332
346
  if (!outcome.startsWith('down: ')) return outcome; // caller-driven finish (e.g. 'answered')
333
347
  wsFails++;
334
- if (wsFails >= 4) return 'ws-unavailable';
348
+ const MAX_WS_FAILS = 4; // then fall back to polling for the rest of the session
349
+ if (wsFails >= MAX_WS_FAILS) return 'ws-unavailable';
335
350
  const backoff = Math.min(2000 * wsFails, 10000);
336
- console.error(`pidge: realtime socket ${outcome.replace('down: ', '')} — reconnecting in ${Math.round(backoff / 1000)}s (attempt ${wsFails}/3)`);
351
+ console.error(`pidge: realtime socket ${outcome.replace('down: ', '')} — reconnecting in ${Math.round(backoff / 1000)}s (attempt ${wsFails}/${MAX_WS_FAILS})`);
337
352
  await sleep(backoff);
338
353
  }
339
354
  return 'deadline';
@@ -499,7 +514,7 @@ async function doWait(cid, { timeout, interval }) {
499
514
  const url = `${BASE}/api/v1/notifications/${encodeURIComponent(cid)}${waitS > 0 ? `?wait=${waitS}` : ''}`;
500
515
  const askedAt = Date.now();
501
516
  try {
502
- const res = await fetch(url, { headers });
517
+ const res = await fetchT(url, { headers }, (waitS + 10) * 1000);
503
518
  checkManifestNews(res);
504
519
  if (res.status === 200) {
505
520
  health.ok();
@@ -552,7 +567,7 @@ async function realtimeWait(cid, { timeout, interval }) {
552
567
  const deadline = Date.now() + timeout * 1000;
553
568
  const answered = async () => {
554
569
  try {
555
- const res = await fetch(`${BASE}/api/v1/notifications/${encodeURIComponent(cid)}`, { headers });
570
+ const res = await fetchT(`${BASE}/api/v1/notifications/${encodeURIComponent(cid)}`, { headers });
556
571
  if (res.status !== 200) return false;
557
572
  const data = await res.json().catch(() => ({}));
558
573
  return !!(data.responded && data.chosen_action && data.chosen_action.kind !== 'snoozed');
@@ -708,7 +723,11 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
708
723
  console.log(JSON.stringify(msgs, null, 2));
709
724
  const upTo = Math.max(...msgs.map((m) => m.id));
710
725
  try {
711
- const ack = await fetch(`${BASE}/api/v1/messages/ack`, {
726
+ // fetchT, not fetch: a wedged proxy stalling this ack would otherwise
727
+ // pin the process forever (the WS drain path awaits printAndAck's exit
728
+ // with no deadline) — messages are already printed, so a timeout here
729
+ // just re-serves them next listen (at-least-once).
730
+ const ack = await fetchT(`${BASE}/api/v1/messages/ack`, {
712
731
  method: 'POST', headers, body: JSON.stringify({ up_to: upTo }),
713
732
  });
714
733
  if (ack.status >= 200 && ack.status < 300) {
@@ -731,7 +750,7 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
731
750
  if (draining) return;
732
751
  draining = true;
733
752
  try {
734
- const res = await fetch(`${BASE}/api/v1/messages`, { headers });
753
+ const res = await fetchT(`${BASE}/api/v1/messages`, { headers });
735
754
  checkManifestNews(res);
736
755
  if (res.status === 200) {
737
756
  health.ok();
@@ -769,7 +788,7 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
769
788
  const waitS = health.degraded ? 0 : Math.max(0, Math.min(25, Math.ceil((deadline - Date.now()) / 1000)));
770
789
  const askedAt = Date.now();
771
790
  try {
772
- const res = await fetch(`${BASE}/api/v1/messages${waitS > 0 ? `?wait=${waitS}` : ''}`, { headers });
791
+ const res = await fetchT(`${BASE}/api/v1/messages${waitS > 0 ? `?wait=${waitS}` : ''}`, { headers }, (waitS + 10) * 1000);
773
792
  checkManifestNews(res);
774
793
  if (res.status === 200) {
775
794
  health.ok();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pidge-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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",