pidge-cli 0.15.1 → 0.15.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.2 — #280 the skill self-heals
4
+
5
+ #280 — the local skill self-heals: any pidge command silently refreshes
6
+ `.claude/skills/pidge/SKILL.md` when the skill revision or manifest version is newer than
7
+ what's installed, so onboarded agents always run the latest skill.
8
+
9
+ - **feat:** a bumpable `SKILL_REVISION` constant + the live manifest version are baked into a
10
+ first-line marker (`<!-- pidge-skill rev=R manifest=N -->`) of every generated `SKILL.md`.
11
+ `ensureSkillFresh()` (run from `checkManifestNews`, which already fires on every command via
12
+ the `x-pidge-manifest-version` header) compares the installed marker against this CLI's spine
13
+ and the server's manifest; when either is newer it silently regenerates the skill and prints
14
+ one stderr note. Only an EXISTING skill is refreshed (never auto-created), at most once per
15
+ process, fully best-effort (a refresh failure never breaks your command), and `QUIET_NAG`
16
+ suppresses the note (the regenerate still happens).
17
+ - **note:** this composes with `@latest` — an agent on `npx pidge-cli@latest` picks up a new
18
+ `SKILL_REVISION` and self-heals the spine on its next command. A PINNED CLI still self-heals
19
+ on a server manifest bump (via the header) but can't gain a newer hand-authored spine without
20
+ updating the CLI. BUMP `SKILL_REVISION` in any future sprint that edits the SKILL.md spine.
21
+
3
22
  ## 0.15.1 — #274-D skill polish
4
23
 
5
24
  skill polish — catalog-first actions, write-for-the-lock-screen (banner = title+body; body_markdown is detail-only), good-report guidance; gold examples now set a plain --body.
package/bin/pidge.js CHANGED
@@ -562,8 +562,17 @@ function fetchT(url, opts = {}, timeoutMs = 30000) {
562
562
  // than what this CLI shipped knowing, nudge on stderr — the agent re-reads the
563
563
  // manifest (whats_new) and learns the new capabilities without polling.
564
564
  const KNOWN_MANIFEST_VERSION = 46;
565
+ // #280: the hand-authored skill SPINE version. BUMP whenever the SKILL.md spine
566
+ // (the non-generated prose in installSkill) changes — an existing install whose
567
+ // baked marker is older than this self-heals on its next pidge command, so an
568
+ // onboarded agent always runs the latest skill without any human action. Start at 1.
569
+ const SKILL_REVISION = 1;
565
570
  const NAG_TTL_MS = 24 * 60 * 60 * 1000; // #241: at most one nag per 24 h
566
571
  let newsWarned = false;
572
+ // #280: the self-heal runs at most ONCE per process (one regeneration, even when
573
+ // many commands/poll-ticks call checkManifestNews). Non-stale checks stay cheap +
574
+ // repeatable; this only latches once an actual heal is attempted.
575
+ let skillHealed = false;
567
576
 
568
577
  // #241: a tiny per-install state cache (~/.config/pidge/state.json, per-agent
569
578
  // when PIDGE_AGENT is set — same dir as the env file). Best-effort: a read-only
@@ -581,9 +590,14 @@ function writeState(patch) {
581
590
  } catch { /* best-effort — the nag just won't persist its throttle */ }
582
591
  }
583
592
 
584
- function checkManifestNews(res) {
585
- if (QUIET_NAG || newsWarned) return;
593
+ async function checkManifestNews(res) {
586
594
  const ver = parseInt(res.headers.get('x-pidge-manifest-version') || '0', 10);
595
+ // #280: the self-heal runs on EVERY command (its own once-guard + cheap
596
+ // first-line read), BEFORE the nag throttle below — it must fire even when the
597
+ // server isn't ahead of KNOWN_MANIFEST_VERSION (a pure spine bump) and even
598
+ // under QUIET_NAG (which only silences the stderr note, never the regenerate).
599
+ await ensureSkillFresh(ver);
600
+ if (QUIET_NAG || newsWarned) return;
587
601
  // (c) only when the server is ahead of what THIS CLI knows.
588
602
  if (!(ver > KNOWN_MANIFEST_VERSION)) return;
589
603
  // #241 throttle: nag at most once per 24 h, and after that window only when the
@@ -607,6 +621,38 @@ function checkManifestNews(res) {
607
621
  console.error(`pidge: the server has NEW capabilities (manifest v${ver}; this CLI knows v${KNOWN_MANIFEST_VERSION}) — pidge is a thin pipe, so you can use any new /notify field RIGHT NOW via --param KEY=VALUE. Read the catalog (whats_new) in the public manifest: curl $PIDGE_URL/api/v1/manifest (public; add -H "Authorization: Bearer $PIDGE_TOKEN" to also see your channel's config). Updating the CLI only matters to gain native flags: npx pidge-cli@latest (a pinned ref never self-updates). Silence this with --quiet-nag or PIDGE_QUIET_NAG=1.`);
608
622
  }
609
623
 
624
+ // #280: STRUCTURAL self-heal — keep the LOCAL skill current with zero human action.
625
+ // The installed .claude/skills/pidge/SKILL.md is written once at onboarding and then
626
+ // goes stale silently (a CLI/skill improvement gives an onboarded agent no signal, so
627
+ // it keeps running the old skill). This silently regenerates it when EITHER trigger
628
+ // fires: this CLI's hand-authored spine moved (SKILL_REVISION > the baked rev) OR the
629
+ // server's manifest moved (serverManifestVersion > the baked manifest) — caught from
630
+ // the x-pidge-manifest-version header that already rides every response. So the agent's
631
+ // NEXT session is always current. Only REFRESHES an existing skill (creating one is
632
+ // onboarding's job, never a side effect of an unrelated command), runs at most once per
633
+ // process, and is wholly best-effort: any failure is swallowed — a skill refresh must
634
+ // NEVER break the user's actual command.
635
+ async function ensureSkillFresh(serverManifestVersion) {
636
+ if (skillHealed) return;
637
+ try {
638
+ // Resolve the path the SAME way installSkill does (cwd-relative).
639
+ const file = path.join(process.cwd(), '.claude', 'skills', 'pidge', 'SKILL.md');
640
+ if (!fs.existsSync(file)) return; // don't auto-create — only refresh an existing skill
641
+ const firstLine = fs.readFileSync(file, 'utf8').split('\n', 1)[0] || '';
642
+ const revM = firstLine.match(/rev=(\d+)/);
643
+ const manM = firstLine.match(/manifest=(\d+)/);
644
+ const installedRev = revM ? parseInt(revM[1], 10) : 0;
645
+ const installedManifest = manM ? parseInt(manM[1], 10) : 0;
646
+ const stale = SKILL_REVISION > installedRev || (serverManifestVersion || 0) > installedManifest;
647
+ if (!stale) return;
648
+ skillHealed = true; // latch BEFORE the network write — attempt the heal at most once
649
+ const r = await installSkill(BASE, TOKEN); // silent: it already writes the file
650
+ // Respect QUIET_NAG/PIDGE_QUIET_NAG for the note only — we STILL regenerated.
651
+ if (!QUIET_NAG)
652
+ console.error(`pidge: refreshed your local Pidge skill (rev ${SKILL_REVISION}, manifest v${r.manifest_version}) — your next session will use it.`);
653
+ } catch { /* best-effort — a skill refresh must never break the user's command */ }
654
+ }
655
+
610
656
  // ---------------------------------------------------------------------------
611
657
  // #119: the health ledger of one blocking session (wait/ask/listen). Drives
612
658
  // (a) automatic DEGRADE from held ?wait= polls to plain GETs after
@@ -976,7 +1022,7 @@ async function doNotify(extra = {}) {
976
1022
  } catch (e) {
977
1023
  die(`pidge: send failed (network): ${e.message}`, 2);
978
1024
  }
979
- checkManifestNews(res);
1025
+ await checkManifestNews(res);
980
1026
  const ok = res.status >= 200 && res.status < 300;
981
1027
  let info = {};
982
1028
  try { info = JSON.parse(raw); } catch { /* leave {} */ }
@@ -1100,7 +1146,7 @@ async function doWait(cid, { timeout, interval }) {
1100
1146
  const askedAt = Date.now();
1101
1147
  try {
1102
1148
  const res = await fetchT(url, { headers }, (waitS + 10) * 1000);
1103
- checkManifestNews(res);
1149
+ await checkManifestNews(res);
1104
1150
  if (res.status === 200) {
1105
1151
  health.ok();
1106
1152
  const data = await res.json().catch(() => ({}));
@@ -1414,7 +1460,7 @@ async function runContract() {
1414
1460
  } catch (e) {
1415
1461
  die(`pidge: contract set failed (network): ${e.message}`, 2);
1416
1462
  }
1417
- checkManifestNews(res);
1463
+ await checkManifestNews(res);
1418
1464
  if (!(res.status >= 200 && res.status < 300)) die(`pidge: contract set failed (${res.status}): ${body}`, 2);
1419
1465
  // stdout = ONLY the operating_contract, never the raw channel JSON. The
1420
1466
  // /channels PATCH echoes the whole channel — INCLUDING "key":"hld_…" — and
@@ -1464,7 +1510,7 @@ async function doSelftest() {
1464
1510
  const res = await fetchT(`${BASE}/api/v1/selftest`, {
1465
1511
  method: 'POST', headers, body: JSON.stringify({ window_seconds: windowS }),
1466
1512
  });
1467
- checkManifestNews(res);
1513
+ await checkManifestNews(res);
1468
1514
  if (res.status < 200 || res.status >= 300) die(`pidge: selftest: the server refused (${res.status}) — is your key valid? try \`pidge doctor\``, 2);
1469
1515
  fired = await res.json();
1470
1516
  } catch (e) {
@@ -1537,7 +1583,7 @@ async function runDoctor(base = BASE, token = TOKEN, sourceLabel = null) {
1537
1583
  process.exit(2);
1538
1584
  }
1539
1585
  const { res, data } = out;
1540
- checkManifestNews(res);
1586
+ await checkManifestNews(res);
1541
1587
  if (res.status === 401) {
1542
1588
  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)');
1543
1589
  process.exit(2);
@@ -1719,7 +1765,8 @@ async function installSkill(base = BASE, token = TOKEN) {
1719
1765
  const profileTable = (m.profiles && m.profiles.decision_table) || [];
1720
1766
  const notes = m.notes || [];
1721
1767
  const exits = (m.cli && m.cli.output) || '';
1722
- const skill = `---
1768
+ const skill = `<!-- pidge-skill rev=${SKILL_REVISION} manifest=${m.manifest_version} -->
1769
+ ---
1723
1770
  name: pidge
1724
1771
  description: Send rich, actionable iPhone notifications to your human and get their decision back (Pidge). Every send is a TYPE (message/important/urgent/event/live) plus an OPTIONAL response (buttons + send-and-go vs wait). Use when finishing long tasks, needing a decision/approval, sending updates with substance, or anything time-anchored. Also covers reading the human's replies back.
1725
1772
  ---
@@ -1881,7 +1928,7 @@ A turn-based agent (Claude Code, anything invoked on demand) stays COMMANDABLE w
1881
1928
  }
1882
1929
  case 'whoami': {
1883
1930
  const { res, data } = await fetchWhoami().catch((e) => { die(`pidge: whoami failed (network): ${e.message}`, 2); });
1884
- checkManifestNews(res);
1931
+ await checkManifestNews(res);
1885
1932
  if (res.status !== 200) die(`pidge: whoami failed (${res.status}): ${JSON.stringify(data)}`, 2);
1886
1933
  console.log(JSON.stringify(data, null, 2));
1887
1934
  console.error(`pidge: you are canal "${data.channel && data.channel.name}" · ${data.devices ?? '?'} device(s)`);
@@ -2017,7 +2064,7 @@ A turn-based agent (Claude Code, anything invoked on demand) stays COMMANDABLE w
2017
2064
  } catch (e) {
2018
2065
  die(`pidge: cancel failed (network): ${e.message}`, 2);
2019
2066
  }
2020
- checkManifestNews(res);
2067
+ await checkManifestNews(res);
2021
2068
  console.log(raw);
2022
2069
  if (res.status >= 200 && res.status < 300) {
2023
2070
  console.error(`pidge: cancelled ${cid} — nothing will fire`);
@@ -2046,7 +2093,7 @@ A turn-based agent (Claude Code, anything invoked on demand) stays COMMANDABLE w
2046
2093
  } catch (e) {
2047
2094
  die(`pidge: ack failed (network): ${e.message}`, 2);
2048
2095
  }
2049
- checkManifestNews(res);
2096
+ await checkManifestNews(res);
2050
2097
  console.log(raw);
2051
2098
  if (!(res.status >= 200 && res.status < 300)) die(`pidge: ack failed (${res.status}): ${raw}`, 2);
2052
2099
  let adata = {};
@@ -2085,7 +2132,7 @@ A turn-based agent (Claude Code, anything invoked on demand) stays COMMANDABLE w
2085
2132
  } catch (e) {
2086
2133
  die(`pidge: inbox failed (network): ${e.message}`, 2);
2087
2134
  }
2088
- checkManifestNews(res);
2135
+ await checkManifestNews(res);
2089
2136
  console.log(raw);
2090
2137
  if (!(res.status >= 200 && res.status < 300)) die(`pidge: inbox failed (${res.status})`, 2);
2091
2138
  let data = {};
@@ -2194,7 +2241,7 @@ A turn-based agent (Claude Code, anything invoked on demand) stays COMMANDABLE w
2194
2241
  draining = true;
2195
2242
  try {
2196
2243
  const res = await fetchT(`${BASE}/api/v1/messages${queueQs}`, { headers });
2197
- checkManifestNews(res);
2244
+ await checkManifestNews(res);
2198
2245
  if (res.status === 200) {
2199
2246
  health.ok();
2200
2247
  const msgs = (await res.json().catch(() => ({}))).messages || [];
@@ -2253,7 +2300,7 @@ A turn-based agent (Claude Code, anything invoked on demand) stays COMMANDABLE w
2253
2300
  if (waitS > 0) qs.set('wait', String(waitS));
2254
2301
  if (v.all) qs.set('all', 'true');
2255
2302
  const res = await fetchT(`${BASE}/api/v1/messages${qs.size ? `?${qs}` : ''}`, { headers }, (waitS + 10) * 1000);
2256
- checkManifestNews(res);
2303
+ await checkManifestNews(res);
2257
2304
  if (res.status === 200) {
2258
2305
  health.ok();
2259
2306
  const data = await res.json().catch(() => ({}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pidge-cli",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
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",