@join-pantheon/cli 0.32.0 → 0.33.0-rc.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/dist/cli.js CHANGED
@@ -9,24 +9,25 @@ process.emitWarning = ((warning, ...rest) => {
9
9
  import { randomBytes } from "node:crypto";
10
10
  import { gitConfigValue, suggestUsername } from "./onboard-defaults.js";
11
11
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
12
- import { basename, dirname, join, resolve } from "node:path";
12
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
13
13
  import { PantheonClient } from "./client.js";
14
14
  import { agentSpawn, CmdLineUnsafe } from "./which.js";
15
15
  import { fromInvite, looksLikeInviteCode, resolveInviteCode, load, save, toInvite, toJoinLink, resolveFor, loadAll, identity, repoInfo, bindRepo, bindFolder, unbindRepo, findMembership, setDefaultSpace, removeMembership, configFor, CONFIG_DIR, CONFIG_PATH, DEFAULT_RELAY, resolveForHook } from "./config.js";
16
16
  import { fmtLimit } from "./plans.js";
17
17
  import { validEmail, validUsername } from "./accounts.js";
18
- import { install, launcher, installForUser, windowsRepoOverrides } from "./install.js";
18
+ import { install, launcher, installForUser, windowsRepoOverrides, agentAsksOnce } from "./install.js";
19
19
  import { Relay } from "./relay.js";
20
20
  import { runMcp } from "./mcp.js";
21
21
  import { Worker } from "./worker.js";
22
22
  import { DEFAULT_REMINDER_MS, ensureNativeNotifier, HumanNotifier, runNotifiers, sendDesktopNotification } from "./notifier.js";
23
+ import { linuxDesktopReachable } from "./notice-linux.js";
23
24
  import { desktopUnavailable, hostEnv } from "./host-env.js";
24
25
  import { AGENT_SURFACES, applyProfileInit, cleanList, isAgentSurface, loadProfile, organizationProfile, profileInitialized, saveProfile, teamProfile, wakeDefault, wakeMode } from "./profile.js";
25
26
  import { runGuide, terminalIO, questionOrNull, parseSetupFlags, questionnaire, optOutsFrom } from "./setup-guide.js";
26
27
  import { selectActs, formatAct, resolveRef } from "./history-view.js";
27
28
  import { doctor, render, shareBlock } from "./doctor.js";
28
29
  import { FEEDBACK_ADDRESS } from "./feedback.js";
29
- import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, dataSummary, stopProc, statFile } from "./machine.js";
30
+ import { menubarProc, notifierProc, relayProc, workerProc, wiring, logFileFor, dataSummary, stopProc, statFile, elapsed, windowsProcessInfo } from "./machine.js";
30
31
  import { assess } from "./untrusted.js";
31
32
  import { report as spendReport, recentRuns } from "./spend.js";
32
33
  import { renderBrief, renderTeam } from "./brief.js";
@@ -38,7 +39,9 @@ import { removeHook, decideStop, writeIntent, clearIntent, readIntent, installHo
38
39
  import { pending, render as renderPending } from "./pending.js";
39
40
  import { signedRead } from "./reads.js";
40
41
  import { start as qsStart, join as qsJoin, wireUp, teamNameFor, deviceLabel } from "./quickstart.js";
41
- import { installService, uninstallService } from "./service.js";
42
+ import { folderRefusalHere, isTemporaryRefusal } from "./folder-guard.js";
43
+ import { installService, serviceInstalled, serviceLogFile, uninstallService } from "./service.js";
44
+ import { LEGACY_TASK_NAME, hostAlive, hostFiles, readHostState, recordedTaskName } from "./win-host.js";
42
45
  import { ACTS, LIMITS } from "./protocol.js";
43
46
  import { loadPersonas, personaByName, describePersona, initPersona, personaDir } from "./persona.js";
44
47
  import { fingerprint, newKeypair, signAccount } from "./identity.js";
@@ -47,7 +50,8 @@ import { connectFailure, signInHint } from "./refusal.js";
47
50
  import * as mcpWindows from "./mcp-windows.js";
48
51
  import { updateBanner } from "./update-check.js";
49
52
  import { inWokenRun } from "./worker-policy.js";
50
- import { joinInstructions, managedLayout, relayServesInstaller } from "./managed.js";
53
+ import { childNodeFlags, joinInstructions, managedLayout, relayServesInstaller } from "./managed.js";
54
+ import { spawn } from "node:child_process";
51
55
  const argv = process.argv.slice(2);
52
56
  for (let i = argv.length - 1; i >= 1; i--) {
53
57
  const m = /^(--[a-z][a-z0-9-]*)=(.*)$/i.exec(argv[i]);
@@ -822,24 +826,10 @@ function printReady(tools) {
822
826
  console.error(D(` While you're away, a teammate you approve to wake this machine can hand work to it —`));
823
827
  if (workerOn)
824
828
  console.error(D(` isolated branch, no secrets in the worktree. Pause any time from the menu bar.`));
825
- const oneTime = new Map();
826
- for (const t of tools) {
827
- if (/unavailable|not found/i.test(t.status))
828
- continue;
829
- if (/claude/i.test(t.tool))
830
- oneTime.set("claude", `claude say yes once if it asks to trust this folder`);
831
- if (/cursor/i.test(t.tool))
832
- oneTime.set("cursor", `cursor enable the "pantheon" MCP server if it asks`);
833
- if (/codex/i.test(t.tool))
834
- oneTime.set("codex", `codex run /hooks inside codex once to approve the wake-up hook`);
835
- if (/kiro/i.test(t.tool))
836
- oneTime.set("kiro", `kiro run kiro-cli chat --agent pantheon (or kiro-cli agent set-default pantheon) to keep working while a teammate waits; stay signed in — a woken run never starts Kiro signed out, since Kiro would open a browser`);
837
- if (/copilot/i.test(t.tool))
838
- oneTime.set("copilot", `copilot say yes once when it asks to trust this folder; on a Copilot Business or Enterprise plan an org owner must have the "MCP servers in Copilot" policy on`);
839
- }
840
- if (oneTime.size) {
829
+ const oneTime = agentAsksOnce(tools);
830
+ if (oneTime.length) {
841
831
  console.error(`\n${B("Your agents may each ask once")} ${D("(their own security, not ours — one yes each):")}`);
842
- for (const l of oneTime.values())
832
+ for (const l of oneTime)
843
833
  console.error(D(` ${l}`));
844
834
  }
845
835
  }
@@ -909,12 +899,16 @@ ${B("control")}
909
899
  pantheon persona how a woken worker should behave: list / init <name> / show <name>
910
900
  pantheon notifier alert you when a task requires your decision
911
901
  pantheon menubar the menu bar item: status at a glance, pause/resume, decisions
912
- pantheon sessions your devices & active sessions; revoke <id> to sign one out
902
+ pantheon tray Windows: show the tray icon again after you quit it (it also returns at your next logon)
903
+ pantheon sessions your devices & active sessions; revoke <id> to sign one out
913
904
  pantheon logout sign this machine out · pantheon account delete remove your account
914
905
  pantheon audit [--space X] the log: who did what, when (--json to export, --since <hours>)
915
906
  pantheon profile show, create (init), or set your agent and attention profile
916
907
  pantheon directory organization roles and decision authority
917
908
  pantheon approvals decisions waiting on you; show or answer one
909
+ pantheon board your board in the browser, on any OS: what needs you, what your agents are on, and act on it
910
+ (--open start or reuse it and open it · --url print its address
911
+ · --watch the same lanes in this terminal, for SSH · --keep never stop when idle · --stop)
918
912
  pantheon stop [relay|worker|notifier|menubar|all] stop it
919
913
  pantheon hook install your agent keeps working when a peer is waiting on it
920
914
  (auto-detects claude / codex / cursor / copilot / kiro on this machine)
@@ -930,6 +924,7 @@ ${B("control")}
930
924
  (--questions prints them for an agent to ask you; answers go back as flags,
931
925
  to setup --yes or straight to start / join)
932
926
  pantheon upgrade pull the latest pantheon, rebuild, and re-wire your agents
927
+ pantheon upgrade <version> install exactly that release (e.g. to try one before your team does)
933
928
 
934
929
  ${D("more: pantheon help --all")}
935
930
  `;
@@ -938,6 +933,7 @@ ${B("less common")}
938
933
  pantheon relay run only the relay, in this terminal
939
934
  pantheon service install [relay|worker|notifier|menubar] | restart | uninstall
940
935
  run a long-lived component in the background, from login
936
+ (Windows: service start <worker|notifier> resumes one stopped with pantheon stop)
941
937
  pantheon members the full list, with key fingerprints
942
938
  pantheon pending what a live agent window has not yet seen
943
939
  pantheon goal open "<title>" --with <who> open a goal to work in (goal list · goal close <id> "<summary>")
@@ -951,7 +947,32 @@ ${B("less common")}
951
947
  pantheon limits the guard values in force
952
948
  pantheon version the pantheon version this machine is running
953
949
  `;
954
- const QUIET = new Set(["mcp", "hook", "pending", "version", "tray-status"]);
950
+ let workerAlerts = null;
951
+ let workerAlertsSaid = false;
952
+ function notifyFromWorker(n) {
953
+ if (process.platform === "linux") {
954
+ if (!workerAlerts || Date.now() - workerAlerts.at > 60_000)
955
+ workerAlerts = { at: Date.now(), ok: linuxDesktopReachable() };
956
+ if (!workerAlerts.ok) {
957
+ if (!workerAlertsSaid) {
958
+ workerAlertsSaid = true;
959
+ console.error(D("alerts: none — no graphical session on this machine; run notices wait in pantheon brief and pantheon board --watch"));
960
+ }
961
+ return;
962
+ }
963
+ }
964
+ if (process.platform !== "win32") {
965
+ sendDesktopNotification(n);
966
+ return;
967
+ }
968
+ const c = spawn(process.execPath, [...childNodeFlags(), process.argv[1], "notice-show"], {
969
+ env: { ...process.env, PANTHEON_NOTICE_B64: Buffer.from(JSON.stringify(n), "utf8").toString("base64") },
970
+ detached: true, stdio: "ignore", windowsHide: true,
971
+ });
972
+ c.on("error", () => { });
973
+ c.unref();
974
+ }
975
+ const QUIET = new Set(["mcp", "hook", "pending", "version", "tray-status", "handle-uri", "notice-show"]);
955
976
  function usageFor(command, help) {
956
977
  const plain = help.replace(/\x1b\[[0-9;]*m/g, "").split("\n");
957
978
  const out = [];
@@ -990,6 +1011,7 @@ const COMMAND_FLAGS = {
990
1011
  signup: "--email <address> --username <name> --relay <url> --label <device name>",
991
1012
  audit: "--space <team> --since <hours> --json",
992
1013
  setup: "--questions (and every answer as a flag, with --yes)",
1014
+ board: "--open --url --watch --keep --stop",
993
1015
  };
994
1016
  const HELP_OPAQUE = new Set(["mcp", "hook", "pending", "tray-status", "help", "version", "-v", "--help", "-h"]);
995
1017
  function rememberWorkingProxy() {
@@ -1031,13 +1053,13 @@ function slipDistance(a, b) {
1031
1053
  }
1032
1054
  return d[a.length][b.length];
1033
1055
  }
1034
- const EXTRA_FLAGS = "--agent-cmd --approve-all --ask-before-sending --child --code --data --device --display --force --help --host --idle --keywords --local " +
1035
- "--no-continue --no-guide --no-notify --path --peek --poll --portable --publish --queued --quiet --remind --scratch --session --surface --token --unattended " +
1056
+ const EXTRA_FLAGS = "--action-id --agent-cmd --approve-all --ask-before-sending --child --code --data --device --display --force --help --host --idle --keywords --local " +
1057
+ "--no-browser --no-continue --no-guide --no-notify --only-action --path --peek --poll --portable --publish --queued --quiet --remind --scratch --serve --session --surface --token --unattended " +
1036
1058
  "--until --user --version --all --json " +
1037
1059
  "--name --title --wake --commands --unsandboxed --alerts --decides --playbook --base --responsibilities --human-required --agent-can-decide --capabilities";
1038
1060
  const KNOWN_FLAGS = new Set([...`${Object.values(COMMAND_FLAGS).join(" ")} ${EXTRA_FLAGS}`.matchAll(/--([a-z][a-z0-9-]*)/g)].map((m) => m[1]));
1039
1061
  function refuseUnknownFlags() {
1040
- if (["mcp", "hook", "tray-status", "_menubar"].includes(cmd))
1062
+ if (["mcp", "hook", "tray-status", "_menubar", "handle-uri"].includes(cmd))
1041
1063
  return;
1042
1064
  for (const a of argv.slice(1)) {
1043
1065
  if (!/^--[a-z][a-z0-9-]*(?:=\S*)?$/i.test(a))
@@ -1522,14 +1544,28 @@ async function main() {
1522
1544
  }
1523
1545
  case "upgrade": {
1524
1546
  const { upgrade } = await import("./upgrade.js");
1525
- console.error(`${B("pantheon upgrade")}\n`);
1526
- const r = upgrade();
1547
+ const asked = argv[1] && !argv[1].startsWith("-") ? argv[1] : undefined;
1548
+ if (asked) {
1549
+ const { isVersion } = await import("./installer.js");
1550
+ if (!isVersion(asked))
1551
+ die(`usage: pantheon upgrade [version] ("${asked}" is not a version, e.g. 0.32.2)`);
1552
+ }
1553
+ console.error(`${B("pantheon upgrade")}${asked ? ` ${asked}` : ""}\n`);
1554
+ const r = upgrade(undefined, asked ? { version: asked, exact: true } : {});
1527
1555
  for (const s of r.steps)
1528
1556
  console.error(` ${s.ok ? C("ok") : "\x1b[31mFAIL\x1b[0m"} ${s.name.padEnd(6)} ${s.detail}`);
1529
1557
  if (!r.ok) {
1530
1558
  console.error(`\n${B("upgrade stopped")} — nothing was changed.${r.needsPerson ? "" : " Fix the above and run `pantheon upgrade` again."}`);
1531
1559
  process.exit(1);
1532
1560
  }
1561
+ if (r.alreadyCurrent && r.steps.some((x) => /another pantheon upgrade is running/.test(x.detail))) {
1562
+ console.error(`\n${B("nothing installed")} — another upgrade is running on this machine (most likely the worker's own). Try again in a minute.`);
1563
+ process.exit(1);
1564
+ }
1565
+ if (asked && r.alreadyCurrent && r.steps.some((x) => /not on npm yet/.test(x.detail))) {
1566
+ console.error(`\n${B("nothing installed")} — npm has no ${asked}. Check the version (\`npm view @join-pantheon/cli versions\`).`);
1567
+ process.exit(1);
1568
+ }
1533
1569
  if (r.alreadyCurrent) {
1534
1570
  console.error(`\n${B("already up to date")} ${D(`(${r.from})`)}`);
1535
1571
  if (process.platform === "darwin") {
@@ -1998,14 +2034,26 @@ async function main() {
1998
2034
  console.log(` ${D("start it with: pantheon start")}`);
1999
2035
  }
2000
2036
  if (worker.running) {
2001
- console.log(` worker ${C("running")} ${worker.managedBy === "launchd" ? `service ${worker.label}` : "started by hand"} pid ${worker.pid}`);
2037
+ console.log(` worker ${C("running")} ${worker.managedBy === "launchd" ? `service ${worker.label}` : worker.managedBy === "host" ? `background host (${worker.label})` : "started by hand"} pid ${worker.pid}`);
2002
2038
  console.log(` ${D("a teammate approved on this machine (pantheon wake list) can start an agent run here")}`);
2003
2039
  }
2004
2040
  else {
2005
- console.log(` worker ${D("not running")} ${D("nobody can start an agent run on this machine")}`);
2041
+ const paused = worker.managedBy === "host" && existsSync(hostFiles().disabled("worker"));
2042
+ console.log(` worker ${D(paused ? "paused" : "not running")} ${D(paused ? "pantheon service start worker resumes it" : "nobody can start an agent run on this machine")}`);
2006
2043
  }
2007
2044
  const menubar = menubarProc();
2008
- if (menubar.running) {
2045
+ if (process.platform === "win32") {
2046
+ const st = readHostState();
2047
+ if (hostAlive(st)) {
2048
+ console.log(` host ${C("running")} task ${recordedTaskName() ?? LEGACY_TASK_NAME} pid ${st.pid} up ${elapsed(Date.now() - st.started)}`);
2049
+ const tray = st.children.tray;
2050
+ const quit = existsSync(hostFiles().disabled("tray"));
2051
+ console.log(` tray ${tray?.phase === "running" ? `${C("running")} pid ${tray.pid}` : D(quit ? "quit — back at your next logon, or now: pantheon tray" : tray?.gaveUp === "clm" ? "not shown — PowerShell is in Constrained Language Mode here (pantheon board instead)" : "not running")}`);
2052
+ }
2053
+ else
2054
+ console.log(` host ${D("not running")} ${D(serviceInstalled("worker") || serviceInstalled("notifier") ? "pantheon service start worker (or log out and back in)" : "pantheon service install worker — the worker, alerts and the tray in the background")}`);
2055
+ }
2056
+ else if (menubar.running) {
2009
2057
  console.log(` menu bar ${C("running")} ${menubar.managedBy === "launchd" ? `service ${menubar.label}` : "started by hand"} pid ${menubar.pid}`);
2010
2058
  }
2011
2059
  else {
@@ -2013,7 +2061,7 @@ async function main() {
2013
2061
  console.log(` menu bar ${D("not running")} ${D(why ?? "pantheon service install menubar — status and pause/resume in the menu bar")}`);
2014
2062
  }
2015
2063
  if (notifier.running) {
2016
- console.log(` notifier ${C("running")} ${notifier.managedBy === "launchd" ? `service ${notifier.label}` : "started by hand"} pid ${notifier.pid}`);
2064
+ console.log(` notifier ${C("running")} ${notifier.managedBy === "launchd" || notifier.managedBy === "systemd" ? `service ${notifier.label}` : notifier.managedBy === "host" ? `background host (${notifier.label})` : "started by hand"} pid ${notifier.pid}`);
2017
2065
  console.log(` ${D("native alerts are on for decisions that require you")}`);
2018
2066
  }
2019
2067
  else {
@@ -2093,7 +2141,7 @@ async function main() {
2093
2141
  const proc = kind === "relay" ? relayProc(Number(flag("port", "8787"))) : kind === "worker" ? workerProc() : notifierProc();
2094
2142
  const f = logFileFor(kind, proc);
2095
2143
  if (!f)
2096
- die(`no ${kind} log found. ${process.platform === "linux" ? `If it runs as a service: journalctl --user -u pantheon-${kind}` : process.platform === "darwin" ? "If it runs as a service, the plist names the path" : "Services are not used on Windows"}; if you started it by hand, its output went to that terminal.`);
2144
+ die(`no ${kind} log found. ${process.platform === "linux" ? `If it runs as a service: journalctl --user -u pantheon-${kind}` : process.platform === "darwin" ? "If it runs as a service, the plist names the path" : `The Pantheon host writes it to ${serviceLogFile(kind)} once it has started it (its own log: ${hostFiles().log})`}; if you started it by hand, its output went to that terminal.`);
2097
2145
  console.error(D(`${f} (${statFile(f)})\n`));
2098
2146
  const follow = argv.includes("-f") || argv.includes("--follow");
2099
2147
  const { tailLines, followFile } = await import("./machine.js");
@@ -2501,6 +2549,13 @@ async function main() {
2501
2549
  console.error(` perms:${l}`);
2502
2550
  removed.push(l.trim().replace(/^removed /, ""));
2503
2551
  }
2552
+ if (process.platform === "win32") {
2553
+ const { unregisterUriHandler, realRegIo } = await import("./uri-handler.js");
2554
+ const { borrowedProfile } = await import("./service.js");
2555
+ const u = unregisterUriHandler(realRegIo(() => borrowedProfile()));
2556
+ removed.push(...u.removed);
2557
+ left.push(...u.left);
2558
+ }
2504
2559
  const { unlinkSync, existsSync: exists, rmSync, readdirSync } = await import("node:fs");
2505
2560
  if (all && exists(CONFIG_PATH)) {
2506
2561
  if (keepIdentity) {
@@ -3418,6 +3473,64 @@ async function main() {
3418
3473
  console.log(`\n${D("Change it: pantheon policy grant|revoke self-upgrade|run-commands|unsandboxed-runs · personas: pantheon persona init · who may wake this machine: pantheon wake allow|deny <who>")}`);
3419
3474
  return;
3420
3475
  }
3476
+ case "notice-show": {
3477
+ const raw = process.env.PANTHEON_NOTICE_B64;
3478
+ if (!raw)
3479
+ return;
3480
+ try {
3481
+ sendDesktopNotification(JSON.parse(Buffer.from(raw, "base64").toString("utf8")));
3482
+ }
3483
+ catch { }
3484
+ return;
3485
+ }
3486
+ case "handle-uri": {
3487
+ const { handleUri, parseHandleUriArgs, logUri } = await import("./uri-handler.js");
3488
+ const { spawn, spawnSync } = await import("node:child_process");
3489
+ const { childNodeFlags } = await import("./managed.js");
3490
+ const { statSync } = await import("node:fs");
3491
+ const self = (args) => [...childNodeFlags(), process.argv[1], ...args];
3492
+ const hasBoard = /\n\s*pantheon board\b/.test(HELP_ALL);
3493
+ const parsed = parseHandleUriArgs(argv.slice(1), isAbsolute, (p) => resolve(p));
3494
+ const isDir = (p) => { try {
3495
+ return statSync(p).isDirectory();
3496
+ }
3497
+ catch {
3498
+ return false;
3499
+ } };
3500
+ if ("error" in parsed || (parsed.home && !isDir(parsed.home))) {
3501
+ const why = "error" in parsed ? parsed.error : `its --home (${parsed.home}) is not a folder on this machine`;
3502
+ console.log(`Pantheon did not act on this link: ${why}`);
3503
+ logUri(CONFIG_DIR, `refused (${why})`);
3504
+ process.exit(2);
3505
+ }
3506
+ const uriHome = parsed.home ?? CONFIG_DIR;
3507
+ const childEnv = parsed.home ? { ...process.env, PANTHEON_HOME: parsed.home } : process.env;
3508
+ const code = await handleUri(parsed.rest, {
3509
+ home: uriHome,
3510
+ wokenRun: inWokenRun(),
3511
+ runQueued: (actionId) => spawnSync(process.execPath, self(["explain", "--queued", "--only-action", "--action-id", actionId]), { stdio: "inherit", env: childEnv }).status ?? 1,
3512
+ openBoard: hasBoard ? () => new Promise((done) => {
3513
+ const c = spawn(process.execPath, self(["board", "--open"]), { detached: true, stdio: "ignore", windowsHide: true, env: childEnv });
3514
+ const t = setTimeout(() => { c.unref(); done(true); }, 4_000);
3515
+ c.on("error", () => { clearTimeout(t); done(false); });
3516
+ c.on("exit", (status) => { clearTimeout(t); done(status === 0); });
3517
+ }) : null,
3518
+ runBrief: () => spawnSync(process.execPath, self(["brief"]), { stdio: "inherit", env: childEnv }).status ?? 1,
3519
+ print: (l) => console.log(l),
3520
+ pause: async () => {
3521
+ if (!process.stdin.isTTY)
3522
+ return;
3523
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
3524
+ try {
3525
+ await questionOrNull(rl, D("\npress Enter to close "));
3526
+ }
3527
+ finally {
3528
+ rl.close();
3529
+ }
3530
+ },
3531
+ });
3532
+ process.exit(code);
3533
+ }
3421
3534
  case "explain": {
3422
3535
  const { readFileSync, rmSync } = await import("node:fs");
3423
3536
  const CONFIG_DIR = dirname(CONFIG_PATH);
@@ -3428,18 +3541,48 @@ async function main() {
3428
3541
  refuse("a woken run cannot act for the menu bar — a person has to click it");
3429
3542
  const f = join(CONFIG_DIR, "bin", ".hud-action.json");
3430
3543
  const { statSync } = await import("node:fs");
3544
+ const want = flag("action-id");
3545
+ if (want !== undefined && !/^[0-9a-f]{16,64}$/.test(want))
3546
+ refuse("that is not an action id");
3431
3547
  let st = null;
3432
3548
  let raw = null;
3433
3549
  try {
3434
3550
  st = statSync(f);
3435
3551
  raw = readFileSync(f, "utf8");
3436
- rmSync(f, { force: true });
3437
3552
  }
3438
3553
  catch {
3439
3554
  st = null;
3440
3555
  raw = null;
3441
3556
  }
3557
+ if (raw !== null) {
3558
+ let nonce;
3559
+ try {
3560
+ nonce = JSON.parse(raw).nonce;
3561
+ }
3562
+ catch {
3563
+ nonce = undefined;
3564
+ }
3565
+ if (want !== undefined && nonce !== want)
3566
+ refuse("another click replaced that action before it ran — nothing was sent from here; click it again");
3567
+ if (want === undefined && typeof nonce === "string")
3568
+ refuse("another Pantheon window is sending an action right now — click it again in a moment");
3569
+ }
3570
+ if (raw !== null) {
3571
+ try {
3572
+ rmSync(f, { force: true });
3573
+ }
3574
+ catch { }
3575
+ }
3442
3576
  if (st && raw !== null) {
3577
+ let parsed = null;
3578
+ try {
3579
+ parsed = JSON.parse(raw);
3580
+ }
3581
+ catch {
3582
+ parsed = null;
3583
+ }
3584
+ if (want !== undefined && parsed && typeof parsed.nonce === "string" && /^[0-9a-f]{16,64}$/.test(parsed.nonce))
3585
+ console.log(`action ${parsed.nonce}`);
3443
3586
  const age = Date.now() - st.mtimeMs;
3444
3587
  const { runInFlight } = await import("./worker.js");
3445
3588
  const run = runInFlight(CONFIG_DIR);
@@ -3447,13 +3590,10 @@ async function main() {
3447
3590
  refuse(`that menu-bar action was written ${Math.round(age / 1000)}s ago — click it again`);
3448
3591
  if (run && st.mtimeMs >= run.at - 1_000)
3449
3592
  refuse("that menu-bar action was written while a woken run was working on this machine, so it is not trusted — click it again once the run is done");
3450
- try {
3451
- hud = JSON.parse(raw);
3452
- }
3453
- catch {
3454
- hud = null;
3455
- }
3593
+ hud = parsed;
3456
3594
  }
3595
+ if (argv.includes("--only-action") && !hud)
3596
+ refuse("that action was already taken or is no longer there — click it again");
3457
3597
  }
3458
3598
  const cfg = (() => {
3459
3599
  if (!hud?.space)
@@ -3477,7 +3617,7 @@ async function main() {
3477
3617
  const { pickExplainAgent, decisionPrompt: dp, allDecisionsPrompt: adp, decisionTitle: dt, sessionName } = await import("./decision-ui.js");
3478
3618
  const { loadProfile } = await import("./profile.js");
3479
3619
  const { detectAvailable, launchArgs } = await import("./agents.js");
3480
- let id = argv.find((a, i) => i > 0 && !a.startsWith("--"));
3620
+ let id = argv.find((a, i) => i > 0 && !a.startsWith("--") && argv[i - 1] !== "--action-id");
3481
3621
  if (argv.includes("--queued")) {
3482
3622
  const queued = join(CONFIG_DIR, "bin", ".explain-id");
3483
3623
  try {
@@ -3978,7 +4118,7 @@ async function main() {
3978
4118
  once: argv.includes("--once"),
3979
4119
  ...(argv.includes("--no-notify") ? {} : {
3980
4120
  notify: (n) => { try {
3981
- sendDesktopNotification(n);
4121
+ notifyFromWorker(n);
3982
4122
  }
3983
4123
  catch { } },
3984
4124
  }),
@@ -4009,7 +4149,7 @@ async function main() {
4009
4149
  once: argv.includes("--once"),
4010
4150
  ...(argv.includes("--no-notify") ? {} : {
4011
4151
  notify: (n) => { try {
4012
- sendDesktopNotification(n);
4152
+ notifyFromWorker(n);
4013
4153
  }
4014
4154
  catch { } },
4015
4155
  }),
@@ -4024,6 +4164,58 @@ async function main() {
4024
4164
  await worker.run();
4025
4165
  return;
4026
4166
  }
4167
+ case "board": {
4168
+ const b = await import("./board.js");
4169
+ if (inWokenRun())
4170
+ die("a woken run cannot open the board — a person has to run this here");
4171
+ if (argv.includes("--watch")) {
4172
+ await b.watchBoard();
4173
+ return;
4174
+ }
4175
+ if (argv.includes("--serve")) {
4176
+ await b.serveBoard({ keep: argv.includes("--keep") });
4177
+ process.exit(0);
4178
+ }
4179
+ if (argv.includes("--stop")) {
4180
+ const s = await b.runningBoard();
4181
+ if (!s) {
4182
+ console.error(D("no board is running"));
4183
+ return;
4184
+ }
4185
+ await b.boardPost(s, "/api/stop").catch(() => null);
4186
+ console.error(D("the board is stopped"));
4187
+ return;
4188
+ }
4189
+ const r = await b.ensureBoard({ keep: argv.includes("--keep") }).catch((e) => die(e.message));
4190
+ if (argv.includes("--url")) {
4191
+ console.log(r.url);
4192
+ console.error(D(" good once, within 30 seconds — pantheon board --url makes another"));
4193
+ if (!argv.includes("--open"))
4194
+ return;
4195
+ }
4196
+ const url = argv.includes("--url") ? await b.boardLinkFor(r.state).catch((e) => die(e.message)) : r.url;
4197
+ const opened = argv.includes("--no-browser") ? false : await b.openBrowser(url).catch(() => false);
4198
+ console.error(`${B("pantheon board")} ${D(r.reused ? "(already running)" : "(started)")}`);
4199
+ if (!opened)
4200
+ console.error(` open this in your browser within 30 seconds (it works once): ${C(url)}`);
4201
+ else
4202
+ console.error(D(" opened in your browser · pantheon board --url prints its address · --watch shows it in this terminal"));
4203
+ console.error(D(r.state.keep ? " it stays running" : " it stops itself after 30 minutes with no open tab"));
4204
+ return;
4205
+ }
4206
+ case "tray": {
4207
+ if (process.platform !== "win32")
4208
+ die(process.platform === "darwin" ? "the tray icon is Windows'; on macOS the menu bar is: pantheon menubar" : "the tray icon is Windows'; on Linux: pantheon board");
4209
+ const { showTray } = await import("./service.js");
4210
+ try {
4211
+ for (const l of showTray())
4212
+ console.error(l);
4213
+ }
4214
+ catch (e) {
4215
+ die(e.message);
4216
+ }
4217
+ return;
4218
+ }
4027
4219
  case "tray-status": {
4028
4220
  const { trayStatus } = await import("./tray.js");
4029
4221
  console.log(JSON.stringify(await trayStatus()));
@@ -4100,6 +4292,19 @@ async function main() {
4100
4292
  await new Promise((r) => child.on("close", () => r()));
4101
4293
  return;
4102
4294
  }
4295
+ case "host": {
4296
+ const { runHost, provenOrphan } = await import("./win-host.js");
4297
+ const { childNodeFlags, managedLauncher } = await import("./managed.js");
4298
+ const layout = managedLayout();
4299
+ runHost({
4300
+ node: process.execPath,
4301
+ flags: childNodeFlags(),
4302
+ managed: layout ? { pkgDir: layout.pkgDir, prevDir: layout.prevDir } : null,
4303
+ ...(managedLauncher() ? { pantheonCommand: managedLauncher() } : {}),
4304
+ isOurs: (pid, r) => process.platform === "win32" && provenOrphan(r, windowsProcessInfo(pid)),
4305
+ });
4306
+ return;
4307
+ }
4103
4308
  case "notifier": {
4104
4309
  const nf = loadAll();
4105
4310
  const teams = nf ? Object.values(nf.spaces ?? {}).map((m) => configFor(nf, m, null)) : [];
@@ -4110,6 +4315,15 @@ async function main() {
4110
4315
  log: (line) => console.error(cfgs.length > 1 ? `[${c.space}] ${line}` : line),
4111
4316
  }));
4112
4317
  console.error(`${B("pantheon notifier")} ${D(`(${cfgs.length === 1 ? "" : cfgs.length + " teams · "}ctrl-c to stop)`)}`);
4318
+ if (process.platform === "win32") {
4319
+ try {
4320
+ const { ensureUriHandler } = await import("./notice-windows.js");
4321
+ console.error(D(` ${ensureUriHandler().line}`));
4322
+ }
4323
+ catch (e) {
4324
+ console.error(D(` pantheon: links not registered (${e.message.split("\n")[0]})`));
4325
+ }
4326
+ }
4113
4327
  const stopAll = () => { for (const n of notifiers)
4114
4328
  n.stop(); process.exit(0); };
4115
4329
  process.on("SIGINT", stopAll);
@@ -4148,6 +4362,17 @@ async function main() {
4148
4362
  console.error(l);
4149
4363
  return;
4150
4364
  }
4365
+ if (sub === "start" && process.platform === "win32" && (kind === "worker" || kind === "notifier")) {
4366
+ const { enableService } = await import("./service.js");
4367
+ try {
4368
+ for (const l of enableService(kind))
4369
+ console.error(l);
4370
+ }
4371
+ catch (e) {
4372
+ die(e.message);
4373
+ }
4374
+ return;
4375
+ }
4151
4376
  if (kind === "relay") {
4152
4377
  const data = resolveDataDir(flag("data")).dir;
4153
4378
  const port = flag("port", "8787");
@@ -4184,12 +4409,30 @@ async function main() {
4184
4409
  if (flag("remind"))
4185
4410
  extraN.push("--remind", flag("remind"));
4186
4411
  console.error(`${B("installing human notifications as a background service")}`);
4187
- try {
4188
- const binary = ensureNativeNotifier();
4189
- console.error(D(` actionable HUD ${binary}`));
4412
+ if (process.platform === "linux") {
4413
+ const { linuxGraphicalSession } = await import("./host-env.js");
4414
+ if (!linuxGraphicalSession(process.env))
4415
+ console.error(D(" no graphical session in this shell (SSH or headless?) — alerts appear once you are logged in to the desktop; pantheon doctor says which way they are shown"));
4416
+ let lines = [];
4417
+ try {
4418
+ lines = installService(kind, homedir(), extraN);
4419
+ }
4420
+ catch (e) {
4421
+ die(e.message);
4422
+ }
4423
+ for (const l of lines)
4424
+ console.error(l);
4425
+ console.error(`\n${D("Alerts come from your desktop's own notifications. Done answers a decision from the alert; a click opens the board. No agent run or wake permission is involved.")}`);
4426
+ return;
4190
4427
  }
4191
- catch (e) {
4192
- die(`could not build the native notification helper: ${e.message}`);
4428
+ if (process.platform !== "win32") {
4429
+ try {
4430
+ const binary = ensureNativeNotifier();
4431
+ console.error(D(` actionable HUD ${binary}`));
4432
+ }
4433
+ catch (e) {
4434
+ die(`could not build the native notification helper: ${e.message}`);
4435
+ }
4193
4436
  }
4194
4437
  for (const l of installService(kind, homedir(), extraN))
4195
4438
  console.error(l);
@@ -4266,15 +4509,12 @@ async function main() {
4266
4509
  if (dirname(d) === d)
4267
4510
  break;
4268
4511
  }
4269
- const real = (d) => { try {
4270
- return realpathSync.native(d);
4271
- }
4272
- catch {
4273
- return resolve(d);
4274
- } };
4275
- const here = real(process.cwd());
4276
- if ((here === real(homedir()) || dirname(here) === here) && !argv.includes("--yes")) {
4277
- die(`this is ${dirname(here) === here ? "the root of the disk" : "your home folder"}: binding it puts every folder under it in "${m.name}", and an agent opened anywhere there would speak for that team.\n Bind a project folder instead, or if you mean it: pantheon use ${m.name} --here --yes`);
4512
+ const refused = folderRefusalHere(process.cwd());
4513
+ if (isTemporaryRefusal(refused))
4514
+ die(`${refused.why.replace("<team>", m.name)}.`);
4515
+ if (refused && ["root", "home", "above-home"].includes(refused.kind) && !argv.includes("--yes")) {
4516
+ const what = refused.kind === "root" ? "the root of the disk" : refused.kind === "home" ? "your home folder" : "a folder that holds your home folder";
4517
+ die(`this is ${what}: binding it puts every folder under it in "${m.name}", and an agent opened anywhere there would speak for that team.\n Bind a project folder instead, or if you mean it: pantheon use ${m.name} --here --yes`);
4278
4518
  }
4279
4519
  if ((await import("./repo-find.js")).holdsRepo(process.cwd())) {
4280
4520
  const n = await bindReposIn(process.cwd(), all, m, argv.includes("--yes"), `pantheon use ${m.name} --here --yes`);
@@ -5,6 +5,7 @@ import { chooseAgent, claudeSandboxable, NOT_FOR_UNATTENDED_RUNS, parseRepoComma
5
5
  import { detectClaudeFeatures } from "./claude-confine.js";
6
6
  import { loadPersonas } from "./persona.js";
7
7
  import { plistPath, existingUnitPath } from "./service.js";
8
+ import { readDefinition } from "./win-host.js";
8
9
  import { loadPolicy, unsandboxedAllowed } from "./worker-policy.js";
9
10
  export function workerServiceAllowsBash(plat = platform()) {
10
11
  try {
@@ -17,7 +18,7 @@ export function workerServiceAllowsBash(plat = platform()) {
17
18
  return args.includes("--allow-bash");
18
19
  }
19
20
  if (plat === "win32")
20
- return null;
21
+ return readDefinition("worker")?.args.includes("--allow-bash") ?? null;
21
22
  const u = existingUnitPath("worker");
22
23
  if (!u)
23
24
  return null;