pidge-cli 0.7.0 → 0.8.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 +32 -3
- package/bin/pidge.js +93 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,19 +11,48 @@ then gets the answer as JSON — no webhook, no polling loop to write.
|
|
|
11
11
|
> current spec (fields, profiles, guarantees). This CLI is a thin pipe over it — any
|
|
12
12
|
> new server field works without a CLI update via `--param key=value`.
|
|
13
13
|
|
|
14
|
-
## Setup in one command (v0.
|
|
14
|
+
## Setup in one command (v0.8.0 — the claim flow)
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
# The human copies a setup prompt from the Pidge app (Canais → the channel) —
|
|
18
18
|
# it carries a SINGLE-USE claim code (15 min TTL), never the key:
|
|
19
19
|
npx pidge-cli setup --claim <code> --url https://pidge.sh
|
|
20
|
-
# → exchanges the code for the real key,
|
|
21
|
-
#
|
|
20
|
+
# → exchanges the code for the real key, stores it (chmod 600), runs `pidge doctor`.
|
|
21
|
+
# The secret never appears on screen or in any chat (the CLI writes it).
|
|
22
22
|
|
|
23
23
|
npx pidge-cli doctor # validate anytime: env source, server, key, "canal X · N devices"
|
|
24
24
|
npx pidge-cli whoami # which channel does this key speak for (JSON)
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
### Many agents on one machine — isolate them (read this)
|
|
28
|
+
|
|
29
|
+
`~/.config/pidge/env` is **one slot per machine-user**: every agent without its
|
|
30
|
+
own identity reads the same key, so one agent's `setup` makes another agent send
|
|
31
|
+
as the wrong channel (this bit us for real — a cron got hijacked). Each agent
|
|
32
|
+
must have its **own** identity. Cheapest correct setups, in order:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# A. per-agent env var — the cleanest; the human sets it at the agent's launch
|
|
36
|
+
# (systemd unit / launcher / profile). Env var always wins over any file.
|
|
37
|
+
export PIDGE_TOKEN=hld_… # this agent only
|
|
38
|
+
|
|
39
|
+
# B. per-agent config file — set ONE non-secret id at launch; the CLI namespaces
|
|
40
|
+
# the file to ~/.config/pidge/agents/<id>/env and still writes the key for you
|
|
41
|
+
# (no secret in the agent's chat). setup/doctor/everything follow it.
|
|
42
|
+
export PIDGE_AGENT=javier
|
|
43
|
+
npx pidge-cli setup --claim <code>
|
|
44
|
+
|
|
45
|
+
# C. you're at YOUR terminal and want the env var hygienically from a claim:
|
|
46
|
+
npx pidge-cli setup --claim <code> --print # prints `export …`; writes nothing
|
|
47
|
+
# paste the two lines into THAT agent's launcher. NEVER run --print as an agent
|
|
48
|
+
# (the key would land in its context) — that's what A/B are for.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The bare `~/.config/pidge/env` (no `PIDGE_AGENT`) is fine for a **single** agent;
|
|
52
|
+
`pidge doctor` warns loudly when you're on that shared file. Lost the local key?
|
|
53
|
+
Just re-claim — `POST /claim` returns the channel's **same** key, so re-running
|
|
54
|
+
setup restores the exact identity.
|
|
55
|
+
|
|
27
56
|
## Use it (no install — via npx)
|
|
28
57
|
|
|
29
58
|
```bash
|
package/bin/pidge.js
CHANGED
|
@@ -41,13 +41,28 @@ const path = require('node:path');
|
|
|
41
41
|
const os = require('node:os');
|
|
42
42
|
const crypto = require('node:crypto');
|
|
43
43
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
44
|
+
// Per-agent isolation (incident 2026-06-13): ~/.config/pidge/env is one slot
|
|
45
|
+
// per machine-user, so N agents sharing a HOME share an identity — one agent's
|
|
46
|
+
// setup hijacked another's cron. The fix is a NON-secret namespacing var the
|
|
47
|
+
// human sets ONCE at each agent's launch: PIDGE_AGENT=<id> → the config lives
|
|
48
|
+
// at ~/.config/pidge/agents/<id>/env, isolated by construction. The CLI still
|
|
49
|
+
// WRITES the key (the agent never sees it — #57 hygiene intact), it's just
|
|
50
|
+
// per-agent now. No PIDGE_AGENT ⇒ the legacy shared file (single-agent only).
|
|
51
|
+
// (An explicit PIDGE_TOKEN env var still wins over any file — the purest
|
|
52
|
+
// per-agent path.)
|
|
53
|
+
const AGENT_ID = (process.env.PIDGE_AGENT || '').trim().replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 64);
|
|
54
|
+
function pidgeConfigDir() {
|
|
55
|
+
const base = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'pidge');
|
|
56
|
+
return AGENT_ID ? path.join(base, 'agents', AGENT_ID) : base;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// #57 token hygiene: when the env vars are unset, fall back to the config file
|
|
60
|
+
// (KEY=VALUE the CLI writes during setup, or the HUMAN writes once) so the raw
|
|
61
|
+
// hld_… key never rides the agent's chat/context. Explicit env vars always win;
|
|
62
|
+
// `export ` prefixes, quotes and #comments are tolerated.
|
|
48
63
|
function configEnv() {
|
|
49
64
|
try {
|
|
50
|
-
const file = path.join(
|
|
65
|
+
const file = path.join(pidgeConfigDir(), 'env');
|
|
51
66
|
const out = {};
|
|
52
67
|
for (let line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
53
68
|
line = line.trim().replace(/^export\s+/, '');
|
|
@@ -107,14 +122,21 @@ const OPTIONS = {
|
|
|
107
122
|
claim: { type: 'string' }, // setup --claim <single-use code>
|
|
108
123
|
// #157 P2: listen keeps going after a batch (supervisor loop, one process)
|
|
109
124
|
follow: { type: 'boolean' },
|
|
125
|
+
force: { type: 'boolean' }, // setup: overwrite a config owned by ANOTHER channel
|
|
126
|
+
print: { type: 'boolean' }, // setup: print export lines instead of writing a file (per-agent, human runs it)
|
|
110
127
|
};
|
|
111
128
|
|
|
112
129
|
const USAGE = `pidge — send an iPhone notification to a human and block until they answer.
|
|
113
130
|
|
|
114
131
|
USAGE
|
|
115
132
|
pidge setup --claim CODE [--url BASE] one-shot onboarding (#110): exchange the single-use
|
|
116
|
-
code for the channel key, store it
|
|
117
|
-
|
|
133
|
+
code for the channel key, store it, run doctor.
|
|
134
|
+
MULTI-AGENT: set PIDGE_AGENT=<id> at each agent's launch
|
|
135
|
+
→ isolated config ~/.config/pidge/agents/<id>/env.
|
|
136
|
+
--print emit 'export PIDGE_TOKEN=…' instead of a file
|
|
137
|
+
(you run it in YOUR terminal; paste into the
|
|
138
|
+
agent's launcher — never run --print as an agent)
|
|
139
|
+
--force overwrite a shared file owned by another channel
|
|
118
140
|
pidge doctor validate the setup WITHOUT exposing secrets:
|
|
119
141
|
env source, server, key, "canal X · N devices"
|
|
120
142
|
pidge whoami which channel does this key speak for (JSON)
|
|
@@ -188,9 +210,13 @@ OPTIONS (notify / ask)
|
|
|
188
210
|
|
|
189
211
|
ENV
|
|
190
212
|
PIDGE_URL your Pidge server (default http://localhost:3000; HERALD_URL honored)
|
|
191
|
-
PIDGE_TOKEN your channel's bearer key (required; HERALD_TOKEN honored)
|
|
192
|
-
|
|
193
|
-
|
|
213
|
+
PIDGE_TOKEN your channel's bearer key (required; HERALD_TOKEN honored). Setting
|
|
214
|
+
this per agent at launch is the cleanest multi-agent isolation —
|
|
215
|
+
env var always wins over any file.
|
|
216
|
+
PIDGE_AGENT <id> namespacing the config file to ~/.config/pidge/agents/<id>/env
|
|
217
|
+
so N agents on one machine never share an identity (the CLI still
|
|
218
|
+
writes the key — no secret in the agent's chat). Unset ⇒ the legacy
|
|
219
|
+
shared ~/.config/pidge/env (single-agent only).
|
|
194
220
|
|
|
195
221
|
OUTPUT
|
|
196
222
|
stdout is machine-readable (notify→201 JSON; ask/wait→chosen_action JSON);
|
|
@@ -241,7 +267,7 @@ function fetchT(url, opts = {}, timeoutMs = 30000) {
|
|
|
241
267
|
// The server advertises its manifest version on every response. When it's newer
|
|
242
268
|
// than what this CLI shipped knowing, nudge ONCE on stderr — the agent re-reads
|
|
243
269
|
// the manifest (whats_new) and learns the new capabilities without polling.
|
|
244
|
-
const KNOWN_MANIFEST_VERSION =
|
|
270
|
+
const KNOWN_MANIFEST_VERSION = 26;
|
|
245
271
|
let newsWarned = false;
|
|
246
272
|
function checkManifestNews(res) {
|
|
247
273
|
const v = parseInt(res.headers.get('x-pidge-manifest-version') || '0', 10);
|
|
@@ -647,13 +673,16 @@ const num = (val, fallback) => (val !== undefined ? parseInt(val, 10) : fallback
|
|
|
647
673
|
// Onboarding v2 (#110): setup --claim / doctor / whoami / skill install.
|
|
648
674
|
// ---------------------------------------------------------------------------
|
|
649
675
|
|
|
650
|
-
const CONFIG_DIR =
|
|
676
|
+
const CONFIG_DIR = pidgeConfigDir();
|
|
651
677
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'env');
|
|
678
|
+
// True when we're reading the LEGACY shared file (no PIDGE_AGENT, no env var) —
|
|
679
|
+
// the multi-agent footgun. doctor warns on it.
|
|
680
|
+
const ON_SHARED_FILE = !AGENT_ID && !process.env.PIDGE_TOKEN && !process.env.HERALD_TOKEN && !!FILE_ENV.PIDGE_TOKEN;
|
|
652
681
|
|
|
653
682
|
// Where the token came from — doctor narrates it, setup respects precedence.
|
|
654
683
|
function tokenSource() {
|
|
655
|
-
if (process.env.PIDGE_TOKEN || process.env.HERALD_TOKEN) return 'env var';
|
|
656
|
-
if (FILE_ENV.PIDGE_TOKEN) return CONFIG_FILE;
|
|
684
|
+
if (process.env.PIDGE_TOKEN || process.env.HERALD_TOKEN) return 'env var (per-agent)';
|
|
685
|
+
if (FILE_ENV.PIDGE_TOKEN) return CONFIG_FILE + (AGENT_ID ? ` (PIDGE_AGENT=${AGENT_ID})` : ' (shared)');
|
|
657
686
|
return null;
|
|
658
687
|
}
|
|
659
688
|
|
|
@@ -669,8 +698,11 @@ async function fetchWhoami(base = BASE, token = TOKEN) {
|
|
|
669
698
|
|
|
670
699
|
// doctor: validate the setup WITHOUT exposing secrets. Narration on stderr,
|
|
671
700
|
// a compact machine-readable line on stdout. Exit 0 healthy / 2 broken.
|
|
672
|
-
async function runDoctor(base = BASE, token = TOKEN) {
|
|
673
|
-
|
|
701
|
+
async function runDoctor(base = BASE, token = TOKEN, sourceLabel = null) {
|
|
702
|
+
// sourceLabel is passed by setup (it knows exactly where the key went —
|
|
703
|
+
// a per-agent file, the shared file, or NOWHERE for --print); the bare
|
|
704
|
+
// `doctor` command computes it from the env/file precedence.
|
|
705
|
+
const source = sourceLabel || (token === TOKEN ? tokenSource() : CONFIG_FILE);
|
|
674
706
|
if (!token) {
|
|
675
707
|
console.error('pidge doctor: NO TOKEN — set PIDGE_TOKEN, or onboard with `pidge setup --claim <code>` (the human copies the code from the Pidge app)');
|
|
676
708
|
process.exit(2);
|
|
@@ -709,6 +741,8 @@ async function runDoctor(base = BASE, token = TOKEN) {
|
|
|
709
741
|
console.error(`pidge doctor: key valid — canal "${data.channel && data.channel.name}" · ${devices} device(s)`);
|
|
710
742
|
if (devices === 0)
|
|
711
743
|
console.error('pidge doctor: WARNING — 0 devices: sends will reach NOBODY until the human installs/opens the Pidge app on their iPhone');
|
|
744
|
+
if (ON_SHARED_FILE)
|
|
745
|
+
console.error(`pidge doctor: WARNING — reading the SHARED file ${CONFIG_FILE}. If another agent runs on this machine, it reads the SAME key and you'll send as each other (the 2026-06-13 incident). Isolate: set PIDGE_AGENT=<id> at this agent's launch (config → ~/.config/pidge/agents/<id>/env) or give it its own PIDGE_TOKEN.`);
|
|
712
746
|
console.error('pidge doctor: all good — try: pidge ask --template decision --title "Pidge funcionando?"');
|
|
713
747
|
console.log(JSON.stringify({ ok: true, base_url: base, channel: data.channel, devices, manifest_version: data.manifest_version }));
|
|
714
748
|
process.exit(0);
|
|
@@ -721,6 +755,28 @@ async function runSetup() {
|
|
|
721
755
|
const code = v.claim;
|
|
722
756
|
if (!code) die('pidge: usage: pidge setup --claim <code> [--url <base>] (the human copies the code from the Pidge app)', 1);
|
|
723
757
|
const base = (v.url || process.env.PIDGE_URL || FILE_ENV.PIDGE_URL || 'https://pidge.sh').replace(/\/+$/, '');
|
|
758
|
+
|
|
759
|
+
// THE SHARED-CONFIG GUARD (real incident, 2026-06-13). Only the FILE path can
|
|
760
|
+
// collide; --print writes nothing, so skip it there. CONFIG_FILE is now
|
|
761
|
+
// per-agent when PIDGE_AGENT is set (no collision by construction), but on the
|
|
762
|
+
// legacy shared file two agents still share it — refuse to clobber a file that
|
|
763
|
+
// still authenticates as some channel unless --force. Checked BEFORE the
|
|
764
|
+
// exchange so the single-use code survives the refusal.
|
|
765
|
+
if (!v.print && !v.force && FILE_ENV.PIDGE_TOKEN) {
|
|
766
|
+
let owner = null;
|
|
767
|
+
try {
|
|
768
|
+
const { res: wres, data: wdata } = await fetchWhoami(base, FILE_ENV.PIDGE_TOKEN);
|
|
769
|
+
if (wres.status === 200 && wdata.channel) owner = wdata.channel.name;
|
|
770
|
+
else if (wres.status !== 401) owner = 'um canal (servidor não confirmou)';
|
|
771
|
+
// 401 ⇒ the stored key is dead — overwriting a corpse needs no --force.
|
|
772
|
+
} catch {
|
|
773
|
+
owner = 'um canal (servidor inalcançável para confirmar)';
|
|
774
|
+
}
|
|
775
|
+
if (owner) {
|
|
776
|
+
die(`pidge: ${CONFIG_FILE} já guarda a chave de "${owner}". Sobrescrever faria qualquer agente que lê esse arquivo enviar como o canal novo (incidente real: um cron foi sequestrado assim). O jeito certo de rodar VÁRIOS agentes na mesma máquina: dê a cada um um PIDGE_AGENT=<id> no launch (cada um ganha ~/.config/pidge/agents/<id>/env isolado), ou um PIDGE_TOKEN próprio, ou rode com --print e cole o export no launcher DESTE agente. Substituir mesmo assim? --force (o claim code continua válido — nada foi consumido).`, 2);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
724
780
|
let res, data = {};
|
|
725
781
|
try {
|
|
726
782
|
res = await fetchT(`${base}/api/v1/claim`, {
|
|
@@ -737,11 +793,30 @@ async function runSetup() {
|
|
|
737
793
|
die(`pidge: claim failed (${res.status}): ${JSON.stringify(data)}`, 2);
|
|
738
794
|
|
|
739
795
|
const finalBase = (data.base_url || base).replace(/\/+$/, '');
|
|
796
|
+
const channelName = data.channel && data.channel.name;
|
|
797
|
+
|
|
798
|
+
// --print: the pure per-agent path — emit the export lines (the HUMAN runs
|
|
799
|
+
// this in THEIR terminal and pastes them into the agent's launcher). Stores
|
|
800
|
+
// nothing; the key shows on screen, so DON'T let an agent run --print (the
|
|
801
|
+
// key would land in its context — that's what the file path is for). stdout
|
|
802
|
+
// is eval-able; the guidance goes to stderr.
|
|
803
|
+
if (v.print) {
|
|
804
|
+
console.log(`export PIDGE_URL=${finalBase}`);
|
|
805
|
+
console.log(`export PIDGE_TOKEN=${data.key}`);
|
|
806
|
+
console.error(`pidge: canal "${channelName}" — modo POR-AGENTE (nada gravado em disco). Cole as duas linhas no ambiente de lançamento DESTE agente (systemd/launcher/cron/profile). Cada agente tem a SUA chave; perdeu, é só pegar outro código no app e re-rodar (a chave do canal é a MESMA). NÃO rode --print de dentro de um agente — a chave apareceria no contexto dele.`);
|
|
807
|
+
await runDoctor(finalBase, data.key, 'fresh claim (per-agent env — not stored on disk)');
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// File path (default): the CLI writes the key — the agent never sees it
|
|
812
|
+
// (#57). Per-agent when PIDGE_AGENT is set; otherwise the legacy shared file.
|
|
740
813
|
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
741
814
|
fs.writeFileSync(CONFIG_FILE, `PIDGE_URL=${finalBase}\nPIDGE_TOKEN=${data.key}\n`, { mode: 0o600 });
|
|
742
815
|
try { fs.chmodSync(CONFIG_FILE, 0o600); } catch { /* mode set on create */ }
|
|
743
|
-
console.error(`pidge:
|
|
744
|
-
|
|
816
|
+
console.error(`pidge: canal "${channelName}" configurado — chave em ${CONFIG_FILE} (chmod 600, nunca exibida)`);
|
|
817
|
+
if (!AGENT_ID)
|
|
818
|
+
console.error('pidge: este é o arquivo COMPARTILHADO (single-agent). Vai rodar 2+ agentes nesta máquina? Dê a cada um PIDGE_AGENT=<id> no launch (arquivo isolado por agente) — senão eles enviam como o mesmo canal.');
|
|
819
|
+
await runDoctor(finalBase, data.key, CONFIG_FILE);
|
|
745
820
|
}
|
|
746
821
|
|
|
747
822
|
// skill install (#110e): persistent Pidge knowledge for Claude Code agents —
|