@join-pantheon/cli 0.22.0 → 0.23.1
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 +5 -0
- package/dist/cli.js +116 -26
- package/dist/client.js +42 -0
- package/dist/config.js +15 -3
- package/dist/doctor.js +13 -0
- package/dist/email.js +47 -23
- package/dist/host-env.js +57 -0
- package/dist/notice.js +157 -0
- package/dist/notifier.js +25 -14
- package/dist/protocol.js +10 -1
- package/dist/quickstart.js +15 -4
- package/dist/relay.js +180 -74
- package/dist/roster-file.js +19 -0
- package/dist/service.js +25 -2
- package/dist/setup-guide.js +4 -2
- package/dist/tray.js +10 -4
- package/dist/worker.js +7 -11
- package/helper/macos/main.swift +231 -65
- package/helper/macos/menubar.swift +110 -43
- package/package.json +2 -2
package/dist/notice.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { decisionPrompt, decisionTitle } from "./decision-ui.js";
|
|
2
|
+
export const TOOL_LABELS = { "claude-code": "Claude Code", codex: "Codex", cursor: "Cursor" };
|
|
3
|
+
export const BUDGET = { who: 60, heading: 40, context: 70, body: 190 };
|
|
4
|
+
const oneLine = (s) => s.replace(/\s+/g, " ").trim();
|
|
5
|
+
export function fit(text, max) {
|
|
6
|
+
const t = oneLine(text);
|
|
7
|
+
if (t.length <= max)
|
|
8
|
+
return t;
|
|
9
|
+
const cut = t.slice(0, max - 1);
|
|
10
|
+
const space = cut.lastIndexOf(" ");
|
|
11
|
+
return `${(space > max * 0.6 ? cut.slice(0, space) : cut).trimEnd()}…`;
|
|
12
|
+
}
|
|
13
|
+
export function enrich(summary, body, max) {
|
|
14
|
+
const head = plain(summary);
|
|
15
|
+
const sentences = plain(body).split(/(?<=[.!?])\s+/).filter(Boolean);
|
|
16
|
+
let out = head;
|
|
17
|
+
for (const s of sentences) {
|
|
18
|
+
if (!out) {
|
|
19
|
+
out = s;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (out.includes(s.slice(0, 40)))
|
|
23
|
+
continue;
|
|
24
|
+
if (out.length + 1 + s.length > max)
|
|
25
|
+
break;
|
|
26
|
+
out = `${out} ${s}`;
|
|
27
|
+
}
|
|
28
|
+
return fit(out || head, max);
|
|
29
|
+
}
|
|
30
|
+
export function relTime(ts, now = Date.now()) {
|
|
31
|
+
const s = Math.max(0, Math.round((now - ts) / 1000));
|
|
32
|
+
if (s < 45)
|
|
33
|
+
return "just now";
|
|
34
|
+
const m = Math.round(s / 60);
|
|
35
|
+
if (m < 60)
|
|
36
|
+
return `${m}m ago`;
|
|
37
|
+
const h = Math.round(m / 60);
|
|
38
|
+
if (h < 24)
|
|
39
|
+
return `${h}h ago`;
|
|
40
|
+
const d = Math.round(h / 24);
|
|
41
|
+
return d === 1 ? "yesterday" : `${d}d ago`;
|
|
42
|
+
}
|
|
43
|
+
export function plain(text) {
|
|
44
|
+
return oneLine(text)
|
|
45
|
+
.replace(/^[a-z0-9._-]+\s+(?:asked|asks|says|said|needs a decision|escalated)\s*:\s*/i, "")
|
|
46
|
+
.replace(/^(?:ASK|ANSWER|PROPOSE|ACCEPT|OBJECT|CONTRACT|COMMIT|DELIVERED|ESCALATE|HUMAN_REQUIRED|NOTE|CLOSE)\s+from\s+[a-z0-9._-]+\s*:\s*/i, "")
|
|
47
|
+
.replace(/^(?:ASK|ANSWER|PROPOSE|ACCEPT|OBJECT|CONTRACT|COMMIT|DELIVERED|ESCALATE|HUMAN_REQUIRED|NOTE|CLOSE)\s*[:—-]\s*/, "")
|
|
48
|
+
.replace(/^(?:human (?:action|input) required)\s*[—:-]?\s*/i, "")
|
|
49
|
+
.replace(/^[a-z0-9._-]+\s+(?:asked|asks)\s*:\s*/i, "");
|
|
50
|
+
}
|
|
51
|
+
const agentLabel = (agent) => {
|
|
52
|
+
if (!agent)
|
|
53
|
+
return null;
|
|
54
|
+
const a = agent.toLowerCase();
|
|
55
|
+
if (a.includes("claude"))
|
|
56
|
+
return "Claude Code";
|
|
57
|
+
if (a.includes("codex"))
|
|
58
|
+
return "Codex";
|
|
59
|
+
if (a.includes("cursor"))
|
|
60
|
+
return "Cursor";
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
function whoLine(from, agent, ts, now) {
|
|
64
|
+
const parts = [from];
|
|
65
|
+
const a = agentLabel(agent);
|
|
66
|
+
if (a)
|
|
67
|
+
parts.push(a);
|
|
68
|
+
if (ts)
|
|
69
|
+
parts.push(relTime(ts, now));
|
|
70
|
+
return fit(parts.join(" · "), BUDGET.who);
|
|
71
|
+
}
|
|
72
|
+
function primaryFor(tools, preferred, prompt) {
|
|
73
|
+
if (!tools.length)
|
|
74
|
+
return undefined;
|
|
75
|
+
const def = tools.find((t) => t.id === preferred) ?? tools[0];
|
|
76
|
+
return { label: `Open in ${def.label}`, default: def.id, tools, prompt };
|
|
77
|
+
}
|
|
78
|
+
export function composeDecision(o, act, goal, peer, ctx) {
|
|
79
|
+
const now = ctx.now ?? Date.now();
|
|
80
|
+
const from = act?.from ?? peer?.member ?? o.body.match(/^([a-z0-9._-]+)\s+(?:needs a decision|escalated)/i)?.[1] ?? "a teammate";
|
|
81
|
+
const body = act
|
|
82
|
+
? enrich(act.summary ?? "", act.body, BUDGET.body) || decisionTitle(plain(o.body), BUDGET.body)
|
|
83
|
+
: decisionTitle(plain(o.body), BUDGET.body);
|
|
84
|
+
return {
|
|
85
|
+
kind: "decision",
|
|
86
|
+
who: whoLine(from, peer?.agent ?? act?.role === "worker" ? peer?.agent : peer?.agent, act?.ts ?? o.created_at, now),
|
|
87
|
+
heading: "Needs your decision",
|
|
88
|
+
context: fit(goal?.title ?? "", BUDGET.context),
|
|
89
|
+
body: fit(body, BUDGET.body),
|
|
90
|
+
ttl: 0,
|
|
91
|
+
sound: true,
|
|
92
|
+
primary: primaryFor(ctx.tools, ctx.preferred, decisionPrompt(o.id)),
|
|
93
|
+
details: { decision: o.id, ...(act ? { act: act.id } : {}) },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export function composeDelivery(act, goal, peer, ctx) {
|
|
97
|
+
const now = ctx.now ?? Date.now();
|
|
98
|
+
const m = (act.meta ?? {});
|
|
99
|
+
const where = m.branch ? `${m.branch}${m.commit ? ` @ ${m.commit.slice(0, 7)}` : ""}` : "a branch";
|
|
100
|
+
const said = enrich(act.summary ?? "", act.body, BUDGET.body - where.length - 4);
|
|
101
|
+
const prompt = `A teammate delivered work in Pantheon: fetch ${m.branch ?? "their branch"}${m.commit ? ` at ${m.commit}` : ""} from ${act.from}, check it against the goal "${goal?.title ?? act.goal ?? ""}", then ACCEPT or OBJECT (act ${act.id}).`;
|
|
102
|
+
return {
|
|
103
|
+
kind: "delivery",
|
|
104
|
+
who: whoLine(act.from, peer?.agent, act.ts, now),
|
|
105
|
+
heading: "Delivered work to you",
|
|
106
|
+
context: fit(goal?.title ?? "", BUDGET.context),
|
|
107
|
+
body: fit(`${said ? `${said} ` : ""}(${where})`, BUDGET.body),
|
|
108
|
+
ttl: 15,
|
|
109
|
+
sound: false,
|
|
110
|
+
primary: primaryFor(ctx.tools, ctx.preferred, prompt),
|
|
111
|
+
details: { act: act.id },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const VERB = {
|
|
115
|
+
ANSWER: "Answered", ACCEPT: "Accepted", OBJECT: "Objected to", DELIVERED: "Delivered on", PROPOSE: "Proposed for", NOTE: "Noted on", ESCALATE: "Escalated", COMMIT: "Committed to",
|
|
116
|
+
};
|
|
117
|
+
const NOUN = { ANSWER: "question", ACCEPT: "delivery", OBJECT: "delivery", DELIVERED: "request", PROPOSE: "request", NOTE: "request", ESCALATE: "request to you", COMMIT: "request" };
|
|
118
|
+
const ASKED = { ASK: "question", PROPOSE: "proposal", DELIVERED: "delivery", CONTRACT: "contract", COMMIT: "commitment", ESCALATE: "escalation", HUMAN_REQUIRED: "decision" };
|
|
119
|
+
function nounFor(obligationBody, replyAct) {
|
|
120
|
+
const m = /^(ASK|PROPOSE|DELIVERED|CONTRACT|COMMIT|ESCALATE|HUMAN_REQUIRED)\s+from\s+/i.exec(obligationBody.trim());
|
|
121
|
+
if (m)
|
|
122
|
+
return ASKED[m[1].toUpperCase()] ?? "request";
|
|
123
|
+
if (/^[a-z0-9._-]+\s+asked\s*:/i.test(obligationBody))
|
|
124
|
+
return "question";
|
|
125
|
+
return (replyAct && NOUN[replyAct]) ?? "request";
|
|
126
|
+
}
|
|
127
|
+
export function composeWorker(run, ctx) {
|
|
128
|
+
const peer = run.triggeredBy && run.triggeredBy !== "unknown" ? run.triggeredBy : "a teammate";
|
|
129
|
+
const asked = plain(run.obligationBody);
|
|
130
|
+
let body;
|
|
131
|
+
if (!run.ok) {
|
|
132
|
+
body = `Could not finish ${peer}'s request: ${asked}. It needs a live session or your decision.`;
|
|
133
|
+
}
|
|
134
|
+
else if (run.reply) {
|
|
135
|
+
const verb = VERB[run.reply.act] ?? "Replied to";
|
|
136
|
+
const noun = nounFor(run.obligationBody, run.reply.act);
|
|
137
|
+
const lead = `${verb} ${peer}'s ${noun}: `;
|
|
138
|
+
const said = enrich(run.reply.summary ?? "", run.reply.body, BUDGET.body - lead.length);
|
|
139
|
+
body = said ? lead + said : lead.replace(/: $/, "");
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
body = `Handled ${peer}'s request: ${asked}`;
|
|
143
|
+
}
|
|
144
|
+
const prompt = `A Pantheon run on this machine could not finish ${peer}'s request${run.goal ? ` in "${run.goal.title}"` : ""}${run.obligationRef ? ` (act ${run.obligationRef})` : ""}. Read the thread, then answer or deliver what is still owed.`;
|
|
145
|
+
return {
|
|
146
|
+
kind: run.ok ? "worker" : "worker_failed",
|
|
147
|
+
who: fit(`Your worker · for ${peer}`, BUDGET.who),
|
|
148
|
+
heading: run.ok ? "Your worker handled it" : "Your worker could not finish",
|
|
149
|
+
context: fit(run.goal?.title ?? "", BUDGET.context),
|
|
150
|
+
body: fit(body, BUDGET.body),
|
|
151
|
+
ttl: run.ok ? 15 : 0,
|
|
152
|
+
sound: !run.ok,
|
|
153
|
+
...(run.ok ? {} : { primary: primaryFor(ctx.tools, ctx.preferred, prompt) }),
|
|
154
|
+
details: { ...(run.reply ? { act: run.reply.id } : run.obligationRef ? { act: run.obligationRef } : {}) },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=notice.js.map
|
package/dist/notifier.js
CHANGED
|
@@ -6,8 +6,9 @@ import { dirname, join, resolve } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { PantheonClient } from "./client.js";
|
|
8
8
|
import { CONFIG_DIR } from "./config.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { loadProfile } from "./profile.js";
|
|
10
|
+
import { composeDecision, composeDelivery, TOOL_LABELS } from "./notice.js";
|
|
11
|
+
import { detectAvailable } from "./agents.js";
|
|
11
12
|
export const DEFAULT_REMINDER_MS = 4 * 60 * 60 * 1_000;
|
|
12
13
|
export function humanObligations(obligations, member) {
|
|
13
14
|
return obligations.filter((o) => o.kind === "HUMAN" && o.owner === `human:${member}`);
|
|
@@ -69,19 +70,26 @@ export function requireDeveloperTools(what, service) {
|
|
|
69
70
|
throw new Error(`building ${what} needs the Xcode Command Line Tools — run \`xcode-select --install\`, then \`pantheon service install ${service}\``);
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
|
-
export function
|
|
73
|
+
export function installedTools() {
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const { backend } of detectAvailable()) {
|
|
77
|
+
const id = backend.id;
|
|
78
|
+
if (!(id in TOOL_LABELS) || seen.has(id))
|
|
79
|
+
continue;
|
|
80
|
+
seen.add(id);
|
|
81
|
+
out.push({ id, label: TOOL_LABELS[id] });
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
export function sendDesktopNotification(notice, home = CONFIG_DIR) {
|
|
73
86
|
if (platform() !== "darwin")
|
|
74
87
|
return false;
|
|
75
|
-
const binary = hudBinaryPath();
|
|
88
|
+
const binary = hudBinaryPath(home);
|
|
76
89
|
if (!existsSync(binary))
|
|
77
90
|
return false;
|
|
78
91
|
try {
|
|
79
|
-
const child = spawn(binary, [
|
|
80
|
-
"post", title, `${subtitle} — ${body}`, SURFACE_BUNDLE_IDS[profile.agent.primary_surface], prompt,
|
|
81
|
-
], {
|
|
82
|
-
env: { ...process.env, PANTHEON_HUD_EYEBROW: eyebrow },
|
|
83
|
-
detached: true, stdio: "ignore",
|
|
84
|
-
});
|
|
92
|
+
const child = spawn(binary, ["post", "--json", JSON.stringify({ ...notice, bin_dir: join(home, "bin") })], { detached: true, stdio: "ignore" });
|
|
85
93
|
child.unref();
|
|
86
94
|
return true;
|
|
87
95
|
}
|
|
@@ -119,9 +127,13 @@ export class HumanNotifier {
|
|
|
119
127
|
const due = prefs.decisions
|
|
120
128
|
? dueHumanObligations(client.obligations, this.cfg.member, state, now(), this.opts.reminderMs)
|
|
121
129
|
: [];
|
|
130
|
+
const tools = installedTools();
|
|
131
|
+
const peerOf = (from) => from ? { member: from, agent: client.roster.find((m) => m.id === from)?.agent } : undefined;
|
|
132
|
+
const withLabel = (n) => this.opts.label ? { ...n, context: n.context ? `${this.opts.label} · ${n.context}` : this.opts.label } : n;
|
|
122
133
|
for (const obligation of due) {
|
|
123
|
-
const
|
|
124
|
-
const
|
|
134
|
+
const act = client.seenAct(obligation.ref);
|
|
135
|
+
const notice = withLabel(composeDecision(obligation, act, obligation.goal ? client.goals.get(obligation.goal) : undefined, peerOf(act?.from), { tools, preferred: profile.agent.primary_surface, now: now() }));
|
|
136
|
+
const ok = send(notice);
|
|
125
137
|
if (ok) {
|
|
126
138
|
state.notified_at[obligation.id] = now();
|
|
127
139
|
dirty = true;
|
|
@@ -142,8 +154,7 @@ export class HumanNotifier {
|
|
|
142
154
|
const forMe = (r.to?.includes(this.cfg.member) ?? false) || (goal?.members.includes(this.cfg.member) ?? false);
|
|
143
155
|
if (!forMe)
|
|
144
156
|
continue;
|
|
145
|
-
const
|
|
146
|
-
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");
|
|
157
|
+
const ok = send(withLabel(composeDelivery(r, goal, peerOf(r.from), { tools, preferred: profile.agent.primary_surface, now: now() })));
|
|
147
158
|
if (ok) {
|
|
148
159
|
state.notified_at[`act:${r.id}`] = now();
|
|
149
160
|
writeState(stateFile, state);
|
package/dist/protocol.js
CHANGED
|
@@ -132,6 +132,15 @@ export const MakeInvite = z.object({
|
|
|
132
132
|
may_wake: z.boolean().default(true),
|
|
133
133
|
ttl_minutes: z.number().int().min(1).max(10080).default(60),
|
|
134
134
|
});
|
|
135
|
+
export const ListInvites = z.object({
|
|
136
|
+
t: z.literal("invites"),
|
|
137
|
+
cid: z.string().max(64).optional(),
|
|
138
|
+
});
|
|
139
|
+
export const RevokeInvite = z.object({
|
|
140
|
+
t: z.literal("revoke_invite"),
|
|
141
|
+
cid: z.string().max(64).optional(),
|
|
142
|
+
token: z.string().min(6).max(64),
|
|
143
|
+
});
|
|
135
144
|
export const Admit = z.object({
|
|
136
145
|
t: z.literal("admit"),
|
|
137
146
|
cid: z.string().max(64).optional(),
|
|
@@ -183,7 +192,7 @@ export const WorkerState = z.object({
|
|
|
183
192
|
detail: z.string().max(200).optional(),
|
|
184
193
|
until: z.number().int().positive().optional(),
|
|
185
194
|
});
|
|
186
|
-
export const Inbound = z.discriminatedUnion("t", [Hello, SendAct, OpenGoal, Claim, Admit, MakeInvite, PublishProfile, BindRepo, DeleteSpace, WorkerState, Ping]);
|
|
195
|
+
export const Inbound = z.discriminatedUnion("t", [Hello, SendAct, OpenGoal, Claim, Admit, MakeInvite, ListInvites, RevokeInvite, PublishProfile, BindRepo, DeleteSpace, WorkerState, Ping]);
|
|
187
196
|
export function shouldDeferToHuman(sessions, work, opts = {}) {
|
|
188
197
|
const now = opts.now ?? Date.now();
|
|
189
198
|
const idleMs = opts.idleMs ?? LIMITS.workerIdleMs;
|
package/dist/quickstart.js
CHANGED
|
@@ -12,6 +12,7 @@ import { detectAvailable, agentById } from "./agents.js";
|
|
|
12
12
|
import { installService } from "./service.js";
|
|
13
13
|
import { ensureNativeNotifier } from "./notifier.js";
|
|
14
14
|
import { ensureTrayBinary, writeTrayCommands } from "./tray.js";
|
|
15
|
+
import { desktopUnavailable, ephemeralWarning, hostEnv } from "./host-env.js";
|
|
15
16
|
import { existsSync } from "node:fs";
|
|
16
17
|
import { grant as grantCap, loadPolicy, policyPath, savePolicy } from "./worker-policy.js";
|
|
17
18
|
import { Relay } from "./relay.js";
|
|
@@ -27,9 +28,12 @@ export function wireUp(opts, deps = {}) {
|
|
|
27
28
|
const installSvc = deps.installService ?? installService;
|
|
28
29
|
const ensureNotifier = deps.ensureNotifier ?? ensureNativeNotifier;
|
|
29
30
|
const ensureTray = deps.ensureTray ?? ensureTrayBinary;
|
|
31
|
+
const host = deps.host ?? hostEnv();
|
|
30
32
|
const tools = installForUser().map((t) => ({ tool: t.tool, status: t.status }));
|
|
31
33
|
const granted = [];
|
|
32
34
|
const warnings = [];
|
|
35
|
+
if (host.ephemeral)
|
|
36
|
+
warnings.push(ephemeralWarning(host));
|
|
33
37
|
if (opts.hook) {
|
|
34
38
|
try {
|
|
35
39
|
const { agents } = installHooks("user");
|
|
@@ -47,10 +51,14 @@ export function wireUp(opts, deps = {}) {
|
|
|
47
51
|
else {
|
|
48
52
|
tools.push({ tool: "wake-up hook", status: "skipped (--no-hook) — a peer's message waits until you type" });
|
|
49
53
|
}
|
|
50
|
-
if (opts.notifications) {
|
|
54
|
+
if (opts.notifications && !host.desktop) {
|
|
55
|
+
tools.push({ tool: "notifications", status: desktopUnavailable(host, "desktop alerts") });
|
|
56
|
+
}
|
|
57
|
+
else if (opts.notifications) {
|
|
51
58
|
try {
|
|
52
59
|
ensureNotifier();
|
|
53
60
|
installSvc("notifier", homedir(), []);
|
|
61
|
+
writeTrayCommands();
|
|
54
62
|
tools.push({ tool: "notifications", status: "on — native alerts for decisions that require you" });
|
|
55
63
|
}
|
|
56
64
|
catch (e) {
|
|
@@ -61,7 +69,10 @@ export function wireUp(opts, deps = {}) {
|
|
|
61
69
|
else {
|
|
62
70
|
tools.push({ tool: "notifications", status: "off (--no-notifications) — human escalations only appear in pantheon status" });
|
|
63
71
|
}
|
|
64
|
-
if (opts.menubar) {
|
|
72
|
+
if (opts.menubar && !host.desktop) {
|
|
73
|
+
tools.push({ tool: "menu bar", status: desktopUnavailable(host, "the menu bar") });
|
|
74
|
+
}
|
|
75
|
+
else if (opts.menubar) {
|
|
65
76
|
try {
|
|
66
77
|
ensureTray();
|
|
67
78
|
writeTrayCommands();
|
|
@@ -241,7 +252,7 @@ export async function start(opts) {
|
|
|
241
252
|
const wired = wireUp({
|
|
242
253
|
hook: opts.hook !== false,
|
|
243
254
|
notifications: opts.notifications !== false,
|
|
244
|
-
menubar: opts.menubar !== false
|
|
255
|
+
menubar: opts.menubar !== false,
|
|
245
256
|
unattended: opts.unattended ?? null,
|
|
246
257
|
...(opts.workerHow ? { workerHow: opts.workerHow } : {}),
|
|
247
258
|
});
|
|
@@ -290,7 +301,7 @@ export async function join(opts) {
|
|
|
290
301
|
const wired = wireUp({
|
|
291
302
|
hook: opts.hook !== false,
|
|
292
303
|
notifications: opts.notifications !== false,
|
|
293
|
-
menubar: opts.menubar !== false
|
|
304
|
+
menubar: opts.menubar !== false,
|
|
294
305
|
unattended: opts.unattended ?? null,
|
|
295
306
|
...(opts.workerHow ? { workerHow: opts.workerHow } : {}),
|
|
296
307
|
});
|