@join-pantheon/cli 0.20.0 → 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 +14 -1
- package/dist/accounts.js +106 -1
- package/dist/agents.js +4 -0
- package/dist/cli.js +85 -10
- package/dist/client.js +23 -8
- package/dist/config.js +14 -0
- package/dist/doctor.js +9 -0
- package/dist/mcp.js +43 -9
- package/dist/notifier.js +26 -6
- package/dist/notify.js +1 -1
- package/dist/pending.js +3 -1
- package/dist/plans.js +46 -0
- package/dist/profile.js +5 -0
- package/dist/protocol.js +13 -1
- package/dist/quickstart.js +2 -1
- package/dist/relay.js +219 -21
- package/dist/roster-file.js +15 -2
- package/dist/setup-guide.js +238 -0
- package/dist/untrusted.js +16 -3
- package/dist/worker.js +78 -13
- package/package.json +2 -2
package/dist/notifier.js
CHANGED
|
@@ -17,10 +17,10 @@ export function dueHumanObligations(obligations, member, state, now, reminderMs)
|
|
|
17
17
|
return last === undefined || now - last >= reminderMs;
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
-
export function pruneNotificationState(state, obligations, member) {
|
|
20
|
+
export function pruneNotificationState(state, obligations, member, now = Date.now()) {
|
|
21
21
|
const active = new Set(humanObligations(obligations, member).map((o) => o.id));
|
|
22
22
|
return {
|
|
23
|
-
notified_at: Object.fromEntries(Object.entries(state.notified_at).filter(([id]) => active.has(id))),
|
|
23
|
+
notified_at: Object.fromEntries(Object.entries(state.notified_at).filter(([id, at]) => active.has(id) || (id.startsWith("act:") && now - at < 7 * 86_400_000))),
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
function readState(file) {
|
|
@@ -108,12 +108,15 @@ export class HumanNotifier {
|
|
|
108
108
|
this.opts.log(`notifier online as ${this.cfg.member}`);
|
|
109
109
|
this.opts.log(` reminders every ${Math.round(this.opts.reminderMs / 60_000)} minutes while unresolved`);
|
|
110
110
|
while (!this.stopped) {
|
|
111
|
-
const pruned = pruneNotificationState(state, client.obligations, this.cfg.member);
|
|
111
|
+
const pruned = pruneNotificationState(state, client.obligations, this.cfg.member, now());
|
|
112
112
|
let dirty = JSON.stringify(pruned) !== JSON.stringify(state);
|
|
113
113
|
state = pruned;
|
|
114
|
-
const
|
|
114
|
+
const profile = loadProfile(this.cfg);
|
|
115
|
+
const prefs = profile.notifications ?? { decisions: true, deliveries: false, worker_done: false };
|
|
116
|
+
const due = prefs.decisions
|
|
117
|
+
? dueHumanObligations(client.obligations, this.cfg.member, state, now(), this.opts.reminderMs)
|
|
118
|
+
: [];
|
|
115
119
|
for (const obligation of due) {
|
|
116
|
-
const profile = loadProfile(this.cfg);
|
|
117
120
|
const goal = obligation.goal ? client.goals.get(obligation.goal)?.title : undefined;
|
|
118
121
|
const ok = send("Your input is needed", oneLine(`${this.opts.label ? `${this.opts.label} · ` : ""}${goal ?? "Pantheon decision"}`, 80), decisionTitle(obligation.body, 180), profile, decisionPrompt(obligation.id));
|
|
119
122
|
if (ok) {
|
|
@@ -127,7 +130,24 @@ export class HumanNotifier {
|
|
|
127
130
|
}
|
|
128
131
|
if (dirty || !existsSync(stateFile))
|
|
129
132
|
writeState(stateFile, state);
|
|
130
|
-
await client.next(pollMs);
|
|
133
|
+
const arrived = await client.next(pollMs);
|
|
134
|
+
if (prefs.deliveries) {
|
|
135
|
+
for (const r of arrived) {
|
|
136
|
+
if (r.act !== "DELIVERED" || r.from === this.cfg.member || state.notified_at[`act:${r.id}`])
|
|
137
|
+
continue;
|
|
138
|
+
const goal = r.goal ? client.goals.get(r.goal) : undefined;
|
|
139
|
+
const forMe = (r.to?.includes(this.cfg.member) ?? false) || (goal?.members.includes(this.cfg.member) ?? false);
|
|
140
|
+
if (!forMe)
|
|
141
|
+
continue;
|
|
142
|
+
const m = r.meta;
|
|
143
|
+
const ok = send("Work delivered to you", oneLine(`${this.opts.label ? `${this.opts.label} · ` : ""}${goal?.title ?? "Pantheon delivery"}`, 80), oneLine(`${r.from} pushed ${m?.branch ?? "a branch"}${m?.commit ? ` @ ${m.commit.slice(0, 7)}` : ""} — ${r.body}`, 180), profile, `A teammate delivered work in pantheon: fetch ${m?.branch ?? "their branch"}${m?.commit ? ` at ${m.commit}` : ""} from ${r.from}, check it against the goal "${goal?.title ?? r.goal ?? ""}", then ACCEPT or OBJECT.`, "Delivered");
|
|
144
|
+
if (ok) {
|
|
145
|
+
state.notified_at[`act:${r.id}`] = now();
|
|
146
|
+
writeState(stateFile, state);
|
|
147
|
+
this.opts.log(`> delivery alert for ${r.id} from ${r.from}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
131
151
|
}
|
|
132
152
|
client.close();
|
|
133
153
|
}
|
package/dist/notify.js
CHANGED
|
@@ -290,7 +290,7 @@ export async function decideStop(raw, deps) {
|
|
|
290
290
|
const acts = report.acts ?? [];
|
|
291
291
|
const owed = report.obligations ?? [];
|
|
292
292
|
const closed = new Set(report.closed_goals ?? []);
|
|
293
|
-
const DEMANDS_RESPONSE = new Set(["ASK", "PROPOSE", "OBJECT", "DELIVERED", "CONTRACT"
|
|
293
|
+
const DEMANDS_RESPONSE = new Set(["ASK", "PROPOSE", "OBJECT", "DELIVERED", "CONTRACT"]);
|
|
294
294
|
const work = acts.filter((a) => {
|
|
295
295
|
if (a.goal && closed.has(a.goal))
|
|
296
296
|
return false;
|
package/dist/pending.js
CHANGED
|
@@ -102,10 +102,12 @@ export async function pending(opts) {
|
|
|
102
102
|
const obligations = surfaced.filter((o) => !alreadySeen.has(o.id));
|
|
103
103
|
const head = obligationsBody?.seq;
|
|
104
104
|
const all = (historyBody?.acts ?? []);
|
|
105
|
+
const mine = all.filter((r) => r.from === cfg.member);
|
|
105
106
|
const acts = all
|
|
106
107
|
.filter((r) => r.from !== cfg.member)
|
|
107
108
|
.filter((r) => r.act !== "JOIN" && r.act !== "LEAVE")
|
|
108
|
-
.filter((r) => !r.to || r.to.length === 0 || r.to.includes(cfg.member))
|
|
109
|
+
.filter((r) => !r.to || r.to.length === 0 || r.to.includes(cfg.member))
|
|
110
|
+
.filter((r) => !mine.some((m) => m.seq > r.seq && (m.ref === r.id || (!!r.goal && m.goal === r.goal))));
|
|
109
111
|
const seq = first
|
|
110
112
|
? head
|
|
111
113
|
: historyBody === null
|
package/dist/plans.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const UNLIMITED = 1_000_000;
|
|
2
|
+
export const DEFAULT_PLANS = {
|
|
3
|
+
free: { spaces: 1, members: 2, wake: true },
|
|
4
|
+
pro: { spaces: 10, members: 25, wake: true },
|
|
5
|
+
};
|
|
6
|
+
export const PLAN_NAME = /^[a-z][a-z0-9_-]{0,23}$/;
|
|
7
|
+
const int = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max ? v : null;
|
|
8
|
+
export function parseLimits(raw) {
|
|
9
|
+
if (!raw || typeof raw !== "object")
|
|
10
|
+
return null;
|
|
11
|
+
const o = raw;
|
|
12
|
+
const num = (v) => typeof v === "string" && v.trim() !== "" ? Number(v) : v;
|
|
13
|
+
const spaces = int(num(o["spaces"]), 0, UNLIMITED);
|
|
14
|
+
const members = int(num(o["members"]), 1, UNLIMITED);
|
|
15
|
+
const wake = typeof o["wake"] === "boolean" ? o["wake"] : o["wake"] === "true" || o["wake"] === "on" || o["wake"] === "1" ? true : o["wake"] === "false" || o["wake"] === "off" || o["wake"] === "0" || o["wake"] === undefined ? false : null;
|
|
16
|
+
if (spaces === null || members === null || wake === null)
|
|
17
|
+
return null;
|
|
18
|
+
return { spaces, members, wake };
|
|
19
|
+
}
|
|
20
|
+
export function parsePlans(raw) {
|
|
21
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
22
|
+
return null;
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const [name, lim] of Object.entries(raw)) {
|
|
25
|
+
if (!PLAN_NAME.test(name))
|
|
26
|
+
return null;
|
|
27
|
+
const l = parseLimits(lim);
|
|
28
|
+
if (!l)
|
|
29
|
+
return null;
|
|
30
|
+
out[name] = l;
|
|
31
|
+
}
|
|
32
|
+
return out["free"] ? out : null;
|
|
33
|
+
}
|
|
34
|
+
export function planLimits(plans, plan, override) {
|
|
35
|
+
if (override) {
|
|
36
|
+
try {
|
|
37
|
+
const l = parseLimits(JSON.parse(override));
|
|
38
|
+
if (l)
|
|
39
|
+
return l;
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
return plans[plan] ?? plans["free"] ?? DEFAULT_PLANS["free"];
|
|
44
|
+
}
|
|
45
|
+
export const fmtLimit = (n) => n >= UNLIMITED ? "unlimited" : String(n);
|
|
46
|
+
//# sourceMappingURL=plans.js.map
|
package/dist/profile.js
CHANGED
|
@@ -25,6 +25,7 @@ export function defaultProfile(cfg) {
|
|
|
25
25
|
human: { display_name: cfg.display ?? cfg.member, title: "", responsibilities: [] },
|
|
26
26
|
agent: { primary_surface: surfaceFromAgent(cfg.agent), wake_enabled: false },
|
|
27
27
|
attention: { urgent: "primary_surface", decision: "primary_surface", fyi: "inbox" },
|
|
28
|
+
notifications: { decisions: true, deliveries: false, worker_done: false },
|
|
28
29
|
authority: {},
|
|
29
30
|
decision_keywords: {},
|
|
30
31
|
capabilities: [],
|
|
@@ -32,6 +33,9 @@ export function defaultProfile(cfg) {
|
|
|
32
33
|
updated_at: 0,
|
|
33
34
|
};
|
|
34
35
|
}
|
|
36
|
+
export function wakeDefault(profile) {
|
|
37
|
+
return (profile.agent.wake_default ?? "anyone") === "anyone";
|
|
38
|
+
}
|
|
35
39
|
export function loadProfile(cfg, file = profilePath()) {
|
|
36
40
|
if (!existsSync(file))
|
|
37
41
|
return defaultProfile(cfg);
|
|
@@ -53,6 +57,7 @@ export function loadProfile(cfg, file = profilePath()) {
|
|
|
53
57
|
},
|
|
54
58
|
agent: { ...defaultProfile(cfg).agent, ...raw.agent, primary_surface: surface },
|
|
55
59
|
attention: { ...defaultProfile(cfg).attention, ...raw.attention },
|
|
60
|
+
notifications: { ...defaultProfile(cfg).notifications, ...(raw.notifications ?? {}) },
|
|
56
61
|
authority: raw.authority ?? {},
|
|
57
62
|
decision_keywords: cleanKeywords(legacy.decision_keywords),
|
|
58
63
|
capabilities: Array.isArray(raw.capabilities) ? raw.capabilities.filter((x) => typeof x === "string") : [],
|
package/dist/protocol.js
CHANGED
|
@@ -160,6 +160,7 @@ export const BindRepo = z.object({
|
|
|
160
160
|
cid: z.string().max(64).optional(),
|
|
161
161
|
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/),
|
|
162
162
|
origin: z.string().max(200).nullable().optional(),
|
|
163
|
+
base: z.string().min(1).max(100).optional(),
|
|
163
164
|
});
|
|
164
165
|
export const OpenGoal = z.object({
|
|
165
166
|
t: z.literal("open_goal"),
|
|
@@ -171,7 +172,18 @@ export const OpenGoal = z.object({
|
|
|
171
172
|
deadline: z.number().int().positive().optional(),
|
|
172
173
|
});
|
|
173
174
|
export const Ping = z.object({ t: z.literal("ping") });
|
|
174
|
-
export const
|
|
175
|
+
export const DeleteSpace = z.object({
|
|
176
|
+
t: z.literal("delete_space"),
|
|
177
|
+
cid: z.string().max(64).optional(),
|
|
178
|
+
confirm: z.string().min(1).max(64),
|
|
179
|
+
});
|
|
180
|
+
export const WorkerState = z.object({
|
|
181
|
+
t: z.literal("worker_state"),
|
|
182
|
+
state: z.enum(["idle", "holding", "running"]),
|
|
183
|
+
detail: z.string().max(200).optional(),
|
|
184
|
+
until: z.number().int().positive().optional(),
|
|
185
|
+
});
|
|
186
|
+
export const Inbound = z.discriminatedUnion("t", [Hello, SendAct, OpenGoal, Claim, Admit, MakeInvite, PublishProfile, BindRepo, DeleteSpace, WorkerState, Ping]);
|
|
175
187
|
export function shouldDeferToHuman(sessions, work, opts = {}) {
|
|
176
188
|
const now = opts.now ?? Date.now();
|
|
177
189
|
const idleMs = opts.idleMs ?? LIMITS.workerIdleMs;
|
package/dist/quickstart.js
CHANGED
|
@@ -4,6 +4,7 @@ import { basename } from "node:path";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { PantheonClient } from "./client.js";
|
|
6
6
|
import { load, save, toInvite, resolveFor, repoInfo, bindRepo, DEFAULT_RELAY } from "./config.js";
|
|
7
|
+
import { loadProfile, wakeDefault } from "./profile.js";
|
|
7
8
|
import { newKeypair } from "./identity.js";
|
|
8
9
|
import { installForUser } from "./install.js";
|
|
9
10
|
import { writeIntent, installHooks } from "./notify.js";
|
|
@@ -203,7 +204,7 @@ export async function start(opts) {
|
|
|
203
204
|
warnings.push(`this repo was bound to another team (${replaced.space}); it now belongs to "${c.space}"`);
|
|
204
205
|
}
|
|
205
206
|
opts.log(` space ${c.space} ${reuse ? "— reconnected" : "— created, you own it"}${repo ? ` ${repo.origin ?? "local repo"} → bound to it` : ""}`);
|
|
206
|
-
const inv = await client.makeInvite(
|
|
207
|
+
const inv = await client.makeInvite(wakeDefault(loadProfile(c)), opts.ttlMinutes);
|
|
207
208
|
invite = toInvite({ ...c, token: inv.url });
|
|
208
209
|
claimed = true;
|
|
209
210
|
}
|