@join-pantheon/cli 0.19.1 → 0.21.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 +38 -1
- package/dist/accounts.js +167 -1
- package/dist/admin-stats.js +116 -0
- package/dist/agents.js +6 -0
- package/dist/cli.js +417 -94
- package/dist/client.js +52 -7
- package/dist/config.js +243 -23
- package/dist/doctor.js +22 -1
- package/dist/mcp.js +67 -24
- package/dist/notifier.js +27 -7
- package/dist/notify.js +1 -1
- package/dist/pending.js +3 -1
- package/dist/persona.js +4 -1
- package/dist/plans.js +46 -0
- package/dist/profile.js +33 -4
- package/dist/protocol.js +22 -1
- package/dist/quickstart.js +37 -7
- package/dist/relay.js +732 -49
- package/dist/roster-file.js +39 -0
- package/dist/setup-guide.js +238 -0
- package/dist/store.js +5 -0
- package/dist/tray.js +22 -1
- package/dist/untrusted.js +16 -3
- package/dist/worker-fleet.js +84 -0
- package/dist/worker.js +116 -19
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -67,7 +67,10 @@ every Conductor window, nothing to commit), the wake-up hook so an agent finishe
|
|
|
67
67
|
waiting for, the **worker** so the machine answers the team even while its human is away (on by
|
|
68
68
|
default when run inside a git repo; `--no-worker` opts out — and a peer still needs an explicit
|
|
69
69
|
`--may-wake` grant before they can use it), native decision alerts, and the menu bar. Each command
|
|
70
|
-
ends with a "You're set" summary
|
|
70
|
+
ends with a "You're set" summary, then a short guided setup on a terminal: whether teammates may wake
|
|
71
|
+
your machine while you are away, what a woken run may execute, who you are and which decisions are
|
|
72
|
+
yours alone (so agents stop and ask you), and which alerts you want. Plain questions, mostly options;
|
|
73
|
+
`pantheon setup` re-runs it any time. Restart your agent once and ask it *"check pantheon_status"*.
|
|
71
74
|
|
|
72
75
|
Already a member — new laptop, or just pulled a newer build? The same full setup is one command:
|
|
73
76
|
`pantheon install --user` (from your project's repo).
|
|
@@ -89,6 +92,15 @@ pantheon: that invite was already used by "alex" — ask for a fresh one
|
|
|
89
92
|
|
|
90
93
|
Need another (a third person, or your own second machine)? `pantheon invite`.
|
|
91
94
|
|
|
95
|
+
### Plans on the hosted relay
|
|
96
|
+
|
|
97
|
+
A free account creates **one space** with **two people** in it — you and one invite. Personas,
|
|
98
|
+
messages and history are not limited. Joining spaces other people created is not limited either,
|
|
99
|
+
so a colleague on a bigger plan can always invite you. When you hit a limit the command says so
|
|
100
|
+
and what to do; `pantheon status` shows your plan and how much of it you are using. Need more?
|
|
101
|
+
[joinpantheon.network](https://www.joinpantheon.network). A space you founded and no longer need
|
|
102
|
+
can be dissolved with `pantheon space delete <team> --yes`, which frees the slot.
|
|
103
|
+
|
|
92
104
|
### Reaching each other across cities
|
|
93
105
|
|
|
94
106
|
On the hosted relay there is nothing to work out: every signed-in machine dials
|
|
@@ -107,6 +119,28 @@ That address only works on this machine.
|
|
|
107
119
|
The relay should outlive your editor — if it sits on your laptop and you close the lid, your
|
|
108
120
|
teammate's delegate goes offline too. `pantheon start --relay wss://your-host` points at a hosted one.
|
|
109
121
|
|
|
122
|
+
### On more than one team
|
|
123
|
+
|
|
124
|
+
One laptop can be in several spaces at once. `pantheon join` adds a team instead of replacing
|
|
125
|
+
the one you had, and every team keeps its own token, so switching never needs a fresh invite.
|
|
126
|
+
What decides which team an agent speaks in is **the repo it is open in**: `start` and `join`
|
|
127
|
+
bind the checkout they were run from to that team, and an agent opened there acts in it. An
|
|
128
|
+
agent opened in a folder bound to no team refuses to send rather than guess.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pantheon spaces # your teams, which repo is bound to which, where this folder points
|
|
132
|
+
pantheon use mobile --here # bind this repo to a team (and register the repo with it)
|
|
133
|
+
pantheon repo list # the repos a team works from — a delivery must come from one of them
|
|
134
|
+
pantheon repo add --base main # the branch workers cut from and deliveries are judged against (default: origin's HEAD)
|
|
135
|
+
pantheon leave mobile # drop one team from this machine
|
|
136
|
+
pantheon profile set --space mobile --human-required pricing # authority that applies in one team only
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The relay checks too: a session whose checkout is not one of the team's repos is refused, and a
|
|
140
|
+
`DELIVERED` must name a commit in one of them, so work from the wrong repo cannot land in a team.
|
|
141
|
+
The worker serves every bound repo from one process, one run at a time, on `pantheon/<team>/<goal>`
|
|
142
|
+
branches. Details and the scenario list: `docs/MULTI-SPACE.md`.
|
|
143
|
+
|
|
110
144
|
### If something is wrong
|
|
111
145
|
|
|
112
146
|
```bash
|
|
@@ -416,6 +450,7 @@ pantheon watch live tail of everything the delegates say to
|
|
|
416
450
|
pantheon doctor preflight: node, keys, MCP entry, relay, membership
|
|
417
451
|
pantheon worker / service stay answerable while you are away (install as a service)
|
|
418
452
|
pantheon persona list / init / show — what a woken worker may do
|
|
453
|
+
pantheon spaces / use / repo several teams on one machine — what is bound where; bind this repo; a team's repos
|
|
419
454
|
pantheon sessions · logout your devices and sessions; sign this machine out
|
|
420
455
|
pantheon account delete remove your account and everything the relay holds on you
|
|
421
456
|
pantheon upgrade pull the latest release and re-wire your agents
|
|
@@ -448,6 +483,8 @@ verified email, username, and device public keys; the IPs and timestamps of your
|
|
|
448
483
|
an audit trail of identity and membership events; and, per space, the acts your delegates
|
|
449
484
|
exchanged plus open goals and obligations. Your code, credentials, and repo never leave your
|
|
450
485
|
machine — the relay has no filesystem or shell access by design.
|
|
486
|
+
Whoever operates the relay can read those acts — the operator dashboard shows each space's
|
|
487
|
+
message history — so treat a space like a shared team channel, not a private notebook.
|
|
451
488
|
|
|
452
489
|
- `pantheon sessions` shows every device and session on your account; `revoke` signs one out.
|
|
453
490
|
- `pantheon audit` is your own trail; `pantheon account delete` removes your account, devices,
|
package/dist/accounts.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_PLANS, parsePlans, parseLimits, planLimits } from "./plans.js";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
3
|
import { createHash, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
|
|
3
4
|
import { chmodSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
@@ -99,7 +100,29 @@ export class Accounts {
|
|
|
99
100
|
CREATE INDEX IF NOT EXISTS idx_sessions_account ON sessions(account_id);
|
|
100
101
|
CREATE INDEX IF NOT EXISTS idx_audit_ts ON audit(ts);
|
|
101
102
|
CREATE INDEX IF NOT EXISTS idx_audit_space ON audit(space);
|
|
103
|
+
CREATE TABLE IF NOT EXISTS blocks (
|
|
104
|
+
pattern TEXT PRIMARY KEY,
|
|
105
|
+
kind TEXT NOT NULL,
|
|
106
|
+
created_at INTEGER NOT NULL
|
|
107
|
+
);
|
|
108
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
109
|
+
key TEXT PRIMARY KEY,
|
|
110
|
+
json TEXT NOT NULL,
|
|
111
|
+
updated_at INTEGER NOT NULL
|
|
112
|
+
);
|
|
113
|
+
CREATE TABLE IF NOT EXISTS spaces (
|
|
114
|
+
name TEXT PRIMARY KEY,
|
|
115
|
+
owner_account TEXT REFERENCES accounts(id) ON DELETE SET NULL,
|
|
116
|
+
created_at INTEGER NOT NULL,
|
|
117
|
+
deleted_at INTEGER
|
|
118
|
+
);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_spaces_owner ON spaces(owner_account);
|
|
102
120
|
`);
|
|
121
|
+
const cols = new Set(this.db.prepare("PRAGMA table_info(accounts)").all().map((c) => c.name));
|
|
122
|
+
if (!cols.has("plan"))
|
|
123
|
+
this.db.exec("ALTER TABLE accounts ADD COLUMN plan TEXT NOT NULL DEFAULT 'free'");
|
|
124
|
+
if (!cols.has("limits_json"))
|
|
125
|
+
this.db.exec("ALTER TABLE accounts ADD COLUMN limits_json TEXT");
|
|
103
126
|
}
|
|
104
127
|
close() { this.db.close(); }
|
|
105
128
|
accountByUsername(username) {
|
|
@@ -119,6 +142,8 @@ export class Accounts {
|
|
|
119
142
|
if (!device)
|
|
120
143
|
return { ok: false, reason: "no_device" };
|
|
121
144
|
const account = this.accountById(device.account_id);
|
|
145
|
+
if (account && account.status === "suspended")
|
|
146
|
+
return { ok: false, reason: "suspended" };
|
|
122
147
|
if (!account || account.status !== "active")
|
|
123
148
|
return { ok: false, reason: "no_account" };
|
|
124
149
|
if (account.username !== normalizeUsername(username))
|
|
@@ -129,6 +154,8 @@ export class Accounts {
|
|
|
129
154
|
const e = normalizeEmail(email);
|
|
130
155
|
if (!validEmail(e) || e.length > 254)
|
|
131
156
|
return { ok: false, reason: "bad_email" };
|
|
157
|
+
if (this.isBlocked(e))
|
|
158
|
+
return { ok: false, reason: "blocked" };
|
|
132
159
|
if (purpose === "signup") {
|
|
133
160
|
const u = normalizeUsername(opts.username ?? "");
|
|
134
161
|
if (!validUsername(u))
|
|
@@ -192,7 +219,7 @@ export class Accounts {
|
|
|
192
219
|
else {
|
|
193
220
|
const id = newId("acct");
|
|
194
221
|
run("INSERT INTO accounts (id, email, username, status, created_at, tos_at) VALUES (?, ?, ?, 'active', ?, ?)").run(id, e, v.pending_username, now(), now());
|
|
195
|
-
account = { id, email: e, username: v.pending_username, status: "active", created_at: now(), tos_at: now() };
|
|
222
|
+
account = { id, email: e, username: v.pending_username, status: "active", created_at: now(), tos_at: now(), plan: "free", limits_json: null };
|
|
196
223
|
}
|
|
197
224
|
}
|
|
198
225
|
else {
|
|
@@ -279,6 +306,145 @@ export class Accounts {
|
|
|
279
306
|
this.db.prepare("INSERT INTO audit (id, ts, actor_account, event, space, target, detail, ip) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
280
307
|
.run(newId("aud"), now(), row.actor_account ?? null, row.event, row.space ?? null, row.target ?? null, row.detail ?? null, row.ip ?? null);
|
|
281
308
|
}
|
|
309
|
+
setStatus(accountId, status, ip = "") {
|
|
310
|
+
const acct = this.accountById(accountId);
|
|
311
|
+
if (!acct)
|
|
312
|
+
return false;
|
|
313
|
+
this.db.prepare("UPDATE accounts SET status = ? WHERE id = ?").run(status, accountId);
|
|
314
|
+
if (status === "suspended")
|
|
315
|
+
this.db.prepare("UPDATE sessions SET revoked_at = ? WHERE account_id = ? AND revoked_at IS NULL").run(now(), accountId);
|
|
316
|
+
this.audit({ event: status === "suspended" ? "admin_account_suspended" : "admin_account_reinstated", target: acct.username, ip });
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
adminRevoke(kind, id, ip = "") {
|
|
320
|
+
if (kind === "session") {
|
|
321
|
+
const s = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
|
|
322
|
+
if (!s || s.revoked_at)
|
|
323
|
+
return { sessionIds: [], deviceId: null, accountId: null };
|
|
324
|
+
this.db.prepare("UPDATE sessions SET revoked_at = ? WHERE id = ?").run(now(), id);
|
|
325
|
+
this.audit({ event: "admin_session_revoked", target: id, ip });
|
|
326
|
+
return { sessionIds: [id], deviceId: null, accountId: s.account_id };
|
|
327
|
+
}
|
|
328
|
+
const d = this.db.prepare("SELECT * FROM devices WHERE id = ?").get(id);
|
|
329
|
+
if (!d)
|
|
330
|
+
return { sessionIds: [], deviceId: null, accountId: null };
|
|
331
|
+
const open = this.db.prepare("SELECT id FROM sessions WHERE device_id = ? AND revoked_at IS NULL").all(id);
|
|
332
|
+
this.db.prepare("UPDATE sessions SET revoked_at = ? WHERE device_id = ? AND revoked_at IS NULL").run(now(), id);
|
|
333
|
+
this.db.prepare("DELETE FROM devices WHERE id = ?").run(id);
|
|
334
|
+
this.audit({ event: "admin_device_removed", target: d.fingerprint, ip });
|
|
335
|
+
return { sessionIds: open.map((r) => r.id), deviceId: id, accountId: d.account_id };
|
|
336
|
+
}
|
|
337
|
+
block(pattern, ip = "") {
|
|
338
|
+
const p = pattern.toLowerCase().trim();
|
|
339
|
+
const kind = p.includes("@") ? "email" : "domain";
|
|
340
|
+
this.db.prepare("INSERT OR REPLACE INTO blocks (pattern, kind, created_at) VALUES (?, ?, ?)").run(p, kind, now());
|
|
341
|
+
this.audit({ event: "admin_blocked", target: p, detail: kind, ip });
|
|
342
|
+
}
|
|
343
|
+
unblock(pattern, ip = "") {
|
|
344
|
+
const p = pattern.toLowerCase().trim();
|
|
345
|
+
const r = this.db.prepare("DELETE FROM blocks WHERE pattern = ?").run(p);
|
|
346
|
+
if (Number(r.changes) > 0)
|
|
347
|
+
this.audit({ event: "admin_unblocked", target: p, ip });
|
|
348
|
+
}
|
|
349
|
+
isBlocked(email) {
|
|
350
|
+
const e = normalizeEmail(email);
|
|
351
|
+
const domain = e.split("@")[1] ?? "";
|
|
352
|
+
const row = this.db.prepare("SELECT 1 x FROM blocks WHERE pattern = ? OR pattern = ? LIMIT 1").get(e, domain);
|
|
353
|
+
return !!row;
|
|
354
|
+
}
|
|
355
|
+
allBlocks() {
|
|
356
|
+
return this.db.prepare("SELECT * FROM blocks ORDER BY created_at DESC").all();
|
|
357
|
+
}
|
|
358
|
+
recentVerifications(limit = 50) {
|
|
359
|
+
return this.db.prepare("SELECT email, purpose, created_at, expires_at, attempts FROM verifications ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
360
|
+
}
|
|
361
|
+
plans() {
|
|
362
|
+
const row = this.db.prepare("SELECT json FROM settings WHERE key = 'plans'").get();
|
|
363
|
+
if (row) {
|
|
364
|
+
try {
|
|
365
|
+
const p = parsePlans(JSON.parse(row.json));
|
|
366
|
+
if (p)
|
|
367
|
+
return p;
|
|
368
|
+
}
|
|
369
|
+
catch { }
|
|
370
|
+
}
|
|
371
|
+
if (process.env.PANTHEON_PLANS_JSON) {
|
|
372
|
+
try {
|
|
373
|
+
const p = parsePlans(JSON.parse(process.env.PANTHEON_PLANS_JSON));
|
|
374
|
+
if (p)
|
|
375
|
+
return p;
|
|
376
|
+
}
|
|
377
|
+
catch { }
|
|
378
|
+
}
|
|
379
|
+
return structuredClone(DEFAULT_PLANS);
|
|
380
|
+
}
|
|
381
|
+
setPlans(plans) {
|
|
382
|
+
if (!parsePlans(plans))
|
|
383
|
+
throw new Error("malformed plan table");
|
|
384
|
+
this.db.prepare("INSERT INTO settings (key, json, updated_at) VALUES ('plans', ?, ?) ON CONFLICT(key) DO UPDATE SET json = excluded.json, updated_at = excluded.updated_at")
|
|
385
|
+
.run(JSON.stringify(plans), now());
|
|
386
|
+
}
|
|
387
|
+
setPlan(accountId, plan) {
|
|
388
|
+
if (!this.plans()[plan])
|
|
389
|
+
return false;
|
|
390
|
+
return this.db.prepare("UPDATE accounts SET plan = ? WHERE id = ?").run(plan, accountId).changes > 0;
|
|
391
|
+
}
|
|
392
|
+
setLimits(accountId, json) {
|
|
393
|
+
if (json !== null) {
|
|
394
|
+
let ok = false;
|
|
395
|
+
try {
|
|
396
|
+
ok = !!parseLimits(JSON.parse(json));
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
ok = false;
|
|
400
|
+
}
|
|
401
|
+
if (!ok)
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
return this.db.prepare("UPDATE accounts SET limits_json = ? WHERE id = ?").run(json, accountId).changes > 0;
|
|
405
|
+
}
|
|
406
|
+
limitsFor(acct) { return planLimits(this.plans(), acct.plan, acct.limits_json); }
|
|
407
|
+
planCounts() {
|
|
408
|
+
const out = {};
|
|
409
|
+
for (const r of this.db.prepare("SELECT plan, COUNT(*) c FROM accounts GROUP BY plan").all())
|
|
410
|
+
out[r.plan] = r.c;
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
planLimitHits() {
|
|
414
|
+
const m = new Map();
|
|
415
|
+
for (const r of this.db.prepare("SELECT actor_account a, COUNT(*) c FROM audit WHERE event = 'plan_limit' AND actor_account IS NOT NULL GROUP BY actor_account").all())
|
|
416
|
+
m.set(r.a, r.c);
|
|
417
|
+
return m;
|
|
418
|
+
}
|
|
419
|
+
recordSpace(name, ownerAccount) {
|
|
420
|
+
this.db.prepare("INSERT INTO spaces (name, owner_account, created_at, deleted_at) VALUES (?, ?, ?, NULL) " +
|
|
421
|
+
"ON CONFLICT(name) DO UPDATE SET owner_account = excluded.owner_account, created_at = excluded.created_at, deleted_at = NULL WHERE spaces.deleted_at IS NOT NULL").run(name, ownerAccount, now());
|
|
422
|
+
}
|
|
423
|
+
markSpaceDeleted(name) {
|
|
424
|
+
this.db.prepare("UPDATE spaces SET deleted_at = ? WHERE name = ? AND deleted_at IS NULL").run(now(), name);
|
|
425
|
+
}
|
|
426
|
+
spaceOwner(name) {
|
|
427
|
+
const r = this.db.prepare("SELECT owner_account FROM spaces WHERE name = ? AND deleted_at IS NULL").get(name);
|
|
428
|
+
return r?.owner_account ? this.accountById(r.owner_account) : null;
|
|
429
|
+
}
|
|
430
|
+
spacesOwned(accountId) {
|
|
431
|
+
return this.db.prepare("SELECT name FROM spaces WHERE owner_account = ? AND deleted_at IS NULL ORDER BY created_at").all(accountId).map((r) => r.name);
|
|
432
|
+
}
|
|
433
|
+
spaceOwners() {
|
|
434
|
+
const m = new Map();
|
|
435
|
+
for (const r of this.db.prepare("SELECT name, owner_account FROM spaces WHERE deleted_at IS NULL AND owner_account IS NOT NULL").all())
|
|
436
|
+
m.set(r.name, r.owner_account);
|
|
437
|
+
return m;
|
|
438
|
+
}
|
|
439
|
+
ownedCounts() {
|
|
440
|
+
const m = new Map();
|
|
441
|
+
for (const r of this.db.prepare("SELECT owner_account a, COUNT(*) c FROM spaces WHERE deleted_at IS NULL AND owner_account IS NOT NULL GROUP BY owner_account").all())
|
|
442
|
+
m.set(r.a, r.c);
|
|
443
|
+
return m;
|
|
444
|
+
}
|
|
445
|
+
hasSpaceRecord(name) {
|
|
446
|
+
return !!this.db.prepare("SELECT 1 FROM spaces WHERE name = ?").get(name);
|
|
447
|
+
}
|
|
282
448
|
allAccounts() {
|
|
283
449
|
return this.db.prepare("SELECT * FROM accounts ORDER BY created_at DESC").all();
|
|
284
450
|
}
|
|
@@ -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,12 +93,18 @@ export function headlessSpawn(opts) {
|
|
|
93
93
|
PANTHEON_AGENT: opts.backend.id,
|
|
94
94
|
PANTHEON_ROLE: "worker",
|
|
95
95
|
PANTHEON_BRANCH: opts.branch,
|
|
96
|
+
...(opts.base ? { PANTHEON_BASE: opts.base } : {}),
|
|
97
|
+
...(opts.goal ? { PANTHEON_GOAL: opts.goal } : {}),
|
|
98
|
+
...(opts.cfg.repo ? { PANTHEON_REPO: opts.cfg.repo } : {}),
|
|
99
|
+
...(opts.cfg.spaceId ? { PANTHEON_SPACE_ID: opts.cfg.spaceId } : {}),
|
|
96
100
|
};
|
|
97
101
|
const home = opts.runHome ?? process.env.PANTHEON_HOME;
|
|
98
102
|
const fileEnv = {
|
|
99
103
|
PANTHEON_AGENT: opts.backend.id,
|
|
100
104
|
PANTHEON_ROLE: "worker",
|
|
101
105
|
PANTHEON_BRANCH: opts.branch,
|
|
106
|
+
...(opts.base ? { PANTHEON_BASE: opts.base } : {}),
|
|
107
|
+
...(opts.goal ? { PANTHEON_GOAL: opts.goal } : {}),
|
|
102
108
|
...(home ? { PANTHEON_HOME: home } : {}),
|
|
103
109
|
};
|
|
104
110
|
if (opts.backend.id === "claude-code") {
|