create-egregore 0.19.5 → 0.19.6

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.
Files changed (130) hide show
  1. package/assets/egregore-launcher.js +407 -25
  2. package/bin/cli.js +34 -20
  3. package/lib/local.js +21 -5
  4. package/lib/pi-runtime.js +186 -0
  5. package/lib/setup.js +56 -11
  6. package/lib/ui.js +1 -1
  7. package/package.json +7 -5
  8. package/runtime/codex/.codex/spec-manifest.json +1 -1
  9. package/runtime/codex/AGENTS.md +7 -2
  10. package/runtime/codex/bin/codex-render-spec.mjs +6 -1
  11. package/runtime/codex/bin/codex-session-start.sh +5 -59
  12. package/runtime/codex/bin/codex-skill-render.mjs +7 -5
  13. package/runtime/codex/bin/lib/context.sh +8 -1
  14. package/runtime/codex/bin/lib/greeting.sh +12 -108
  15. package/runtime/codex/bin/lib/metrics.sh +374 -0
  16. package/runtime/codex/bin/publish-artifact.sh +14 -0
  17. package/runtime/codex/bin/settings.sh +274 -0
  18. package/runtime/codex/manifest.json +2 -0
  19. package/runtime/pi/.codex/config.toml +15 -0
  20. package/runtime/pi/.codex/hooks/branch-guard.js +242 -0
  21. package/runtime/pi/.codex/hooks/search-hint.js +65 -0
  22. package/runtime/pi/.codex/hooks.json +30 -0
  23. package/runtime/pi/.codex/skills/activity/SKILL.md +70 -0
  24. package/runtime/pi/.codex/skills/add/SKILL.md +25 -0
  25. package/runtime/pi/.codex/skills/announce/SKILL.md +54 -0
  26. package/runtime/pi/.codex/skills/answer/SKILL.md +25 -0
  27. package/runtime/pi/.codex/skills/archive/SKILL.md +41 -0
  28. package/runtime/pi/.codex/skills/ask/SKILL.md +59 -0
  29. package/runtime/pi/.codex/skills/branch/SKILL.md +25 -0
  30. package/runtime/pi/.codex/skills/checkup/SKILL.md +41 -0
  31. package/runtime/pi/.codex/skills/commit/SKILL.md +25 -0
  32. package/runtime/pi/.codex/skills/contribute/SKILL.md +25 -0
  33. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +25 -0
  34. package/runtime/pi/.codex/skills/dashboard/SKILL.md +59 -0
  35. package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +74 -0
  36. package/runtime/pi/.codex/skills/delete-user/SKILL.md +25 -0
  37. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +25 -0
  38. package/runtime/pi/.codex/skills/emissary/SKILL.md +41 -0
  39. package/runtime/pi/.codex/skills/env/SKILL.md +25 -0
  40. package/runtime/pi/.codex/skills/handoff/SKILL.md +67 -0
  41. package/runtime/pi/.codex/skills/harvest/SKILL.md +107 -0
  42. package/runtime/pi/.codex/skills/ingest/SKILL.md +25 -0
  43. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +25 -0
  44. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  45. package/runtime/pi/.codex/skills/invite/SKILL.md +40 -0
  46. package/runtime/pi/.codex/skills/issue/SKILL.md +41 -0
  47. package/runtime/pi/.codex/skills/me/SKILL.md +25 -0
  48. package/runtime/pi/.codex/skills/meeting/SKILL.md +41 -0
  49. package/runtime/pi/.codex/skills/minion/SKILL.md +25 -0
  50. package/runtime/pi/.codex/skills/note/SKILL.md +25 -0
  51. package/runtime/pi/.codex/skills/onboarding/SKILL.md +41 -0
  52. package/runtime/pi/.codex/skills/pr/SKILL.md +25 -0
  53. package/runtime/pi/.codex/skills/project/SKILL.md +41 -0
  54. package/runtime/pi/.codex/skills/pull/SKILL.md +25 -0
  55. package/runtime/pi/.codex/skills/push/SKILL.md +25 -0
  56. package/runtime/pi/.codex/skills/quest/SKILL.md +77 -0
  57. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +25 -0
  58. package/runtime/pi/.codex/skills/reflect/SKILL.md +41 -0
  59. package/runtime/pi/.codex/skills/review-pr/SKILL.md +41 -0
  60. package/runtime/pi/.codex/skills/save/SKILL.md +62 -0
  61. package/runtime/pi/.codex/skills/scroll/SKILL.md +168 -0
  62. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +4 -0
  63. package/runtime/pi/.codex/skills/scroll/assets/shell.html +542 -0
  64. package/runtime/pi/.codex/skills/search/SKILL.md +56 -0
  65. package/runtime/pi/.codex/skills/session-view/SKILL.md +25 -0
  66. package/runtime/pi/.codex/skills/setup/SKILL.md +25 -0
  67. package/runtime/pi/.codex/skills/summon/SKILL.md +41 -0
  68. package/runtime/pi/.codex/skills/sync-repos/SKILL.md +25 -0
  69. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +25 -0
  70. package/runtime/pi/.codex/skills/telemetry/SKILL.md +25 -0
  71. package/runtime/pi/.codex/skills/test/SKILL.md +41 -0
  72. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +84 -0
  73. package/runtime/pi/.codex/skills/thread/SKILL.md +25 -0
  74. package/runtime/pi/.codex/skills/todo/SKILL.md +41 -0
  75. package/runtime/pi/.codex/skills/tui-design/SKILL.md +41 -0
  76. package/runtime/pi/.codex/skills/tutorial/SKILL.md +41 -0
  77. package/runtime/pi/.codex/skills/update/SKILL.md +25 -0
  78. package/runtime/pi/.codex/skills/view/SKILL.md +156 -0
  79. package/runtime/pi/.codex/skills/view/agents/openai.yaml +4 -0
  80. package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +221 -0
  81. package/runtime/pi/.codex/skills/visual-explain/SKILL.md +41 -0
  82. package/runtime/pi/.codex/skills/wrap/SKILL.md +55 -0
  83. package/runtime/pi/.codex/spec-manifest.json +114 -0
  84. package/runtime/pi/.pi/APPEND_SYSTEM.md +255 -0
  85. package/runtime/pi/.pi/extensions/egregore.ts +324 -0
  86. package/runtime/pi/.pi/settings.json +6 -0
  87. package/runtime/pi/.pi/spec-manifest.json +12 -0
  88. package/runtime/pi/AGENTS.md +350 -0
  89. package/runtime/pi/bin/activity-data.sh +242 -0
  90. package/runtime/pi/bin/agent.sh +677 -0
  91. package/runtime/pi/bin/codex-install-skills.sh +78 -0
  92. package/runtime/pi/bin/codex-render-spec.mjs +394 -0
  93. package/runtime/pi/bin/codex-session-start.sh +188 -0
  94. package/runtime/pi/bin/codex-skill-render.mjs +323 -0
  95. package/runtime/pi/bin/codex-sync-skills.sh +227 -0
  96. package/runtime/pi/bin/dashboard-data.sh +178 -0
  97. package/runtime/pi/bin/graph-batch.sh +60 -0
  98. package/runtime/pi/bin/graph-op.sh +431 -0
  99. package/runtime/pi/bin/graph-wal.sh +228 -0
  100. package/runtime/pi/bin/graph.sh +177 -0
  101. package/runtime/pi/bin/handoff-pr-backfill.sh +86 -0
  102. package/runtime/pi/bin/handoff-run.sh +581 -0
  103. package/runtime/pi/bin/handoff-save-egregore.sh +136 -0
  104. package/runtime/pi/bin/index-handoff.sh +312 -0
  105. package/runtime/pi/bin/lib/artifact-id.sh +46 -0
  106. package/runtime/pi/bin/lib/config.sh +71 -0
  107. package/runtime/pi/bin/lib/context.sh +542 -0
  108. package/runtime/pi/bin/lib/dashboard-artifact.sh +142 -0
  109. package/runtime/pi/bin/lib/ensure-develop.sh +183 -0
  110. package/runtime/pi/bin/lib/git-sync.sh +288 -0
  111. package/runtime/pi/bin/lib/greeting.sh +340 -0
  112. package/runtime/pi/bin/lib/hash.sh +13 -0
  113. package/runtime/pi/bin/lib/identity.sh +174 -0
  114. package/runtime/pi/bin/lib/metrics.sh +374 -0
  115. package/runtime/pi/bin/lib/permissions.sh +106 -0
  116. package/runtime/pi/bin/lib/time.sh +56 -0
  117. package/runtime/pi/bin/lib/worktree-links.sh +54 -0
  118. package/runtime/pi/bin/notify.sh +207 -0
  119. package/runtime/pi/bin/pi-branch-policy.mjs +15 -0
  120. package/runtime/pi/bin/pi-product-workflows.mjs +324 -0
  121. package/runtime/pi/bin/pi-render-spec.mjs +114 -0
  122. package/runtime/pi/bin/pi-session-start.sh +24 -0
  123. package/runtime/pi/bin/publish-artifact.sh +201 -0
  124. package/runtime/pi/bin/publish-references.sh +144 -0
  125. package/runtime/pi/bin/repo-state.sh +99 -0
  126. package/runtime/pi/bin/telemetry.sh +391 -0
  127. package/runtime/pi/bin/worktree-create.sh +118 -0
  128. package/runtime/pi/bin/worktree-remove.sh +50 -0
  129. package/runtime/pi/bin/worktree.sh +198 -0
  130. package/runtime/pi/manifest.json +176 -0
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // egregore-launcher-version: 0.6.0
2
+ // egregore-launcher-version: 0.7.1
3
3
  //
4
4
  // egregore — launch and browse Egregore instances from your terminal.
5
5
  // Installed by create-egregore. Reads ~/.egregore/instances.json.
@@ -14,6 +14,7 @@
14
14
  // egregore ls | list list instances (non-interactive table)
15
15
  // egregore use <slug> set the default for bare `egregore`
16
16
  // egregore /invite <user> invite a GitHub user to an instance
17
+ // egregore settings [slug] hosting on/off, managed repos, admins (s in browser)
17
18
  // egregore prune [--dry-run] remove entries whose path is gone
18
19
  // egregore rm <slug|path> remove a specific entry
19
20
  // egregore browse force the full-screen browser
@@ -30,7 +31,7 @@ const https = require("node:https");
30
31
  const { spawnSync, spawn } = require("node:child_process");
31
32
  const readline = require("node:readline");
32
33
 
33
- const VERSION = "0.6.0";
34
+ const VERSION = "0.7.1";
34
35
  const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
35
36
  const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
36
37
  const PUBLIC_NPX_PACKAGE = "create-egregore";
@@ -345,6 +346,7 @@ function normalizeRuntime(raw) {
345
346
  if (!value) return null;
346
347
  if (value === "claude" || value === "claude-code" || value === "claude_code") return "claude-code";
347
348
  if (value === "codex" || value === "openai-codex" || value === "openai_codex") return "codex";
349
+ if (value === "pi" || value === "pi-coding-agent" || value === "pi_coding_agent") return "pi";
348
350
  return null;
349
351
  }
350
352
 
@@ -355,6 +357,8 @@ function parseRuntimeFlags(argv) {
355
357
  const arg = argv[i];
356
358
  if (arg === "--codex") {
357
359
  runtimeOverride = "codex";
360
+ } else if (arg === "--pi") {
361
+ runtimeOverride = "pi";
358
362
  } else if (arg === "--claude" || arg === "--claude-code") {
359
363
  runtimeOverride = "claude-code";
360
364
  } else if (arg === "--runtime") {
@@ -368,7 +372,7 @@ function parseRuntimeFlags(argv) {
368
372
  }
369
373
  }
370
374
  const normalized = runtimeOverride ? normalizeRuntime(runtimeOverride) : null;
371
- if (runtimeOverride && !normalized) die(`unknown runtime: ${runtimeOverride}\n expected claude-code or codex`);
375
+ if (runtimeOverride && !normalized) die(`unknown runtime: ${runtimeOverride}\n expected claude-code, codex, or pi`);
372
376
  return { args: rest, runtimeOverride: normalized };
373
377
  }
374
378
 
@@ -385,21 +389,32 @@ function runtimeFor(inst, runtimeOverride) {
385
389
  }
386
390
 
387
391
  function runtimeLabel(runtime) {
388
- return runtime === "codex" ? "Codex" : "Claude Code";
392
+ if (runtime === "codex") return "Codex";
393
+ if (runtime === "pi") return "Pi";
394
+ return "Claude Code";
389
395
  }
390
396
 
391
397
  function runtimeShort(runtime) {
392
- return runtime === "codex" ? "codex" : "claude-code";
398
+ return runtime === "codex" ? "codex" : runtime === "pi" ? "pi" : "claude-code";
393
399
  }
394
400
 
395
401
  function runtimeBinary(runtime) {
396
- return runtime === "codex" ? "codex" : "claude";
402
+ return runtime === "codex" ? "codex" : runtime === "pi" ? "pi" : "claude";
397
403
  }
398
404
 
399
405
  function assistantInstallCommand(runtime) {
400
- return runtime === "codex"
401
- ? "npm install -g @openai/codex"
402
- : "npm install -g @anthropic-ai/claude-code";
406
+ if (runtime === "codex") return "npm install -g @openai/codex";
407
+ if (runtime === "pi") return "npm install -g --ignore-scripts @earendil-works/pi-coding-agent";
408
+ return "npm install -g @anthropic-ai/claude-code";
409
+ }
410
+
411
+ function piTrustArgs() {
412
+ try {
413
+ const help = spawnSync("pi", ["--help"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
414
+ return `${help.stdout || ""}\n${help.stderr || ""}`.includes("--approve") ? ["--approve"] : [];
415
+ } catch {
416
+ return [];
417
+ }
403
418
  }
404
419
 
405
420
  function commandExists(bin) {
@@ -410,13 +425,12 @@ function runtimeChoiceDescription(runtime, inst, stored) {
410
425
  const installed = commandExists(runtimeBinary(runtime));
411
426
  const bits = [];
412
427
  if (!installed) {
413
- return runtime === "codex"
414
- ? "not installed · install Codex, then run egregore"
415
- : "not installed · install Claude Code, then run egregore";
428
+ return `not installed · install ${runtimeLabel(runtime)}, then run egregore`;
416
429
  }
417
430
  bits.push("installed");
418
431
  if (runtime === stored) bits.push("saved default");
419
432
  if (runtime === "codex") bits.push("portable agent protocol");
433
+ else if (runtime === "pi") bits.push("project-local meta-harness");
420
434
  else bits.push("Claude Code integration");
421
435
  return bits.join(" · ");
422
436
  }
@@ -439,7 +453,7 @@ function runtimePickerLines(inst, runtimes, stored, cursor) {
439
453
  lines.push(` ${marker} ${BOLD}${label}${RESET} ${DIM}${desc}${RESET}`);
440
454
  }
441
455
  // Coming-soon harnesses — shown for visibility, not selectable yet.
442
- for (const soon of ["Hermes", "Pi"]) {
456
+ for (const soon of ["Hermes"]) {
443
457
  lines.push(` ${DIM}· ${pad(soon, labelWidth)} coming soon${RESET}`);
444
458
  }
445
459
  lines.push("");
@@ -529,9 +543,7 @@ function runtimeChoicesFor(inst, runtimeOverride) {
529
543
  const override = normalizeRuntime(runtimeOverride);
530
544
  if (override) return [override];
531
545
  const stored = storedRuntimeFor(inst);
532
- const choices = stored === "codex"
533
- ? ["codex", "claude-code"]
534
- : ["claude-code", "codex"];
546
+ const choices = [stored, ...["claude-code", "codex", "pi"].filter((runtime) => runtime !== stored)];
535
547
  return choices.sort((a, b) => Number(commandExists(runtimeBinary(b))) - Number(commandExists(runtimeBinary(a))));
536
548
  }
537
549
 
@@ -637,6 +649,25 @@ function hasCodexCoreSupport(egregoreDir) {
637
649
  );
638
650
  }
639
651
 
652
+ function hasPiCoreSupport(egregoreDir) {
653
+ return (
654
+ fs.existsSync(path.join(egregoreDir, "bin", "pi-session-start.sh")) &&
655
+ fs.existsSync(path.join(egregoreDir, ".pi", "APPEND_SYSTEM.md")) &&
656
+ fs.existsSync(path.join(egregoreDir, ".pi", "settings.json")) &&
657
+ fs.existsSync(path.join(egregoreDir, ".pi", "extensions", "egregore.ts")) &&
658
+ fs.existsSync(path.join(egregoreDir, ".codex", "skills"))
659
+ );
660
+ }
661
+
662
+ function piSupportMessage(egregoreDir) {
663
+ return [
664
+ "Pi meta-harness support is missing from this Egregore checkout.",
665
+ ` path: ${egregoreDir}`,
666
+ "",
667
+ "Run npx -y create-egregore@latest to install the packaged Pi runtime, then rerun egregore.",
668
+ ].join("\n");
669
+ }
670
+
640
671
  function installCodexSkills(egregoreDir) {
641
672
  const script = path.join(egregoreDir, "bin", "codex-install-skills.sh");
642
673
  if (!fs.existsSync(script)) return false;
@@ -662,6 +693,7 @@ function runtimeLaunchArgs(runtime, egregoreDir) {
662
693
  "--cd", egregoreDir,
663
694
  ];
664
695
  }
696
+ if (runtime === "pi") return piTrustArgs();
665
697
  return ["start"];
666
698
  }
667
699
 
@@ -695,6 +727,8 @@ async function launch(inst, runtimeOverride) {
695
727
  if (!skillsInstalled || !cardRendered) {
696
728
  die(codexSupportMessage(inst.path));
697
729
  }
730
+ } else if (runtime === "pi") {
731
+ if (!hasPiCoreSupport(inst.path)) die(piSupportMessage(inst.path));
698
732
  }
699
733
  // Replace the current process with the selected assistant in the instance dir.
700
734
  // spawn + detached + exit-on-exit replicates exec() in Node.
@@ -822,10 +856,12 @@ function cmdHelp() {
822
856
  ` ${CYAN}egregore${RESET} pick an instance, then pick a harness`,
823
857
  ` ${CYAN}egregore${RESET} ${DIM}<slug|name>${RESET} pick a harness for the matching instance`,
824
858
  ` ${CYAN}egregore --runtime${RESET} ${DIM}<name> <slug>${RESET} advanced harness override`,
859
+ ` ${DIM}names: claude-code · codex · pi${RESET}`,
825
860
  ` ${CYAN}egregore browse${RESET} force the full-screen browser`,
826
861
  ` ${CYAN}egregore ls${RESET} ${DIM}| list${RESET} list installed instances`,
827
862
  ` ${CYAN}egregore use${RESET} ${DIM}<slug>${RESET} set the default for bare ${BOLD}egregore${RESET}`,
828
863
  ` ${CYAN}egregore /invite${RESET} ${DIM}<user> [slug]${RESET} invite a GitHub user`,
864
+ ` ${CYAN}egregore settings${RESET} ${DIM}[slug]${RESET} hosting on/off, repos, admins`,
829
865
  ` ${CYAN}egregore prune${RESET} ${DIM}[--dry-run]${RESET} remove entries with missing paths`,
830
866
  ` ${CYAN}egregore rm${RESET} ${DIM}<slug|path>${RESET} remove a specific entry`,
831
867
  ``,
@@ -836,6 +872,7 @@ function cmdHelp() {
836
872
  ` ${DIM}n / +${RESET} create a new Egregore`,
837
873
  ` ${DIM}u${RESET} set selected as default`,
838
874
  ` ${DIM}i${RESET} invite a GitHub user to selected instance`,
875
+ ` ${DIM}s${RESET} settings (hosting on/off, repos, admins)`,
839
876
  ` ${DIM}d${RESET} delete selected entry (offers cleanup of ${RED}✗${RESET} missing ones)`,
840
877
  ` ${DIM}?${RESET} toggle help overlay`,
841
878
  ` ${DIM}q${RESET} quit browser (esc goes back — it never quits)`,
@@ -1251,6 +1288,343 @@ async function cmdInvite(args) {
1251
1288
  return inviteToInstance(inst, username);
1252
1289
  }
1253
1290
 
1291
+ // ── settings ───────────────────────────────────────────────────────
1292
+ //
1293
+ // The launcher stays a THIN caller: every config change shells out to the
1294
+ // instance's deterministic `bin/settings.sh` verbs (one source of truth, no
1295
+ // JS reimplementation to drift from the script). This is the reliability point
1296
+ // — the correctness lives in the tested script; the screen below just drives it.
1297
+
1298
+ function settingsScriptPath(inst) {
1299
+ return path.join(inst.path, "bin", "settings.sh");
1300
+ }
1301
+
1302
+ function runSettings(inst, args) {
1303
+ const script = settingsScriptPath(inst);
1304
+ if (!fs.existsSync(script)) {
1305
+ return { status: 127, stdout: "", stderr: "bin/settings.sh not found — update this egregore (run /pull) to get settings.\n", missing: true };
1306
+ }
1307
+ try {
1308
+ return spawnSync("bash", [script, ...args], { cwd: inst.path, stdio: "pipe", encoding: "utf-8", timeout: 15000 });
1309
+ } catch (err) {
1310
+ return { status: 1, stdout: "", stderr: String(err) };
1311
+ }
1312
+ }
1313
+
1314
+ function settingsDump(inst) {
1315
+ const r = runSettings(inst, ["dump"]);
1316
+ if (!r || r.status !== 0) return null;
1317
+ try { return JSON.parse(r.stdout); } catch { return null; }
1318
+ }
1319
+
1320
+ // Nested raw-mode list editor for a domain (repo | admin | people): hover an
1321
+ // item and press `d` to remove it, `a` to open an inline add field, esc to go
1322
+ // back. Mirrors the main browser's affordances. Its own raw-mode loop; resolves
1323
+ // when the user backs out. Each mutation is ONE deterministic settings.sh verb;
1324
+ // validation, the last-admin guard, and GitHub-access notes surface as notices.
1325
+ function listEditor(inst, domain, label) {
1326
+ const fieldFor = (d) => {
1327
+ if (domain === "repo") return (d.repos || []).map((r) => r.name || r);
1328
+ if (domain === "admin") return d.admins || [];
1329
+ return d.people || [];
1330
+ };
1331
+ const noun = domain === "repo" ? "repo name" : "github handle";
1332
+
1333
+ return new Promise((resolve) => {
1334
+ let items = fieldFor(settingsDump(inst) || {});
1335
+ let cursor = 0;
1336
+ let mode = "browse"; // browse | add | confirm
1337
+ let input = "";
1338
+ let notice = "";
1339
+ let rendered = 0;
1340
+ let done = false;
1341
+ const previousRaw = !!process.stdin.isRaw;
1342
+ const isMouseEvent = makeMouseFilter();
1343
+
1344
+ const refresh = () => {
1345
+ items = fieldFor(settingsDump(inst) || {});
1346
+ if (cursor >= items.length) cursor = Math.max(0, items.length - 1);
1347
+ };
1348
+
1349
+ const erase = () => {
1350
+ if (!rendered) return;
1351
+ process.stdout.write(`\x1b[${rendered}A` + ANSI.clearDown);
1352
+ rendered = 0;
1353
+ };
1354
+
1355
+ const buildLines = () => {
1356
+ const out = [];
1357
+ out.push(``);
1358
+ out.push(` ${BOLD}${label}${RESET}`);
1359
+ out.push(` ${DIM}────────────────────────────────────────${RESET}`);
1360
+ if (items.length === 0) {
1361
+ out.push(` ${DIM}(none)${RESET}`);
1362
+ } else {
1363
+ items.forEach((it, i) => {
1364
+ const sel = i === cursor;
1365
+ out.push(` ${sel ? CYAN + "❯" + RESET : " "} ${sel ? BOLD + it + RESET : it}`);
1366
+ });
1367
+ }
1368
+ out.push(``);
1369
+ if (notice) out.push(` ${notice}`);
1370
+ if (mode === "add") {
1371
+ out.push(` ${CYAN}add ${noun}:${RESET} ${input}${CYAN}▌${RESET} ${DIM}enter save · esc cancel${RESET}`);
1372
+ } else if (mode === "confirm") {
1373
+ out.push(` ${YELLOW}remove ${items[cursor]}?${RESET} ${DIM}y / N${RESET}`);
1374
+ } else {
1375
+ out.push(` ${DIM}↑↓ nav · ${RESET}${BOLD}a${RESET}${DIM} add · ${RESET}${BOLD}d${RESET}${DIM} delete · esc back${RESET}`);
1376
+ }
1377
+ return out;
1378
+ };
1379
+
1380
+ const render = () => {
1381
+ erase();
1382
+ const lines = buildLines();
1383
+ rendered = lines.length;
1384
+ process.stdout.write(lines.map((l) => l + ANSI.clearLine).join("\n") + "\n");
1385
+ };
1386
+
1387
+ const cleanup = () => {
1388
+ if (done) return;
1389
+ done = true;
1390
+ process.stdin.removeListener("keypress", onKey);
1391
+ process.stdout.write(ANSI.mouseOff);
1392
+ if (process.stdin.setRawMode) process.stdin.setRawMode(previousRaw);
1393
+ if (!previousRaw) process.stdin.pause();
1394
+ erase();
1395
+ };
1396
+
1397
+ // Run a verb (spawnSync blocks briefly — people ops hit GitHub). Surface the
1398
+ // last stdout/stderr line as a notice; refresh the list from disk after.
1399
+ const runVerb = (args) => {
1400
+ const r = runSettings(inst, args);
1401
+ const msg = ((r.stdout || "") + " " + (r.stderr || "")).trim().split("\n").map((s) => s.trim()).filter(Boolean).pop() || "";
1402
+ notice = r && r.status === 0 ? msg : `${YELLOW}${msg}${RESET}`;
1403
+ refresh();
1404
+ };
1405
+
1406
+ function onKey(chunk, key = {}) {
1407
+ if (isMouseEvent(chunk, key)) return;
1408
+ if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
1409
+
1410
+ if (mode === "add") {
1411
+ if (key.name === "escape") { mode = "browse"; input = ""; render(); return; }
1412
+ if (key.name === "return") {
1413
+ const v = input.trim().split(/\s+/)[0];
1414
+ mode = "browse"; input = "";
1415
+ if (v) runVerb([domain, "add", v]);
1416
+ render();
1417
+ return;
1418
+ }
1419
+ if (key.name === "backspace") { input = input.slice(0, -1); render(); return; }
1420
+ if (chunk && chunk.length === 1 && chunk >= " " && !key.ctrl && !key.meta) { input += chunk; render(); return; }
1421
+ return;
1422
+ }
1423
+
1424
+ if (mode === "confirm") {
1425
+ if (chunk === "y" || chunk === "Y") {
1426
+ const victim = items[cursor];
1427
+ mode = "browse";
1428
+ if (victim) runVerb([domain, "remove", victim]);
1429
+ } else { mode = "browse"; }
1430
+ render();
1431
+ return;
1432
+ }
1433
+
1434
+ // browse
1435
+ if (key.name === "escape" || key.name === "left") { cleanup(); resolve(); return; }
1436
+ if (key.name === "up" || key.name === "k") { if (items.length) cursor = (cursor - 1 + items.length) % items.length; render(); return; }
1437
+ if (key.name === "down" || key.name === "j") { if (items.length) cursor = (cursor + 1) % items.length; render(); return; }
1438
+ if (chunk === "a") { mode = "add"; input = ""; notice = ""; render(); return; }
1439
+ if (chunk === "d") { if (items.length) { mode = "confirm"; render(); } return; }
1440
+ }
1441
+
1442
+ readline.emitKeypressEvents(process.stdin);
1443
+ if (process.stdin.setRawMode) process.stdin.setRawMode(true);
1444
+ process.stdin.resume();
1445
+ process.stdout.write(ANSI.mouseOff);
1446
+ process.stdin.on("keypress", onKey);
1447
+ render();
1448
+ });
1449
+ }
1450
+
1451
+ // The `s`-key settings screen: a raw-mode menu (hosting toggle + repo/admin
1452
+ // editors) over bin/settings.sh. Modeled on pickRuntimeInteractive.
1453
+ function settingsScreen(inst) {
1454
+ const title = inst.slug || inst.name || path.basename(inst.path);
1455
+
1456
+ if (!fs.existsSync(settingsScriptPath(inst))) {
1457
+ console.log(`\n ${YELLOW}Settings unavailable${RESET} — ${BOLD}${title}${RESET} has no ${DIM}bin/settings.sh${RESET}.`);
1458
+ console.log(` Update it: open the egregore and run ${CYAN}/pull${RESET}.\n`);
1459
+ return Promise.resolve();
1460
+ }
1461
+
1462
+ let data = settingsDump(inst) || { hosting: true, repos: [], admins: [], people: [] };
1463
+
1464
+ const rows = [
1465
+ {
1466
+ label: () => `Hosted Artifacts`,
1467
+ value: () => (data.hosting ? `${GREEN}ON${RESET}` : `${YELLOW}OFF${RESET}`),
1468
+ detailLines: () => [data.hosting
1469
+ ? ` ${DIM}Enter toggles. When OFF, artifacts never upload to egregore.xyz.${RESET}`
1470
+ : ` ${DIM}Artifacts stay local. /view still renders them on localhost.${RESET}`],
1471
+ },
1472
+ {
1473
+ label: () => `Managed repos`,
1474
+ value: () => `${DIM}${data.repos.length}${RESET}`,
1475
+ detailLines: () => data.repos.length
1476
+ ? data.repos.map((r) => ` ${DIM}·${RESET} ${r.name || r}`)
1477
+ : [` ${DIM}(none)${RESET}`],
1478
+ },
1479
+ {
1480
+ label: () => `Admins`,
1481
+ value: () => `${DIM}${data.admins.length}${RESET}`,
1482
+ detailLines: () => data.admins.length
1483
+ ? [` ${DIM}${data.admins.join(", ")}${RESET}`]
1484
+ : [` ${DIM}(none)${RESET}`],
1485
+ },
1486
+ {
1487
+ label: () => `People`,
1488
+ value: () => `${DIM}${(data.people || []).length}${RESET}`,
1489
+ detailLines: () => (data.people && data.people.length)
1490
+ ? [` ${DIM}${data.people.join(", ")}${RESET}`]
1491
+ : [` ${DIM}(none)${RESET}`],
1492
+ },
1493
+ ];
1494
+
1495
+ return new Promise((resolve) => {
1496
+ let cursor = 0;
1497
+ let rendered = 0;
1498
+ let done = false;
1499
+ let notice = "";
1500
+ const previousRaw = !!process.stdin.isRaw;
1501
+ const isMouseEvent = makeMouseFilter();
1502
+
1503
+ const erase = () => {
1504
+ if (!rendered) return;
1505
+ // Move to the TOP of the last-rendered block, then wipe everything below
1506
+ // in one shot. Clearing line-by-line leaves the cursor at the BOTTOM, so
1507
+ // the next redraw starts a row lower each time — that's the drift that
1508
+ // pushes blank "margins" on top. Up-N + clearDown returns to the top and
1509
+ // clears cleanly, so every render repaints in place.
1510
+ process.stdout.write(`\x1b[${rendered}A` + ANSI.clearDown);
1511
+ rendered = 0;
1512
+ };
1513
+
1514
+ const buildLines = () => {
1515
+ const out = [];
1516
+ out.push(``);
1517
+ out.push(` ${BOLD}Settings${RESET} ${DIM}·${RESET} ${BOLD}${title}${RESET}`);
1518
+ out.push(` ${DIM}────────────────────────────────────────${RESET}`);
1519
+ rows.forEach((row, i) => {
1520
+ const sel = i === cursor;
1521
+ const marker = sel ? `${CYAN}❯${RESET}` : ` `;
1522
+ const name = sel ? `${BOLD}${row.label()}${RESET}` : row.label();
1523
+ out.push(` ${marker} ${name} ${row.value()}`);
1524
+ if (sel) for (const dl of row.detailLines()) out.push(dl);
1525
+ });
1526
+ out.push(``);
1527
+ if (notice) out.push(` ${notice}`);
1528
+ out.push(` ${DIM}↑↓ nav · enter ${cursor === 0 ? "toggle" : "edit"} · esc back · q quit${RESET}`);
1529
+ return out;
1530
+ };
1531
+
1532
+ const render = () => {
1533
+ erase();
1534
+ const lines = buildLines();
1535
+ rendered = lines.length;
1536
+ process.stdout.write(lines.map((l) => l + ANSI.clearLine).join("\n") + "\n");
1537
+ };
1538
+
1539
+ const teardownRaw = () => {
1540
+ process.stdin.removeListener("keypress", onKey);
1541
+ process.stdout.write(ANSI.mouseOff);
1542
+ if (process.stdin.setRawMode) process.stdin.setRawMode(previousRaw);
1543
+ if (!previousRaw) process.stdin.pause();
1544
+ };
1545
+
1546
+ const setupRaw = () => {
1547
+ readline.emitKeypressEvents(process.stdin);
1548
+ if (process.stdin.setRawMode) process.stdin.setRawMode(true);
1549
+ process.stdin.resume();
1550
+ process.stdout.write(ANSI.mouseOff);
1551
+ process.stdin.on("keypress", onKey);
1552
+ };
1553
+
1554
+ const cleanup = () => {
1555
+ if (done) return;
1556
+ done = true;
1557
+ teardownRaw();
1558
+ erase();
1559
+ };
1560
+
1561
+ const toggleHosting = () => {
1562
+ const r = runSettings(inst, ["hosting", data.hosting ? "off" : "on"]);
1563
+ if (!r || r.status !== 0) {
1564
+ notice = `${YELLOW}${((r && r.stderr) || "could not change hosting").trim().split("\n")[0]}${RESET}`;
1565
+ } else {
1566
+ data = settingsDump(inst) || data;
1567
+ notice = `hosting ${data.hosting ? `${GREEN}ON${RESET}` : `${YELLOW}OFF${RESET}`}`;
1568
+ }
1569
+ render();
1570
+ };
1571
+
1572
+ // Hand off to the nested raw-mode list editor, then repaint this screen with
1573
+ // refreshed counts when the user backs out.
1574
+ const openList = (domain, label) => {
1575
+ erase();
1576
+ teardownRaw();
1577
+ listEditor(inst, domain, label).then(() => {
1578
+ data = settingsDump(inst) || data;
1579
+ notice = "";
1580
+ setupRaw();
1581
+ render();
1582
+ });
1583
+ };
1584
+
1585
+ function onKey(chunk, key = {}) {
1586
+ if (isMouseEvent(chunk, key)) return;
1587
+ if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
1588
+ if (key.name === "q") { cleanup(); resolve(); return; }
1589
+ // Esc / ← = back (esc never quits).
1590
+ if (key.name === "escape" || key.name === "left") { cleanup(); resolve(); return; }
1591
+ if (key.name === "up" || key.name === "k") { cursor = (cursor - 1 + rows.length) % rows.length; render(); return; }
1592
+ if (key.name === "down" || key.name === "j") { cursor = (cursor + 1) % rows.length; render(); return; }
1593
+ if (key.name === "return" || chunk === " ") {
1594
+ if (cursor === 0) toggleHosting();
1595
+ else if (cursor === 1) openList("repo", "Managed repos");
1596
+ else if (cursor === 2) openList("admin", "Admins");
1597
+ else if (cursor === 3) openList("people", "People");
1598
+ }
1599
+ }
1600
+
1601
+ setupRaw();
1602
+ render();
1603
+ });
1604
+ }
1605
+
1606
+ async function cmdSettings(args) {
1607
+ const registry = readRegistry();
1608
+ if (registry.length === 0) die("no egregores installed yet.\n run: npx -y create-egregore@latest");
1609
+ const DOMAINS = new Set(["hosting", "repo", "admin", "people", "dump"]);
1610
+ let rest = args.slice();
1611
+ let slug = "";
1612
+ if (rest.length && !DOMAINS.has(rest[0])) { slug = rest[0]; rest = rest.slice(1); }
1613
+ const inst = resolveInviteInstance(registry, slug);
1614
+ if (!inst) {
1615
+ die(`multiple egregores installed; choose one:\n egregore settings <slug> [hosting|repo|admin]\n run: egregore ls`);
1616
+ }
1617
+ // No verb + a TTY → interactive screen. Otherwise forward straight to the verb.
1618
+ if (rest.length === 0) {
1619
+ if (isTTY && canPrompt()) return settingsScreen(inst);
1620
+ rest = ["dump"];
1621
+ }
1622
+ const r = runSettings(inst, rest);
1623
+ if (r.stdout) process.stdout.write(r.stdout.endsWith("\n") ? r.stdout : r.stdout + "\n");
1624
+ if (r.stderr) process.stderr.write(r.stderr);
1625
+ process.exitCode = r.status || 0;
1626
+ }
1627
+
1254
1628
  // ── tiny sync confirm (non-TUI flows) ───────────────────────────────
1255
1629
 
1256
1630
  function syncConfirm(question) {
@@ -1370,7 +1744,7 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
1370
1744
  ? `filter /${filter}`
1371
1745
  : "press ? for help";
1372
1746
  const canShowBigLogo = bodyWidth >= Math.max(...EGREGORE_ART.map((line) => line.length)) && height >= 24;
1373
- const skillStatus = `Codex ${nativeCount} native · ${adapterCount} adapter · ${skillsCount} total · Claude ${claudeCount}`;
1747
+ const skillStatus = `Codex ${nativeCount} native · ${adapterCount} adapter · Pi ${skillsCount} shared · Claude ${claudeCount}`;
1374
1748
  const skillGroups = [
1375
1749
  `${DIM}core${RESET}: ${workflows}`,
1376
1750
  `${DIM}team${RESET}: ${joinShort(["/handoff", "/ask", "/invite", "/quest"], Math.max(16, bodyWidth - 8))}`,
@@ -1411,8 +1785,8 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
1411
1785
  `${BOLD}Harnesses${RESET}`,
1412
1786
  `${DIM}claude-code${RESET}: ${commandExists("claude") ? "installed" : "not installed"}`,
1413
1787
  `${DIM}codex${RESET}: ${commandExists("codex") ? "installed" : "not installed"}`,
1788
+ `${DIM}pi${RESET}: ${commandExists("pi") ? "installed" : "not installed"}`,
1414
1789
  `${DIM}Hermes — Coming Soon${RESET}`,
1415
- `${DIM}Pi — Coming Soon${RESET}`,
1416
1790
  ];
1417
1791
  const rows = Math.max(EGREGORE_SIGIL.length + 2, right.length);
1418
1792
  for (let i = 0; i < rows; i++) {
@@ -1424,11 +1798,11 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
1424
1798
  lines.push(boxLine(`${BOLD}Available Skills${RESET}`, innerWidth, margin));
1425
1799
  for (const group of skillGroups) lines.push(boxLine(group, innerWidth, margin));
1426
1800
  lines.push(emptyBoxLine(innerWidth, margin));
1427
- lines.push(boxLine(`${BOLD}Harnesses${RESET} ${DIM}claude-code${RESET} ${commandExists("claude") ? "installed" : "missing"} · ${DIM}codex${RESET} ${commandExists("codex") ? "installed" : "missing"} · ${DIM}Hermes/Pi — Coming Soon${RESET}`, innerWidth, margin));
1801
+ lines.push(boxLine(`${BOLD}Harnesses${RESET} ${DIM}claude-code${RESET} ${commandExists("claude") ? "installed" : "missing"} · ${DIM}codex${RESET} ${commandExists("codex") ? "installed" : "missing"} · ${DIM}pi${RESET} ${commandExists("pi") ? "installed" : "missing"} · ${DIM}Hermes — Coming Soon${RESET}`, innerWidth, margin));
1428
1802
  }
1429
1803
 
1430
1804
  lines.push(emptyBoxLine(innerWidth, margin));
1431
- lines.push(boxLine(`${DIM}enter selects an instance, then choose Claude Code or Codex without leaving this screen${RESET}`, innerWidth, margin));
1805
+ lines.push(boxLine(`${DIM}enter selects an instance, then choose Claude Code, Codex, or Pi without leaving this screen${RESET}`, innerWidth, margin));
1432
1806
  lines.push(bottom);
1433
1807
  return lines;
1434
1808
  }
@@ -1461,7 +1835,7 @@ function renderRuntimePanel({ inst, cursor, width, height }) {
1461
1835
  lines.push(boxLine(`${marker} ${pad(labelText, 16)} ${DIM}${desc}${RESET}`, innerWidth, margin));
1462
1836
  }
1463
1837
  // Coming-soon harnesses — shown for visibility, not selectable yet.
1464
- for (const soon of ["Hermes", "Pi"]) {
1838
+ for (const soon of ["Hermes"]) {
1465
1839
  lines.push(boxLine(`${DIM}· ${pad(soon, 16)} coming soon${RESET}`, innerWidth, margin));
1466
1840
  }
1467
1841
 
@@ -1674,7 +2048,7 @@ async function browser(args = {}) {
1674
2048
  const label = (k) => `${DIM}${pad(k, 10)}${RESET}`;
1675
2049
  previewLines.push(` ${label("action")} ${BOLD}Set up a new Egregore${RESET}`);
1676
2050
  previewLines.push(` ${label("flow")} guided install, Telegram, invite, ready/start`);
1677
- previewLines.push(` ${label("harness")} choose Claude Code or Codex during setup`);
2051
+ previewLines.push(` ${label("harness")} choose Claude Code, Codex, or Pi during setup`);
1678
2052
  previewLines.push(` ${label("start")} press enter to begin`);
1679
2053
  } else if (!selected) {
1680
2054
  previewLines.push(`${DIM} (no selection)${RESET}`);
@@ -1697,7 +2071,7 @@ async function browser(args = {}) {
1697
2071
  if (pv.skills) {
1698
2072
  const skillLine = selectedWorkflowLine(pv, Math.max(16, blockWidth - 20));
1699
2073
  previewLines.push(` ${label("invoke")} ${skillLine}`);
1700
- previewLines.push(` ${label("skills")} Codex ${BOLD}${pv.skills.native.length}${RESET} native, ${BOLD}${pv.skills.adapterCount}${RESET} adapters; Claude ${BOLD}${pv.skills.claudeCount || 0}${RESET}`);
2074
+ previewLines.push(` ${label("skills")} Codex ${BOLD}${pv.skills.native.length}${RESET} native + ${BOLD}${pv.skills.adapterCount}${RESET} adapters; Pi ${BOLD}${pv.skills.names.length}${RESET} shared; Claude ${BOLD}${pv.skills.claudeCount || 0}${RESET}`);
1701
2075
  }
1702
2076
  const modeBits = [];
1703
2077
  modeBits.push(runtimeShort(runtimeFor(selected, args.runtimeOverride)));
@@ -1757,8 +2131,8 @@ async function browser(args = {}) {
1757
2131
  footer = band(`● ${stripAnsi(state.message)}`, bg);
1758
2132
  } else {
1759
2133
  footer = blockWidth < 96
1760
- ? band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("i", "invite")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46)
1761
- : band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("/", "filter")} ${key("i", "invite")} ${key("u", "default")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46);
2134
+ ? band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("i", "invite")} ${key("s", "settings")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46)
2135
+ : band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("/", "filter")} ${key("i", "invite")} ${key("s", "settings")} ${key("u", "default")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46);
1762
2136
  }
1763
2137
  out.push(M + footer + "\n");
1764
2138
  if (state.showHelp) {
@@ -2056,6 +2430,13 @@ async function browser(args = {}) {
2056
2430
  render();
2057
2431
  return;
2058
2432
  }
2433
+ if (chunk === "s") {
2434
+ const pick = visible()[state.cursor]?.e;
2435
+ if (!pick) return;
2436
+ cleanup();
2437
+ resolve(settingsScreen(pick));
2438
+ return;
2439
+ }
2059
2440
  if (chunk === "u") {
2060
2441
  const pick = visible()[state.cursor]?.e;
2061
2442
  if (!pick) return;
@@ -2174,6 +2555,7 @@ function main() {
2174
2555
  if (cmd === "ls" || cmd === "list") return cmdLs();
2175
2556
  if (cmd === "use") return cmdUse(args[1]);
2176
2557
  if (cmd === "/invite" || cmd === "invite") return cmdInvite(args.slice(1));
2558
+ if (cmd === "settings" || cmd === "config") return cmdSettings(args.slice(1));
2177
2559
  if (cmd === "prune") return cmdPruneNonInteractive(args[1] === "--dry-run" || args[1] === "-n");
2178
2560
  if (cmd === "rm" || cmd === "remove") return cmdRmNonInteractive(args[1]);
2179
2561
  if (cmd === "browse") return browser({ force: true, runtimeOverride });