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.
- package/README.md +15 -1
- package/bin/pidge.js +86 -5
- 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 =
|
|
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(
|
|
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
|
-
|
|
1093
|
-
|
|
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
|