@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/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 due = dueHumanObligations(client.obligations, this.cfg.member, state, now(), this.opts.reminderMs);
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", "ESCALATE"]);
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 Inbound = z.discriminatedUnion("t", [Hello, SendAct, OpenGoal, Claim, Admit, MakeInvite, PublishProfile, BindRepo, Ping]);
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;
@@ -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(true, opts.ttlMinutes);
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
  }