pidge-cli 0.11.0 → 0.11.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.
package/README.md CHANGED
@@ -33,6 +33,13 @@ then gets the answer as JSON — no webhook, no polling loop to write.
33
33
  > stdout now carries only the `operating_contract`, so the key never lands in an agent's
34
34
  > transcript/logs.
35
35
  >
36
+ > **v0.11.1** (Pidge manifest v31): **`pidge doctor` now probes the realtime path** (#171) —
37
+ > after the HTTP checks it opens a quick `/cable` subscription and reports `realtime: ok` or
38
+ > `realtime: INDISPONÍVEL` (the #119 failure class an HTTP-only doctor couldn't see: an edge
39
+ > killing held responses, a proxy refusing the WebSocket). Exit stays `0` — an unavailable WS
40
+ > just degrades `listen` to polling — but you learn it BEFORE the first deaf listen. The doctor
41
+ > hint now leads with `pidge hello`, and the version nudge knows v31 (#229).
42
+ >
36
43
  > **v0.11.0** (Pidge manifest v30): the **first-contact WOW** (#217). New **`pidge hello`** —
37
44
  > your channel's debut handshake, narrated LIVE on the lock screen by a server-driven 3-stage
38
45
  > Live Activity (Conectando → toque para confirmar → Concluído ✓) so your human *sees* the
@@ -138,7 +145,7 @@ npx pidge-cli notify --title "Relatório" --file ./relatorio.xlsx
138
145
  | `contract set <k>=<v>` / `contract show` | **v0.9.0:** DECLARE how you operate (`keep_connection_alive`, `mirror_in_origin_session`, `listen_mode=turn_based\|persistent\|external_daemon`, `quiet_when_idle`). **Advisory, never policy** — you declare, the human registers their expectation and *sees* if you honor it; Pidge enforces nothing. An unknown key/bad value is rejected locally (exit 1). |
139
146
  | `selftest [--window N]` | **v0.10.0 (#205):** prove your listener works by ROUND-TRIP — fire a nonce, run the listener, confirm it picks it up + acks in time. PASS exit `0` / FAIL exit `2` with the likely cause (timeout / orphan / transport). Run it as the last onboarding step + whenever sends seem to go unheard. |
140
147
  | `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. **v0.9.0** also claims channel ownership so `doctor` can warn on a silent key swap. **v0.9.1+** declares your `operating_contract` (default `listen_mode=turn_based`; `--listen-mode persistent\|external_daemon` for a supervisor/daemon). |
141
- | `doctor` | Validate the setup **without exposing secrets**: env source, server reachable, key valid, **honest device reach**, channel ownership. Exit 0/2. |
148
+ | `doctor` | Validate the setup **without exposing secrets**: env source, server reachable, key valid, **honest device reach**, channel ownership, and (**v0.11.1, #171**) a **realtime probe** (`realtime: ok / INDISPONÍVEL` — exit stays 0; an unavailable WS just degrades `listen` to polling). Exit 0/2. |
142
149
  | `whoami` | Which channel does this key speak for (JSON). |
143
150
  | `skill install` | Write `.claude/skills/pidge/SKILL.md` generated from the live manifest — persistent Pidge knowledge for Claude Code agents; re-run to update. |
144
151
  | `--version` | Print the CLI version. |
package/bin/pidge.js CHANGED
@@ -302,7 +302,7 @@ function fetchT(url, opts = {}, timeoutMs = 30000) {
302
302
  // The server advertises its manifest version on every response. When it's newer
303
303
  // than what this CLI shipped knowing, nudge ONCE on stderr — the agent re-reads
304
304
  // the manifest (whats_new) and learns the new capabilities without polling.
305
- const KNOWN_MANIFEST_VERSION = 30;
305
+ const KNOWN_MANIFEST_VERSION = 31;
306
306
  let newsWarned = false;
307
307
  function checkManifestNews(res) {
308
308
  const v = parseInt(res.headers.get('x-pidge-manifest-version') || '0', 10);
@@ -381,10 +381,10 @@ function wantRealtime() {
381
381
  // The server pings every ~3 s — that heartbeat is the liveness check (silence
382
382
  // >15 s ⇒ the socket is dead even if TCP hasn't noticed; close → caller
383
383
  // reconnects). Returns {close()} or null if the constructor itself failed.
384
- function cableSubscribe({ channel, onUp, onFrame, onDown }) {
384
+ function cableSubscribe({ channel, onUp, onFrame, onDown, base = BASE, token = TOKEN }) {
385
385
  let ws;
386
386
  try {
387
- ws = new WebSocket(BASE.replace(/^http/, 'ws') + '/cable', ['actioncable-v1-json', TOKEN]);
387
+ ws = new WebSocket(base.replace(/^http/, 'ws') + '/cable', ['actioncable-v1-json', token]);
388
388
  } catch (e) { onDown(e.message); return null; }
389
389
  const identifier = JSON.stringify({ channel });
390
390
  let lastBeat = Date.now();
@@ -452,6 +452,41 @@ async function cableSession({ channel, deadline, onUp, onFrame }) {
452
452
  return 'deadline';
453
453
  }
454
454
 
455
+ // #171: doctor's realtime probe — the failure class an HTTP-only doctor can't
456
+ // see (#119: an edge killing held responses, a proxy refusing the upgrade). A
457
+ // green HTTP doctor can coexist with a `listen` that's deaf over the socket.
458
+ // Open ONE ConversationChannel subscription on /cable (reusing cableSubscribe —
459
+ // the same client `listen` holds), wait for confirm_subscription, close — all
460
+ // within ≤5 s. Degrade is the CONTRACT, not a failure: an unavailable WS just
461
+ // means `listen` polls (works, less instant), so this NEVER changes the exit
462
+ // code — it only lets the agent KNOW before the first deaf listen. Resolves
463
+ // {ok, ms} | {ok:false, reason} | {skipped:true} (Node <22 has no native
464
+ // WebSocket — same gate as wantRealtime, :373).
465
+ function probeRealtime(base, token) {
466
+ if (typeof WebSocket !== 'function') return Promise.resolve({ skipped: true });
467
+ return new Promise((resolve) => {
468
+ const started = Date.now();
469
+ let settled = false;
470
+ let sub = null;
471
+ const done = (result) => {
472
+ if (settled) return; settled = true;
473
+ clearTimeout(guard);
474
+ if (sub) sub.close();
475
+ resolve(result);
476
+ };
477
+ const guard = setTimeout(() => done({ ok: false, reason: 'no confirm_subscription within 5s' }), 5000);
478
+ sub = cableSubscribe({
479
+ channel: 'ConversationChannel',
480
+ base,
481
+ token,
482
+ onUp: () => done({ ok: true, ms: Date.now() - started }),
483
+ onFrame: () => { /* a stray frame during the probe is irrelevant */ },
484
+ onDown: (why) => done({ ok: false, reason: why }),
485
+ });
486
+ if (!sub) done({ ok: false, reason: 'WebSocket constructor failed' });
487
+ });
488
+ }
489
+
455
490
  // Map CLI flags → the /notify JSON body, including only what was provided.
456
491
  function buildBody() {
457
492
  if (!v.title) die('pidge: --title is required', 1);
@@ -1093,8 +1128,25 @@ async function runDoctor(base = BASE, token = TOKEN, sourceLabel = null) {
1093
1128
  console.error('pidge doctor: BROKEN (exit 2) — devices exist but 0 are reachable (all disabled or on the wrong APNs environment): a send reaches nobody.');
1094
1129
  process.exit(2);
1095
1130
  }
1096
- console.error('pidge doctor: all good try: pidge ask --template decision --title "Pidge funcionando?"');
1097
- console.log(JSON.stringify({ ok: true, base_url: base, channel: data.channel, devices, manifest_version: data.manifest_version }));
1131
+ // #171: probe the realtime path (the #119 failure class an HTTP-only doctor
1132
+ // misses). Exit stays 0 either way an unavailable WS degrades to polling.
1133
+ const rt = await probeRealtime(base, token);
1134
+ let realtime;
1135
+ if (rt.skipped) {
1136
+ realtime = 'skipped';
1137
+ console.error('pidge doctor: realtime: skipped — this Node lacks a native WebSocket (need Node ≥22); `listen` will poll. Upgrade Node for instant delivery.');
1138
+ } else if (rt.ok) {
1139
+ realtime = 'ok';
1140
+ console.error(`pidge doctor: realtime: ok (ws connect + subscribe em ${rt.ms}ms)`);
1141
+ } else {
1142
+ realtime = 'unavailable';
1143
+ 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.`);
1144
+ }
1145
+ // #229: lead with `pidge hello` — the first-contact WOW (send + wait in one),
1146
+ // the same debut the /agent-setup guide leads with. It's a thin wrapper over
1147
+ // `ask --template onboarding` (the underlying mechanism, if you need it raw).
1148
+ console.error('pidge doctor: all good — try: pidge hello (first-contact WOW — send + wait in one; equivalent: pidge ask --template onboarding)');
1149
+ console.log(JSON.stringify({ ok: true, base_url: base, channel: data.channel, devices, manifest_version: data.manifest_version, realtime }));
1098
1150
  process.exit(0);
1099
1151
  }
1100
1152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pidge-cli",
3
- "version": "0.11.0",
3
+ "version": "0.11.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",