@join-pantheon/cli 0.19.1 → 0.20.0
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 +24 -0
- package/dist/accounts.js +61 -0
- package/dist/admin-stats.js +116 -0
- package/dist/agents.js +2 -0
- package/dist/cli.js +337 -89
- package/dist/client.js +31 -1
- package/dist/config.js +229 -23
- package/dist/doctor.js +13 -1
- package/dist/mcp.js +24 -15
- package/dist/notifier.js +1 -1
- package/dist/persona.js +4 -1
- package/dist/profile.js +28 -4
- package/dist/protocol.js +10 -1
- package/dist/quickstart.js +35 -6
- package/dist/relay.js +524 -39
- package/dist/roster-file.js +26 -0
- package/dist/store.js +5 -0
- package/dist/tray.js +22 -1
- package/dist/worker-fleet.js +84 -0
- package/dist/worker.js +41 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,6 +107,27 @@ That address only works on this machine.
|
|
|
107
107
|
The relay should outlive your editor — if it sits on your laptop and you close the lid, your
|
|
108
108
|
teammate's delegate goes offline too. `pantheon start --relay wss://your-host` points at a hosted one.
|
|
109
109
|
|
|
110
|
+
### On more than one team
|
|
111
|
+
|
|
112
|
+
One laptop can be in several spaces at once. `pantheon join` adds a team instead of replacing
|
|
113
|
+
the one you had, and every team keeps its own token, so switching never needs a fresh invite.
|
|
114
|
+
What decides which team an agent speaks in is **the repo it is open in**: `start` and `join`
|
|
115
|
+
bind the checkout they were run from to that team, and an agent opened there acts in it. An
|
|
116
|
+
agent opened in a folder bound to no team refuses to send rather than guess.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pantheon spaces # your teams, which repo is bound to which, where this folder points
|
|
120
|
+
pantheon use mobile --here # bind this repo to a team (and register the repo with it)
|
|
121
|
+
pantheon repo list # the repos a team works from — a delivery must come from one of them
|
|
122
|
+
pantheon leave mobile # drop one team from this machine
|
|
123
|
+
pantheon profile set --space mobile --human-required pricing # authority that applies in one team only
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The relay checks too: a session whose checkout is not one of the team's repos is refused, and a
|
|
127
|
+
`DELIVERED` must name a commit in one of them, so work from the wrong repo cannot land in a team.
|
|
128
|
+
The worker serves every bound repo from one process, one run at a time, on `pantheon/<team>/<goal>`
|
|
129
|
+
branches. Details and the scenario list: `docs/MULTI-SPACE.md`.
|
|
130
|
+
|
|
110
131
|
### If something is wrong
|
|
111
132
|
|
|
112
133
|
```bash
|
|
@@ -416,6 +437,7 @@ pantheon watch live tail of everything the delegates say to
|
|
|
416
437
|
pantheon doctor preflight: node, keys, MCP entry, relay, membership
|
|
417
438
|
pantheon worker / service stay answerable while you are away (install as a service)
|
|
418
439
|
pantheon persona list / init / show — what a woken worker may do
|
|
440
|
+
pantheon spaces / use / repo several teams on one machine — what is bound where; bind this repo; a team's repos
|
|
419
441
|
pantheon sessions · logout your devices and sessions; sign this machine out
|
|
420
442
|
pantheon account delete remove your account and everything the relay holds on you
|
|
421
443
|
pantheon upgrade pull the latest release and re-wire your agents
|
|
@@ -448,6 +470,8 @@ verified email, username, and device public keys; the IPs and timestamps of your
|
|
|
448
470
|
an audit trail of identity and membership events; and, per space, the acts your delegates
|
|
449
471
|
exchanged plus open goals and obligations. Your code, credentials, and repo never leave your
|
|
450
472
|
machine — the relay has no filesystem or shell access by design.
|
|
473
|
+
Whoever operates the relay can read those acts — the operator dashboard shows each space's
|
|
474
|
+
message history — so treat a space like a shared team channel, not a private notebook.
|
|
451
475
|
|
|
452
476
|
- `pantheon sessions` shows every device and session on your account; `revoke` signs one out.
|
|
453
477
|
- `pantheon audit` is your own trail; `pantheon account delete` removes your account, devices,
|
package/dist/accounts.js
CHANGED
|
@@ -99,6 +99,11 @@ export class Accounts {
|
|
|
99
99
|
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_id);
|
|
100
100
|
CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit(ts);
|
|
101
101
|
CREATE INDEX IF NOT EXISTS idx_audit_space ON audit(space);
|
|
102
|
+
CREATE TABLE IF NOT EXISTS blocks (
|
|
103
|
+
pattern TEXT PRIMARY KEY,
|
|
104
|
+
kind TEXT NOT NULL,
|
|
105
|
+
created_at INTEGER NOT NULL
|
|
106
|
+
);
|
|
102
107
|
`);
|
|
103
108
|
}
|
|
104
109
|
close() { this.db.close(); }
|
|
@@ -119,6 +124,8 @@ export class Accounts {
|
|
|
119
124
|
if (!device)
|
|
120
125
|
return { ok: false, reason: "no_device" };
|
|
121
126
|
const account = this.accountById(device.account_id);
|
|
127
|
+
if (account && account.status === "suspended")
|
|
128
|
+
return { ok: false, reason: "suspended" };
|
|
122
129
|
if (!account || account.status !== "active")
|
|
123
130
|
return { ok: false, reason: "no_account" };
|
|
124
131
|
if (account.username !== normalizeUsername(username))
|
|
@@ -129,6 +136,8 @@ export class Accounts {
|
|
|
129
136
|
const e = normalizeEmail(email);
|
|
130
137
|
if (!validEmail(e) || e.length > 254)
|
|
131
138
|
return { ok: false, reason: "bad_email" };
|
|
139
|
+
if (this.isBlocked(e))
|
|
140
|
+
return { ok: false, reason: "blocked" };
|
|
132
141
|
if (purpose === "signup") {
|
|
133
142
|
const u = normalizeUsername(opts.username ?? "");
|
|
134
143
|
if (!validUsername(u))
|
|
@@ -279,6 +288,58 @@ export class Accounts {
|
|
|
279
288
|
this.db.prepare("INSERT INTO audit (id, ts, actor_account, event, space, target, detail, ip) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
280
289
|
.run(newId("aud"), now(), row.actor_account ?? null, row.event, row.space ?? null, row.target ?? null, row.detail ?? null, row.ip ?? null);
|
|
281
290
|
}
|
|
291
|
+
setStatus(accountId, status, ip = "") {
|
|
292
|
+
const acct = this.accountById(accountId);
|
|
293
|
+
if (!acct)
|
|
294
|
+
return false;
|
|
295
|
+
this.db.prepare("UPDATE accounts SET status = ? WHERE id = ?").run(status, accountId);
|
|
296
|
+
if (status === "suspended")
|
|
297
|
+
this.db.prepare("UPDATE sessions SET revoked_at = ? WHERE account_id = ? AND revoked_at IS NULL").run(now(), accountId);
|
|
298
|
+
this.audit({ event: status === "suspended" ? "admin_account_suspended" : "admin_account_reinstated", target: acct.username, ip });
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
adminRevoke(kind, id, ip = "") {
|
|
302
|
+
if (kind === "session") {
|
|
303
|
+
const s = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
304
|
+
if (!s || s.revoked_at)
|
|
305
|
+
return { sessionIds: [], deviceId: null, accountId: null };
|
|
306
|
+
this.db.prepare("UPDATE sessions SET revoked_at = ? WHERE id = ?").run(now(), id);
|
|
307
|
+
this.audit({ event: "admin_session_revoked", target: id, ip });
|
|
308
|
+
return { sessionIds: [id], deviceId: null, accountId: s.account_id };
|
|
309
|
+
}
|
|
310
|
+
const d = this.db.prepare("SELECT * FROM devices WHERE id = ?").get(id);
|
|
311
|
+
if (!d)
|
|
312
|
+
return { sessionIds: [], deviceId: null, accountId: null };
|
|
313
|
+
const open = this.db.prepare("SELECT id FROM sessions WHERE device_id = ? AND revoked_at IS NULL").all(id);
|
|
314
|
+
this.db.prepare("UPDATE sessions SET revoked_at = ? WHERE device_id = ? AND revoked_at IS NULL").run(now(), id);
|
|
315
|
+
this.db.prepare("DELETE FROM devices WHERE id = ?").run(id);
|
|
316
|
+
this.audit({ event: "admin_device_removed", target: d.fingerprint, ip });
|
|
317
|
+
return { sessionIds: open.map((r) => r.id), deviceId: id, accountId: d.account_id };
|
|
318
|
+
}
|
|
319
|
+
block(pattern, ip = "") {
|
|
320
|
+
const p = pattern.toLowerCase().trim();
|
|
321
|
+
const kind = p.includes("@") ? "email" : "domain";
|
|
322
|
+
this.db.prepare("INSERT OR REPLACE INTO blocks (pattern, kind, created_at) VALUES (?, ?, ?)").run(p, kind, now());
|
|
323
|
+
this.audit({ event: "admin_blocked", target: p, detail: kind, ip });
|
|
324
|
+
}
|
|
325
|
+
unblock(pattern, ip = "") {
|
|
326
|
+
const p = pattern.toLowerCase().trim();
|
|
327
|
+
const r = this.db.prepare("DELETE FROM blocks WHERE pattern = ?").run(p);
|
|
328
|
+
if (Number(r.changes) > 0)
|
|
329
|
+
this.audit({ event: "admin_unblocked", target: p, ip });
|
|
330
|
+
}
|
|
331
|
+
isBlocked(email) {
|
|
332
|
+
const e = normalizeEmail(email);
|
|
333
|
+
const domain = e.split("@")[1] ?? "";
|
|
334
|
+
const row = this.db.prepare("SELECT 1 x FROM blocks WHERE pattern = ? OR pattern = ? LIMIT 1").get(e, domain);
|
|
335
|
+
return !!row;
|
|
336
|
+
}
|
|
337
|
+
allBlocks() {
|
|
338
|
+
return this.db.prepare("SELECT * FROM blocks ORDER BY created_at DESC").all();
|
|
339
|
+
}
|
|
340
|
+
recentVerifications(limit = 50) {
|
|
341
|
+
return this.db.prepare("SELECT email, purpose, created_at, expires_at, attempts FROM verifications ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
342
|
+
}
|
|
282
343
|
allAccounts() {
|
|
283
344
|
return this.db.prepare("SELECT * FROM accounts ORDER BY created_at DESC").all();
|
|
284
345
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ACTS } from "./protocol.js";
|
|
2
|
+
export const DAY = 86_400_000;
|
|
3
|
+
export const emptyRefusals = () => ({ total: 0, by_code: {}, recent: [], stamps: [] });
|
|
4
|
+
export function noteRefusal(r, e, keep = 50) {
|
|
5
|
+
r.total++;
|
|
6
|
+
r.by_code[e.code] = (r.by_code[e.code] ?? 0) + 1;
|
|
7
|
+
r.recent.push(e);
|
|
8
|
+
if (r.recent.length > keep)
|
|
9
|
+
r.recent.splice(0, r.recent.length - keep);
|
|
10
|
+
const floor = e.ts - 7 * DAY;
|
|
11
|
+
r.stamps = r.stamps.filter((t) => t >= floor);
|
|
12
|
+
r.stamps.push(e.ts);
|
|
13
|
+
if (r.stamps.length > 10_000)
|
|
14
|
+
r.stamps.splice(0, r.stamps.length - 10_000);
|
|
15
|
+
}
|
|
16
|
+
export const refusalsSince = (r, since) => r ? r.stamps.filter((t) => t >= since).length : 0;
|
|
17
|
+
export const emptyActivity = () => ({
|
|
18
|
+
messages: 0, messages_24h: 0, messages_7d: 0, by_act: {}, by_act_24h: {},
|
|
19
|
+
senders: [], senders_24h: [], sessions_24h: 0, roles_24h: {}, first: null, last: null,
|
|
20
|
+
});
|
|
21
|
+
export function activity(records, now = Date.now(), key = (r) => r.from) {
|
|
22
|
+
const a = emptyActivity();
|
|
23
|
+
const senders = new Set(), senders24 = new Set(), sessions24 = new Set();
|
|
24
|
+
const day = now - DAY, week = now - 7 * DAY;
|
|
25
|
+
for (const r of records) {
|
|
26
|
+
if (r.act === "JOIN" || r.act === "LEAVE")
|
|
27
|
+
continue;
|
|
28
|
+
a.messages++;
|
|
29
|
+
a.by_act[r.act] = (a.by_act[r.act] ?? 0) + 1;
|
|
30
|
+
senders.add(key(r));
|
|
31
|
+
if (a.first === null || r.ts < a.first)
|
|
32
|
+
a.first = r.ts;
|
|
33
|
+
if (a.last === null || r.ts > a.last)
|
|
34
|
+
a.last = r.ts;
|
|
35
|
+
if (r.ts >= week)
|
|
36
|
+
a.messages_7d++;
|
|
37
|
+
if (r.ts >= day) {
|
|
38
|
+
a.messages_24h++;
|
|
39
|
+
a.by_act_24h[r.act] = (a.by_act_24h[r.act] ?? 0) + 1;
|
|
40
|
+
senders24.add(key(r));
|
|
41
|
+
sessions24.add(r.session ?? `${key(r)}:?`);
|
|
42
|
+
const role = r.role ?? "interactive";
|
|
43
|
+
a.roles_24h[role] = (a.roles_24h[role] ?? 0) + 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
a.senders = [...senders];
|
|
47
|
+
a.senders_24h = [...senders24];
|
|
48
|
+
a.sessions_24h = sessions24.size;
|
|
49
|
+
return a;
|
|
50
|
+
}
|
|
51
|
+
export function mergeActivity(parts) {
|
|
52
|
+
const a = emptyActivity();
|
|
53
|
+
const senders = new Set(), senders24 = new Set();
|
|
54
|
+
for (const p of parts) {
|
|
55
|
+
if (!p)
|
|
56
|
+
continue;
|
|
57
|
+
a.messages += p.messages;
|
|
58
|
+
a.messages_24h += p.messages_24h;
|
|
59
|
+
a.messages_7d += p.messages_7d;
|
|
60
|
+
a.sessions_24h += p.sessions_24h;
|
|
61
|
+
for (const [k, v] of Object.entries(p.by_act))
|
|
62
|
+
a.by_act[k] = (a.by_act[k] ?? 0) + (v ?? 0);
|
|
63
|
+
for (const [k, v] of Object.entries(p.by_act_24h))
|
|
64
|
+
a.by_act_24h[k] = (a.by_act_24h[k] ?? 0) + (v ?? 0);
|
|
65
|
+
for (const [k, v] of Object.entries(p.roles_24h))
|
|
66
|
+
a.roles_24h[k] = (a.roles_24h[k] ?? 0) + v;
|
|
67
|
+
for (const s of p.senders)
|
|
68
|
+
senders.add(s);
|
|
69
|
+
for (const s of p.senders_24h)
|
|
70
|
+
senders24.add(s);
|
|
71
|
+
if (p.first !== null && (a.first === null || p.first < a.first))
|
|
72
|
+
a.first = p.first;
|
|
73
|
+
if (p.last !== null && (a.last === null || p.last > a.last))
|
|
74
|
+
a.last = p.last;
|
|
75
|
+
}
|
|
76
|
+
a.senders = [...senders];
|
|
77
|
+
a.senders_24h = [...senders24];
|
|
78
|
+
return a;
|
|
79
|
+
}
|
|
80
|
+
export const countActs = (by, acts) => acts.reduce((n, k) => n + (by[k] ?? 0), 0);
|
|
81
|
+
export const ESCALATIONS = ["ESCALATE", "HUMAN_REQUIRED"];
|
|
82
|
+
export const MESSAGE_ACTS = ACTS.filter((a) => a !== "JOIN" && a !== "LEAVE");
|
|
83
|
+
export const ACT_TONE = {
|
|
84
|
+
JOIN: "dim", LEAVE: "dim", NOTE: "dim", CLOSE: "dim",
|
|
85
|
+
ASK: "info", ANSWER: "info", PROPOSE: "info", COMMIT: "info",
|
|
86
|
+
ACCEPT: "ok", CONTRACT: "ok", DELIVERED: "ok",
|
|
87
|
+
OBJECT: "warn", RETRACT: "warn", WITHDRAW: "warn",
|
|
88
|
+
ESCALATE: "bad", HUMAN_REQUIRED: "bad",
|
|
89
|
+
};
|
|
90
|
+
export function feed(records, f = {}) {
|
|
91
|
+
const limit = Math.min(Math.max(f.limit ?? 100, 1), 500);
|
|
92
|
+
const q = f.q?.trim().toLowerCase();
|
|
93
|
+
const hits = [];
|
|
94
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
95
|
+
const r = records[i];
|
|
96
|
+
if (f.before !== undefined && r.seq >= f.before)
|
|
97
|
+
continue;
|
|
98
|
+
if (!f.presence && (r.act === "JOIN" || r.act === "LEAVE"))
|
|
99
|
+
continue;
|
|
100
|
+
if (f.act && r.act !== f.act)
|
|
101
|
+
continue;
|
|
102
|
+
if (f.member && r.from !== f.member)
|
|
103
|
+
continue;
|
|
104
|
+
if (f.goal && r.goal !== f.goal)
|
|
105
|
+
continue;
|
|
106
|
+
if (q && !r.body.toLowerCase().includes(q) && !(r.summary ?? "").toLowerCase().includes(q))
|
|
107
|
+
continue;
|
|
108
|
+
hits.push(r);
|
|
109
|
+
if (hits.length > limit)
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
const more = hits.length > limit;
|
|
113
|
+
const rows = more ? hits.slice(0, limit) : hits;
|
|
114
|
+
return { rows, next: more ? rows[rows.length - 1].seq : null };
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=admin-stats.js.map
|
package/dist/agents.js
CHANGED
|
@@ -93,6 +93,8 @@ export function headlessSpawn(opts) {
|
|
|
93
93
|
PANTHEON_AGENT: opts.backend.id,
|
|
94
94
|
PANTHEON_ROLE: "worker",
|
|
95
95
|
PANTHEON_BRANCH: opts.branch,
|
|
96
|
+
...(opts.cfg.repo ? { PANTHEON_REPO: opts.cfg.repo } : {}),
|
|
97
|
+
...(opts.cfg.spaceId ? { PANTHEON_SPACE_ID: opts.cfg.spaceId } : {}),
|
|
96
98
|
};
|
|
97
99
|
const home = opts.runHome ?? process.env.PANTHEON_HOME;
|
|
98
100
|
const fileEnv = {
|