@join-pantheon/cli 0.21.1 → 0.21.3

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 CHANGED
@@ -70,7 +70,7 @@ default when run inside a git repo; `--no-worker` opts out — and a peer still
70
70
  ends with a "You're set" summary, then a short guided setup on a terminal: whether teammates may wake
71
71
  your machine while you are away, what a woken run may execute, who you are and which decisions are
72
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"*.
73
+ `pantheon setup` re-runs it any time — and an agent setting you up can run `pantheon setup --questions`, ask you in chat, and pass your answers back as flags, so nobody has to open a terminal. Restart your agent once and ask it *"check pantheon_status"*.
74
74
 
75
75
  Already a member — new laptop, or just pulled a newer build? The same full setup is one command:
76
76
  `pantheon install --user` (from your project's repo).
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ import { runMcp } from "./mcp.js";
19
19
  import { Worker } from "./worker.js";
20
20
  import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, sendDesktopNotification } from "./notifier.js";
21
21
  import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, wakeDefault } from "./profile.js";
22
- import { runGuide, terminalIO } from "./setup-guide.js";
22
+ import { runGuide, terminalIO, parseSetupFlags, questionnaire } from "./setup-guide.js";
23
23
  import { doctor, render } from "./doctor.js";
24
24
  import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, dataSummary, stopProc, statFile } from "./machine.js";
25
25
  import { assess } from "./untrusted.js";
@@ -272,12 +272,13 @@ function unattendedProject() {
272
272
  async function guidedSetup(cfg, project, opts = {}) {
273
273
  if (argv.includes("--no-guide"))
274
274
  return;
275
- const interactive = !!process.stdin.isTTY && !argv.includes("--yes");
275
+ const preset = opts.preset ?? {};
276
+ const interactive = !!process.stdin.isTTY && !argv.includes("--yes") && !Object.keys(preset).length;
276
277
  if (!interactive && !opts.force)
277
278
  return;
278
279
  const rl = interactive ? createInterface({ input: process.stdin, output: process.stderr }) : null;
279
280
  const io = rl ? terminalIO(rl, (l) => console.error(l)) : null;
280
- console.error(`\n${B("A few choices, then you're done.")}${io ? "" : D(" (no terminal — taking the defaults)")}`);
281
+ console.error(`\n${B("A few choices, then you're done.")}${io ? "" : Object.keys(preset).length ? D(" (applying the answers you passed)") : D(" (no terminal — taking the defaults)")}`);
281
282
  try {
282
283
  const r = await runGuide(io, { cfg, project }, {
283
284
  publish: async (p) => {
@@ -308,7 +309,7 @@ async function guidedSetup(cfg, project, opts = {}) {
308
309
  bindRepo(info, cfg.spaceId);
309
310
  },
310
311
  } : {}),
311
- });
312
+ }, preset);
312
313
  console.error("");
313
314
  for (const l of r.applied)
314
315
  console.error(` ${C("✓")} ${l}`);
@@ -417,6 +418,7 @@ ${B("control")}
417
418
  pantheon doctor check this machine end to end
418
419
  pantheon setup the guided choices: who may wake this machine, what a run may execute,
419
420
  what you personally decide, which alerts you want — re-run any time
421
+ (--questions prints them for an agent to ask you; answers go back as flags)
420
422
  pantheon upgrade pull the latest pantheon, rebuild, and re-wire your agents
421
423
 
422
424
  ${D("more: pantheon help --all")}
@@ -489,6 +491,8 @@ async function main() {
489
491
  console.error(` ${C(`pantheon join ${r.invite}`)}\n`);
490
492
  console.error(D(` Tell them to run it from their project's git repo. Single use, expires in ${Math.round(Number(flag("ttl", "1440")) / 60)}h.`));
491
493
  console.error(D(` Redeeming it admits them — no separate approval step. Need another? \`pantheon invite\``));
494
+ console.error(D(`\n Not technical? Forward them this instead, and their agent does the rest:`));
495
+ console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${r.invite}`)}`);
492
496
  }
493
497
  if (!r.reach.shareable && r.reach.hint) {
494
498
  console.error(`\n\x1b[33m That address only works on this machine.\x1b[0m`);
@@ -513,6 +517,8 @@ async function main() {
513
517
  const inv = await client.makeInvite(mayWake, ttl).catch((e) => die(e.message));
514
518
  console.log(`pantheon join ${toInvite({ ...cfg, token: inv.url })}`);
515
519
  console.error(D(`\nSingle use, expires ${new Date(inv.expires_at).toLocaleString()}.`));
520
+ console.error(D(`For a teammate who is not technical, forward this and their agent does the rest:`));
521
+ console.error(` ${D("Tell your agent: run")} ${C("curl -fsSL https://www.joinpantheon.network/skills/join.md")} ${D("and follow it. Invite:")} ${C(`pantheon join ${toInvite({ ...cfg, token: inv.url })}`)}`);
516
522
  client.close();
517
523
  return;
518
524
  }
@@ -679,6 +685,35 @@ async function main() {
679
685
  }
680
686
  if (r.alreadyCurrent) {
681
687
  console.error(`\n${B("already up to date")} ${D(`(${r.from})`)}`);
688
+ if (process.platform === "darwin") {
689
+ const { trayStale, ensureTrayBinary, writeTrayCommands } = await import("./tray.js");
690
+ const { notifierStale, ensureNativeNotifier } = await import("./notifier.js");
691
+ const { restartService } = await import("./service.js");
692
+ if (trayStale()) {
693
+ try {
694
+ ensureTrayBinary();
695
+ writeTrayCommands();
696
+ for (const l of restartService("menubar"))
697
+ console.error(l);
698
+ }
699
+ catch (e) {
700
+ console.error(` menu bar: could not rebuild (${e.message.split("\n")[0]})`);
701
+ }
702
+ }
703
+ if (notifierStale()) {
704
+ try {
705
+ ensureNativeNotifier();
706
+ for (const l of restartService("notifier"))
707
+ console.error(l);
708
+ }
709
+ catch (e) {
710
+ console.error(` alerts: could not rebuild (${e.message.split("\n")[0]})`);
711
+ }
712
+ }
713
+ }
714
+ const cur = load();
715
+ if (cur && loadProfile(cur).agent.wake_default === undefined)
716
+ await guidedSetup(cur, repoInfo(process.cwd())?.root ?? null);
682
717
  return;
683
718
  }
684
719
  console.error(`\n${B(`upgraded ${r.from} → ${r.to}`)}`);
@@ -722,8 +757,15 @@ async function main() {
722
757
  return;
723
758
  }
724
759
  case "setup": {
760
+ if (argv.includes("--questions")) {
761
+ console.log(questionnaire());
762
+ return;
763
+ }
764
+ const { preset, errors } = parseSetupFlags(argv);
765
+ if (errors.length)
766
+ die(errors.join("\n") + "\n pantheon setup --questions lists every answer");
725
767
  const cfg = need();
726
- await guidedSetup(cfg, repoInfo(process.cwd())?.root ?? null, { force: true });
768
+ await guidedSetup(cfg, repoInfo(process.cwd())?.root ?? null, { force: true, preset });
727
769
  return;
728
770
  }
729
771
  case "version": {
package/dist/relay.js CHANGED
@@ -1774,7 +1774,7 @@ pantheon signup --email you@work.com --username you</code></pre>
1774
1774
  message: `over ${LIMITS.rateActs} acts in ${LIMITS.rateWindowMs / 1000}s. This is a backstop against a stuck loop, not a hard stop — wait a few seconds and send again. If you are genuinely producing this much, you are probably doing work that belongs in a commit and a DELIVERED, not in acts.` };
1775
1775
  }
1776
1776
  for (const id of to ?? []) {
1777
- if (!sp.members.has(id)) {
1777
+ if (!sp.members.has(id) && !sp.allow.get(id)?.admitted) {
1778
1778
  return { code: "NO_SUCH_MEMBER", message: `nobody here is called "${id}" — members: ${[...sp.members.keys()].join(", ")}` };
1779
1779
  }
1780
1780
  }
@@ -2211,6 +2211,11 @@ pantheon signup --email you@work.com --username you</code></pre>
2211
2211
  lastTouch: Date.now(),
2212
2212
  };
2213
2213
  this.spaces.set(name, sp);
2214
+ for (const e of allow.all()) {
2215
+ if (e.admitted && !sp.members.has(e.member)) {
2216
+ sp.members.set(e.member, { id: e.member, display: e.member, agent: "unknown", last_seen: e.first_seen, sessions: new Map(), activity: new Map(), delivered_seq: 0, stamps: [] });
2217
+ }
2218
+ }
2214
2219
  const open = [...sp.goals.values()].filter((g) => g.state !== "CLOSED").length;
2215
2220
  this.log(`[${name}] space ready (seq ${sp.ledger.head}, ${sp.goals.size} goals — ${open} still open, ${sp.obligations.size} obligations restored)`);
2216
2221
  this.compactSpace(sp);
@@ -55,14 +55,95 @@ export function detectBaseBranch(project) {
55
55
  const cur = git("rev-parse", "--abbrev-ref", "HEAD");
56
56
  return cur && cur !== "HEAD" ? cur : null;
57
57
  }
58
- export async function runGuide(io, ctx, deps = {}) {
58
+ export function questionnaire() {
59
+ const opt = (flag, id, label) => ` ${label} → --${flag} ${id}`;
60
+ return [
61
+ "pantheon setup — the questions, and the flag for each answer.",
62
+ "Ask your human each one in plain words, then run: pantheon setup --yes <flags>",
63
+ "",
64
+ "1. While they're away, may teammates hand work to this machine? (their agent runs here on its own,",
65
+ " in a separate copy of the repo, and delivers on a branch for review)",
66
+ ...WAKE_OPTIONS.map((o) => opt("wake", o.id, o.label)),
67
+ "2. When a teammate's request wakes this machine, what may the run do? (skip if --wake no)",
68
+ opt("commands", "no", "Edit files only — it can answer and draft, but cannot run tests, commit, or push."),
69
+ opt("commands", "yes", "Run commands too — needed to run tests and to commit and push work for review."),
70
+ "3. Their name, as teammates will see it. → --name \"…\"",
71
+ `4. Their role, in a word (${TITLE_OPTIONS.slice(0, -1).join(", ")}, or their own). → --title \"…\"`,
72
+ "5. Which of these do they decide personally? Agents will stop and ask them first. Any number, or none.",
73
+ ...DECISION_AREAS.map((x) => ` ${x.label} → ${x.id}`),
74
+ " pass them comma-separated: --decides deployment,pricing (or --decides none)",
75
+ "6. Which desktop alerts do they want?",
76
+ ...ALERT_OPTIONS.map((o) => opt("alerts", o.id, o.label)),
77
+ "7. Should their worker follow a short written playbook for this repo? (skip if --wake no)",
78
+ opt("playbook", "yes", "Yes — write a starter playbook now (recommended)"),
79
+ opt("playbook", "no", "Not now"),
80
+ "8. Which branch should workers start from, and deliveries be checked against? Usually the branch this",
81
+ " project deploys or releases from (often the default branch). → --base <branch>",
82
+ "",
83
+ "Example: pantheon setup --yes --wake anyone --commands yes --name \"Praneet Sinha\" --title Founder \\",
84
+ " --decides deployment --alerts decisions --playbook yes --base main",
85
+ ].join("\n");
86
+ }
87
+ export function parseSetupFlags(argv) {
88
+ const preset = {};
89
+ const errors = [];
90
+ const val = (name) => { const i = argv.indexOf(`--${name}`); return i >= 0 ? argv[i + 1] : undefined; };
91
+ const yn = (name) => { const v = val(name); if (v === undefined)
92
+ return undefined; if (/^(yes|true|on|1)$/i.test(v))
93
+ return true; if (/^(no|false|off|0)$/i.test(v))
94
+ return false; errors.push(`--${name} takes yes or no, not "${v}"`); return undefined; };
95
+ const wake = val("wake");
96
+ if (wake !== undefined) {
97
+ if (WAKE_OPTIONS.some((o) => o.id === wake))
98
+ preset.wake = wake;
99
+ else
100
+ errors.push(`--wake takes ${WAKE_OPTIONS.map((o) => o.id).join(", ")}, not "${wake}"`);
101
+ }
102
+ const commands = yn("commands");
103
+ if (commands !== undefined)
104
+ preset.commands = commands;
105
+ const name = val("name");
106
+ if (name !== undefined)
107
+ preset.name = name.trim();
108
+ const title = val("title");
109
+ if (title !== undefined)
110
+ preset.title = title.trim();
111
+ const decides = val("decides");
112
+ if (decides !== undefined) {
113
+ const ids = decides.split(",").map((x) => x.trim().toLowerCase()).filter((x) => x && x !== "none");
114
+ const bad = ids.filter((id) => !DECISION_AREAS.some((x) => x.id === id));
115
+ if (bad.length)
116
+ errors.push(`--decides: unknown area${bad.length === 1 ? "" : "s"} ${bad.join(", ")} — use ${DECISION_AREAS.map((x) => x.id).join(", ")} or none`);
117
+ else
118
+ preset.decides = [...new Set(ids)];
119
+ }
120
+ const alerts = val("alerts");
121
+ if (alerts !== undefined) {
122
+ if (ALERT_OPTIONS.some((o) => o.id === alerts))
123
+ preset.notifications = alerts;
124
+ else
125
+ errors.push(`--alerts takes ${ALERT_OPTIONS.map((o) => o.id).join(", ")}, not "${alerts}"`);
126
+ }
127
+ const playbook = yn("playbook");
128
+ if (playbook !== undefined)
129
+ preset.persona = playbook;
130
+ const base = val("base");
131
+ if (base !== undefined && base.trim())
132
+ preset.base = base.trim();
133
+ return { preset, errors };
134
+ }
135
+ export async function runGuide(io, ctx, deps = {}, preset = {}) {
59
136
  const install = deps.installService ?? realInstall;
60
137
  const uninstall = deps.uninstallService ?? realUninstall;
61
138
  const installed = deps.serviceInstalled ?? realInstalled;
62
139
  const file = deps.profileFile ?? profilePath();
63
140
  const profile = loadProfile(ctx.cfg, file);
64
- const d = guideDefaults(profile, ctx.cfg);
141
+ const d = { ...guideDefaults(profile, ctx.cfg), ...preset };
65
142
  const a = { ...d };
143
+ if (a.wake === "no") {
144
+ a.commands = false;
145
+ a.persona = false;
146
+ }
66
147
  const applied = [], warnings = [];
67
148
  if (io) {
68
149
  io.say("A few choices only you can make. Pick a number; Enter keeps the suggested one.\n");
@@ -90,8 +171,9 @@ export async function runGuide(io, ctx, deps = {}) {
90
171
  }
91
172
  const detected = ctx.project && deps.setRepoBase ? detectBaseBranch(ctx.project) : null;
92
173
  if (detected) {
93
- const b = await io.choose("Which branch should your worker start from, and deliveries be checked against? Usually the branch this project deploys or releases from.", [`${detected} (what origin calls its default)`, "A different branch"], 0);
94
- a.base = b === 0 ? detected : ((await io.ask("Branch name", detected)).trim() || detected);
174
+ const suggested = preset.base ?? detected;
175
+ const b = await io.choose("Which branch should your worker start from, and deliveries be checked against? Usually the branch this project deploys or releases from.", [`${suggested}${suggested === detected ? " (what origin calls its default)" : ""}`, "A different branch"], 0);
176
+ a.base = b === 0 ? suggested : ((await io.ask("Branch name", suggested)).trim() || suggested);
95
177
  }
96
178
  }
97
179
  let next = a.title ? applyProfileInit(profile, { name: a.name, title: a.title }) : { ...profile, human: { ...profile.human, display_name: a.name } };
package/dist/worker.js CHANGED
@@ -216,6 +216,43 @@ export class Worker {
216
216
  this.cfg = cfg;
217
217
  this.opts = opts;
218
218
  }
219
+ async bringMachineAlong() {
220
+ if (this.opts.selfUpgrade?.run)
221
+ return;
222
+ try {
223
+ const { restartService, serviceInstalled } = await import("./service.js");
224
+ if (process.platform === "darwin") {
225
+ const { trayStale, ensureTrayBinary, writeTrayCommands } = await import("./tray.js");
226
+ const { notifierStale, ensureNativeNotifier } = await import("./notifier.js");
227
+ if (trayStale()) {
228
+ try {
229
+ ensureTrayBinary();
230
+ writeTrayCommands();
231
+ this.opts.log(" menu bar rebuilt from the new source");
232
+ }
233
+ catch (e) {
234
+ this.opts.log(` menu bar not rebuilt: ${e.message.split("\n")[0]}`);
235
+ }
236
+ }
237
+ if (notifierStale()) {
238
+ try {
239
+ ensureNativeNotifier();
240
+ this.opts.log(" alerts helper rebuilt from the new source");
241
+ }
242
+ catch (e) {
243
+ this.opts.log(` alerts helper not rebuilt: ${e.message.split("\n")[0]}`);
244
+ }
245
+ }
246
+ }
247
+ for (const kind of ["notifier", "menubar"])
248
+ if (serviceInstalled(kind))
249
+ for (const l of restartService(kind))
250
+ this.opts.log(l);
251
+ }
252
+ catch (e) {
253
+ this.opts.log(` could not bring the other services along: ${e.message.split("\n")[0]}`);
254
+ }
255
+ }
219
256
  repoBase() {
220
257
  return this.client?.repos.find((r) => r.fingerprint === this.cfg.repo)?.base ?? null;
221
258
  }
@@ -661,6 +698,7 @@ export class Worker {
661
698
  summary: `self-upgraded pantheon ${result.from} -> ${result.to}; worker restarting on the new build`,
662
699
  body: `This machine's worker upgraded pantheon ${result.from} -> ${result.to} (fast-forward pull + rebuild, granted by its human) and is restarting on the new build.`,
663
700
  }).catch((e) => this.opts.log(`! could not report the upgrade on the ledger: ${e.message}`));
701
+ await this.bringMachineAlong();
664
702
  this.opts.log(`* self-upgrade done: ${result.from} -> ${result.to}; restarting worker to load it`);
665
703
  (this.opts.selfUpgrade?.restart ?? (() => process.exit(0)))();
666
704
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@join-pantheon/cli",
3
- "version": "0.21.1",
3
+ "version": "0.21.3",
4
4
  "description": "Let your coding agent collaborate directly with your teammates' agents — ask, agree an interface, and deliver, without a human relaying messages.",
5
5
  "type": "module",
6
6
  "bin": {