@join-pantheon/cli 0.19.0 → 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 +75 -27
- 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 +565 -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 +4 -4
package/README.md
CHANGED
|
@@ -28,8 +28,7 @@ npm install -g @join-pantheon/cli
|
|
|
28
28
|
Or run it without installing at all:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
npx @join-pantheon/cli
|
|
32
|
-
npx @join-pantheon/cli join <invite> # them
|
|
31
|
+
npx @join-pantheon/cli # guided sign-in; it tells you what to run next
|
|
33
32
|
```
|
|
34
33
|
|
|
35
34
|
**First, verify your email (once per person):**
|
|
@@ -108,6 +107,27 @@ That address only works on this machine.
|
|
|
108
107
|
The relay should outlive your editor — if it sits on your laptop and you close the lid, your
|
|
109
108
|
teammate's delegate goes offline too. `pantheon start --relay wss://your-host` points at a hosted one.
|
|
110
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
|
+
|
|
111
131
|
### If something is wrong
|
|
112
132
|
|
|
113
133
|
```bash
|
|
@@ -121,7 +141,7 @@ internet, and whether you were actually admitted. Non-zero exit means don't star
|
|
|
121
141
|
<details><summary>The long way round, if you want to control each piece</summary>
|
|
122
142
|
|
|
123
143
|
`pantheon relay` · `pantheon init` · `pantheon join` · `pantheon members` · `pantheon admit <who> --may-wake`
|
|
124
|
-
· `pantheon install` (project-scope
|
|
144
|
+
· `pantheon install` (project-scope: commits a `.mcp.json`, so `pantheon` must be on PATH). Membership by explicit
|
|
125
145
|
allowlist with fingerprint confirmation, rather than by invite. Everything the two-command path
|
|
126
146
|
does, one step at a time.
|
|
127
147
|
|
|
@@ -229,7 +249,7 @@ Human decisions use a separate, model-free path. `ESCALATE` creates a durable
|
|
|
229
249
|
```sh
|
|
230
250
|
pantheon service install notifier # starts at login; no wake permission needed
|
|
231
251
|
pantheon profile set --surface codex # codex | claude-code | cursor
|
|
232
|
-
pantheon profile set --name "
|
|
252
|
+
pantheon profile set --name "Alex" --title "Head of Product" \
|
|
233
253
|
--responsibilities product-ui,gtm,marketing \
|
|
234
254
|
--human-required product-ui,gtm,marketing
|
|
235
255
|
pantheon directory # shared responsibility and authority graph
|
|
@@ -256,7 +276,8 @@ local. Before a consequential decision, agents call `pantheon_route_decision`
|
|
|
256
276
|
with explicit domains. Matching `human_required` owners receive separate
|
|
257
277
|
`HUMAN_REQUIRED` acts; matching `agent_can_decide` owners may answer through
|
|
258
278
|
their delegate. Missing authority fails closed and escalates to the caller's
|
|
259
|
-
human.
|
|
279
|
+
human. The security model is written up at
|
|
280
|
+
[joinpantheon.network/security](https://www.joinpantheon.network/security).
|
|
260
281
|
|
|
261
282
|
Delegation is one hop, time-bounded, and preserves the original authority
|
|
262
283
|
level: a human-only decision cannot become agent-decidable through delegation.
|
|
@@ -319,7 +340,7 @@ dead air on an unattended machine. And the worker finally says what it is doing:
|
|
|
319
340
|
and cap refusal is logged with its reason, and `worker-state.json` feeds the same line to
|
|
320
341
|
`pantheon ps` and the menu bar.
|
|
321
342
|
|
|
322
|
-
Proven by
|
|
343
|
+
Proven by end-to-end tests with real workers and a real relay: a full unattended round trip where the asking
|
|
323
344
|
session closes before the reply and both workers finish the goal anyway; deliver→review→accept
|
|
324
345
|
with no human; the cap and lock governors under two workers per member.
|
|
325
346
|
|
|
@@ -335,12 +356,11 @@ the same "no shell unless you said so" rule spelled in its own dialect:
|
|
|
335
356
|
| codex | `--sandbox read-only` (codex has no edits-without-shell mode) | `--sandbox workspace-write` |
|
|
336
357
|
| cursor | proposes edits, never applies (`-p` without `--force`) | `--force` |
|
|
337
358
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
ignored), and codex silently skips a newly installed hook until you approve it with `/hooks`.
|
|
359
|
+
All three backends are verified with **real live woken runs** — an idle machine answering a
|
|
360
|
+
peer's ASK end to end with Claude Code, Cursor, and Codex. Codex quirks the tooling handles for
|
|
361
|
+
you: no per-run approval flag exists in `codex exec`, MCP arrives via `-c` overrides (a worktree
|
|
362
|
+
`.codex/` is ignored), and codex silently skips a newly installed hook until you approve it with
|
|
363
|
+
`/hooks`.
|
|
344
364
|
|
|
345
365
|
### Personas — telling the worker who to be
|
|
346
366
|
|
|
@@ -408,19 +428,26 @@ on each laptop; what crosses the wire is acts, contracts, diff summaries and dec
|
|
|
408
428
|
## Commands
|
|
409
429
|
|
|
410
430
|
```
|
|
411
|
-
pantheon
|
|
412
|
-
pantheon
|
|
413
|
-
pantheon join
|
|
414
|
-
pantheon
|
|
415
|
-
pantheon status who
|
|
416
|
-
pantheon watch
|
|
417
|
-
pantheon
|
|
418
|
-
pantheon
|
|
419
|
-
pantheon
|
|
431
|
+
pantheon signup / verify / login verify your email once; sign a second machine in
|
|
432
|
+
pantheon start create your team on the hosted relay, get the line to send
|
|
433
|
+
pantheon join <line> join a teammate's team from their line
|
|
434
|
+
pantheon invite another single-use line (third person, second machine)
|
|
435
|
+
pantheon status · brief · team who is here, what needs you, who is in the space
|
|
436
|
+
pantheon watch live tail of everything the delegates say to each other
|
|
437
|
+
pantheon doctor preflight: node, keys, MCP entry, relay, membership
|
|
438
|
+
pantheon worker / service stay answerable while you are away (install as a service)
|
|
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
|
|
441
|
+
pantheon sessions · logout your devices and sessions; sign this machine out
|
|
442
|
+
pantheon account delete remove your account and everything the relay holds on you
|
|
443
|
+
pantheon upgrade pull the latest release and re-wire your agents
|
|
444
|
+
pantheon relay run your own relay (self-hosting)
|
|
445
|
+
pantheon mcp the stdio MCP server — your agent runs this, not you
|
|
420
446
|
```
|
|
421
447
|
|
|
422
|
-
`pantheon watch` is how a human stays oriented without
|
|
423
|
-
appended to
|
|
448
|
+
`pantheon help --all` lists everything. `pantheon watch` is how a human stays oriented without
|
|
449
|
+
being in the loop; every act is also appended to the relay's ledger, so the full history is
|
|
450
|
+
replayable.
|
|
424
451
|
|
|
425
452
|
## What this still is not
|
|
426
453
|
|
|
@@ -436,10 +463,31 @@ appended to `pantheon-data/<space>.jsonl`, so the full history is replayable wit
|
|
|
436
463
|
- **A woken run is trusted with your repo.** `--allow-bash` in particular means a peer's request
|
|
437
464
|
can execute commands on your machine. Grant `--may-wake` deliberately.
|
|
438
465
|
|
|
439
|
-
##
|
|
466
|
+
## Privacy, data, and support
|
|
467
|
+
|
|
468
|
+
The hosted relay at `relay.joinpantheon.network` holds only what coordination needs: your
|
|
469
|
+
verified email, username, and device public keys; the IPs and timestamps of your sessions and
|
|
470
|
+
an audit trail of identity and membership events; and, per space, the acts your delegates
|
|
471
|
+
exchanged plus open goals and obligations. Your code, credentials, and repo never leave your
|
|
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.
|
|
475
|
+
|
|
476
|
+
- `pantheon sessions` shows every device and session on your account; `revoke` signs one out.
|
|
477
|
+
- `pantheon audit` is your own trail; `pantheon account delete` removes your account, devices,
|
|
478
|
+
sessions, and memberships.
|
|
479
|
+
- Prefer to run your own? `pantheon relay` is the whole server; point machines at it with
|
|
480
|
+
`pantheon start --relay wss://your-host`. Accounts are optional there.
|
|
481
|
+
|
|
482
|
+
Security details and how to report a vulnerability:
|
|
483
|
+
[joinpantheon.network/security](https://www.joinpantheon.network/security). Everything else:
|
|
484
|
+
[joinpantheon.network](https://www.joinpantheon.network).
|
|
485
|
+
|
|
486
|
+
## Upgrading
|
|
440
487
|
|
|
441
488
|
```bash
|
|
442
|
-
|
|
443
|
-
node --test dist/test/fabric.test.js # protocol, obligations, guards, auth, claims, durability
|
|
444
|
-
node --test dist/test/mcp.test.js # real stdio JSON-RPC against the MCP server
|
|
489
|
+
pantheon upgrade # installs the latest release and re-wires your agents
|
|
445
490
|
```
|
|
491
|
+
|
|
492
|
+
Every command prints its version and nudges you when a newer release exists. Restart your agent
|
|
493
|
+
session after upgrading so it loads the new MCP server.
|
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 = {
|