pidge-cli 0.10.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.
Files changed (3) hide show
  1. package/README.md +15 -1
  2. package/bin/pidge.js +86 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,6 +33,19 @@ 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
+ >
43
+ > **v0.11.0** (Pidge manifest v30): the **first-contact WOW** (#217). New **`pidge hello`** —
44
+ > your channel's debut handshake, narrated LIVE on the lock screen by a server-driven 3-stage
45
+ > Live Activity (Conectando → toque para confirmar → Concluído ✓) so your human *sees* the
46
+ > agent→human→agent loop close. Send + wait in one; run it as your first contact on a fresh
47
+ > channel. (It's a thin `ask --template onboarding` wrapper — that path already works on v0.10.0.)
48
+ >
36
49
  > **v0.10.0** (Pidge manifest v29): the onboarding-close batch. **`pidge selftest`** proves
37
50
  > your listener works by ROUND-TRIP (#205) — fire a nonce, run the listener, confirm it
38
51
  > picks it up + acks in time (PASS exit 0 / FAIL exit 2 with the likely cause). `listen_mode`
@@ -121,6 +134,7 @@ npx pidge-cli notify --title "Relatório" --file ./relatorio.xlsx
121
134
 
122
135
  | Command | What it does |
123
136
  |---|---|
137
+ | `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. |
124
138
  | `ask` | Send a notification **and block** until the human answers; prints the chosen action JSON. The default for agents. |
125
139
  | `notify` | Send only. Prints the raw 201 JSON; the `correlation_id` + warnings go to stderr. |
126
140
  | `wait <correlation_id>` | Block on an already-sent notification until it's answered. |
@@ -131,7 +145,7 @@ npx pidge-cli notify --title "Relatório" --file ./relatorio.xlsx
131
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). |
132
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. |
133
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). |
134
- | `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. |
135
149
  | `whoami` | Which channel does this key speak for (JSON). |
136
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. |
137
151
  | `--version` | Print the CLI version. |
package/bin/pidge.js CHANGED
@@ -157,6 +157,10 @@ USAGE
157
157
  pidge doctor validate the setup WITHOUT exposing secrets:
158
158
  env source, server, key, "canal X · N devices"
159
159
  pidge whoami which channel does this key speak for (JSON)
160
+ pidge hello [options] FIRST-CONTACT WOW (#217): your channel's debut handshake,
161
+ narrated LIVE on the lock screen by a 3-stage Live Activity
162
+ (Conectando → toque para confirmar → Concluído ✓). send + wait
163
+ in one — run it as your FIRST contact on a fresh channel.
160
164
  pidge ask [options] send AND wait for the answer (prints chosen_action JSON)
161
165
  pidge notify [options] send only (prints the 201 JSON)
162
166
  pidge wait <correlation_id> [options] block on an already-sent notification
@@ -298,7 +302,7 @@ function fetchT(url, opts = {}, timeoutMs = 30000) {
298
302
  // The server advertises its manifest version on every response. When it's newer
299
303
  // than what this CLI shipped knowing, nudge ONCE on stderr — the agent re-reads
300
304
  // the manifest (whats_new) and learns the new capabilities without polling.
301
- const KNOWN_MANIFEST_VERSION = 28;
305
+ const KNOWN_MANIFEST_VERSION = 31;
302
306
  let newsWarned = false;
303
307
  function checkManifestNews(res) {
304
308
  const v = parseInt(res.headers.get('x-pidge-manifest-version') || '0', 10);
@@ -377,10 +381,10 @@ function wantRealtime() {
377
381
  // The server pings every ~3 s — that heartbeat is the liveness check (silence
378
382
  // >15 s ⇒ the socket is dead even if TCP hasn't noticed; close → caller
379
383
  // reconnects). Returns {close()} or null if the constructor itself failed.
380
- function cableSubscribe({ channel, onUp, onFrame, onDown }) {
384
+ function cableSubscribe({ channel, onUp, onFrame, onDown, base = BASE, token = TOKEN }) {
381
385
  let ws;
382
386
  try {
383
- 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]);
384
388
  } catch (e) { onDown(e.message); return null; }
385
389
  const identifier = JSON.stringify({ channel });
386
390
  let lastBeat = Date.now();
@@ -448,6 +452,41 @@ async function cableSession({ channel, deadline, onUp, onFrame }) {
448
452
  return 'deadline';
449
453
  }
450
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
+
451
490
  // Map CLI flags → the /notify JSON body, including only what was provided.
452
491
  function buildBody() {
453
492
  if (!v.title) die('pidge: --title is required', 1);
@@ -1089,8 +1128,25 @@ async function runDoctor(base = BASE, token = TOKEN, sourceLabel = null) {
1089
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.');
1090
1129
  process.exit(2);
1091
1130
  }
1092
- console.error('pidge doctor: all good try: pidge ask --template decision --title "Pidge funcionando?"');
1093
- 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 }));
1094
1150
  process.exit(0);
1095
1151
  }
1096
1152
 
@@ -1273,6 +1329,31 @@ ${notes.map((n) => `- ${n}`).join('\n')}
1273
1329
  process.exit(ok ? 0 : 2);
1274
1330
  break;
1275
1331
  }
1332
+ case 'hello': {
1333
+ // #217 — the first-contact WOW: fire the onboarding handshake and block on
1334
+ // your human's confirmation. The SERVER narrates a 3-stage Live Activity on
1335
+ // the lock screen (Conectando → toque para confirmar → Concluído ✓) so your
1336
+ // human SEES the agent→human→agent loop close. One command: send + wait.
1337
+ // Run it as your FIRST contact on a fresh channel. A thin wrapper over `ask`:
1338
+ // it just pins template=onboarding and friendly default copy.
1339
+ if (v.profile === 'tracking')
1340
+ die('pidge: `hello --profile tracking` makes no sense — the handshake waits for a confirmation, which tracking (Live-Activity-only) never produces', 1);
1341
+ v.template = 'onboarding';
1342
+ if (v.title === undefined) v.title = 'Seu agente está pronto 🐦';
1343
+ if (v.body === undefined) v.body = 'Toque em Feito ✓ para confirmar que me recebeu — você vai ver o teste fechar na tela.';
1344
+ const cid = v['correlation-id'] || crypto.randomUUID();
1345
+ v['correlation-id'] = cid;
1346
+ console.error(`pidge: correlation_id=${cid}`);
1347
+ const { ok, info } = await doNotify();
1348
+ if (!ok) process.exit(2);
1349
+ console.error(`pidge: WOW sent (${info.registered_devices} device(s)) — watch the lock screen narrate the handshake; waiting for your human to confirm on ${cid}`);
1350
+ // No --timeout ⇒ obey the template's suggestion from the 201 echo (onboarding
1351
+ // = 3600 s); explicit --timeout always wins.
1352
+ let timeout = num(v.timeout, NaN);
1353
+ if (!Number.isFinite(timeout)) timeout = info.suggested_ask_timeout || 3600;
1354
+ await waitForAnswer(cid, { timeout, interval: num(v.interval, 30) });
1355
+ break;
1356
+ }
1276
1357
  case 'ask': {
1277
1358
  // Send, then block on the answer in one shot. stdout = ONLY chosen_action JSON.
1278
1359
  // tracking is Live-Activity-only: it NEVER produces a chosen_action, so an ask
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pidge-cli",
3
- "version": "0.10.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",