create-egregore 0.19.3 → 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 (154) hide show
  1. package/assets/egregore-launcher.js +410 -26
  2. package/bin/cli.js +34 -20
  3. package/lib/local.js +84 -11
  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 +8 -6
  8. package/runtime/codex/.codex/hooks/search-hint.js +65 -0
  9. package/runtime/codex/.codex/hooks.json +13 -0
  10. package/runtime/codex/.codex/skills/deep-reflect/SKILL.md +14 -10
  11. package/runtime/codex/.codex/skills/minion/SKILL.md +25 -0
  12. package/runtime/codex/.codex/skills/scroll/SKILL.md +168 -0
  13. package/runtime/codex/.codex/skills/scroll/agents/openai.yaml +4 -0
  14. package/runtime/codex/.codex/skills/scroll/assets/shell.html +542 -0
  15. package/runtime/codex/.codex/skills/search/SKILL.md +56 -0
  16. package/runtime/codex/.codex/skills/session-view/SKILL.md +25 -0
  17. package/runtime/codex/.codex/skills/thread/SKILL.md +25 -0
  18. package/runtime/codex/.codex/skills/view/SKILL.md +147 -32
  19. package/runtime/codex/.codex/skills/view/agents/openai.yaml +4 -0
  20. package/runtime/codex/.codex/skills/view/assets/compose-scaffold.html +221 -0
  21. package/runtime/codex/.codex/spec-manifest.json +4 -4
  22. package/runtime/codex/AGENTS.md +31 -5
  23. package/runtime/codex/bin/activity-data.sh +46 -4
  24. package/runtime/codex/bin/agent.sh +10 -2
  25. package/runtime/codex/bin/codex-install-skills.sh +1 -1
  26. package/runtime/codex/bin/codex-render-spec.mjs +24 -2
  27. package/runtime/codex/bin/codex-session-start.sh +6 -60
  28. package/runtime/codex/bin/codex-skill-render.mjs +20 -12
  29. package/runtime/codex/bin/codex-sync-skills.sh +4 -0
  30. package/runtime/codex/bin/graph-op.sh +56 -5
  31. package/runtime/codex/bin/graph.sh +36 -0
  32. package/runtime/codex/bin/handoff-run.sh +157 -7
  33. package/runtime/codex/bin/handoff-save-egregore.sh +6 -1
  34. package/runtime/codex/bin/index-handoff.sh +9 -3
  35. package/runtime/codex/bin/lib/context.sh +97 -18
  36. package/runtime/codex/bin/lib/dashboard-artifact.sh +1 -1
  37. package/runtime/codex/bin/lib/git-sync.sh +35 -9
  38. package/runtime/codex/bin/lib/greeting.sh +30 -104
  39. package/runtime/codex/bin/lib/metrics.sh +374 -0
  40. package/runtime/codex/bin/publish-artifact.sh +43 -1
  41. package/runtime/codex/bin/settings.sh +274 -0
  42. package/runtime/codex/manifest.json +18 -1
  43. package/runtime/pi/.codex/config.toml +15 -0
  44. package/runtime/pi/.codex/hooks/branch-guard.js +242 -0
  45. package/runtime/pi/.codex/hooks/search-hint.js +65 -0
  46. package/runtime/pi/.codex/hooks.json +30 -0
  47. package/runtime/pi/.codex/skills/activity/SKILL.md +70 -0
  48. package/runtime/pi/.codex/skills/add/SKILL.md +25 -0
  49. package/runtime/pi/.codex/skills/announce/SKILL.md +54 -0
  50. package/runtime/pi/.codex/skills/answer/SKILL.md +25 -0
  51. package/runtime/pi/.codex/skills/archive/SKILL.md +41 -0
  52. package/runtime/pi/.codex/skills/ask/SKILL.md +59 -0
  53. package/runtime/pi/.codex/skills/branch/SKILL.md +25 -0
  54. package/runtime/pi/.codex/skills/checkup/SKILL.md +41 -0
  55. package/runtime/pi/.codex/skills/commit/SKILL.md +25 -0
  56. package/runtime/pi/.codex/skills/contribute/SKILL.md +25 -0
  57. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +25 -0
  58. package/runtime/pi/.codex/skills/dashboard/SKILL.md +59 -0
  59. package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +74 -0
  60. package/runtime/pi/.codex/skills/delete-user/SKILL.md +25 -0
  61. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +25 -0
  62. package/runtime/pi/.codex/skills/emissary/SKILL.md +41 -0
  63. package/runtime/pi/.codex/skills/env/SKILL.md +25 -0
  64. package/runtime/pi/.codex/skills/handoff/SKILL.md +67 -0
  65. package/runtime/pi/.codex/skills/harvest/SKILL.md +107 -0
  66. package/runtime/pi/.codex/skills/ingest/SKILL.md +25 -0
  67. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +25 -0
  68. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  69. package/runtime/pi/.codex/skills/invite/SKILL.md +40 -0
  70. package/runtime/pi/.codex/skills/issue/SKILL.md +41 -0
  71. package/runtime/pi/.codex/skills/me/SKILL.md +25 -0
  72. package/runtime/pi/.codex/skills/meeting/SKILL.md +41 -0
  73. package/runtime/pi/.codex/skills/minion/SKILL.md +25 -0
  74. package/runtime/pi/.codex/skills/note/SKILL.md +25 -0
  75. package/runtime/pi/.codex/skills/onboarding/SKILL.md +41 -0
  76. package/runtime/pi/.codex/skills/pr/SKILL.md +25 -0
  77. package/runtime/pi/.codex/skills/project/SKILL.md +41 -0
  78. package/runtime/pi/.codex/skills/pull/SKILL.md +25 -0
  79. package/runtime/pi/.codex/skills/push/SKILL.md +25 -0
  80. package/runtime/pi/.codex/skills/quest/SKILL.md +77 -0
  81. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +25 -0
  82. package/runtime/pi/.codex/skills/reflect/SKILL.md +41 -0
  83. package/runtime/pi/.codex/skills/review-pr/SKILL.md +41 -0
  84. package/runtime/pi/.codex/skills/save/SKILL.md +62 -0
  85. package/runtime/pi/.codex/skills/scroll/SKILL.md +168 -0
  86. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +4 -0
  87. package/runtime/pi/.codex/skills/scroll/assets/shell.html +542 -0
  88. package/runtime/pi/.codex/skills/search/SKILL.md +56 -0
  89. package/runtime/pi/.codex/skills/session-view/SKILL.md +25 -0
  90. package/runtime/pi/.codex/skills/setup/SKILL.md +25 -0
  91. package/runtime/pi/.codex/skills/summon/SKILL.md +41 -0
  92. package/runtime/pi/.codex/skills/sync-repos/SKILL.md +25 -0
  93. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +25 -0
  94. package/runtime/pi/.codex/skills/telemetry/SKILL.md +25 -0
  95. package/runtime/pi/.codex/skills/test/SKILL.md +41 -0
  96. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +84 -0
  97. package/runtime/pi/.codex/skills/thread/SKILL.md +25 -0
  98. package/runtime/pi/.codex/skills/todo/SKILL.md +41 -0
  99. package/runtime/pi/.codex/skills/tui-design/SKILL.md +41 -0
  100. package/runtime/pi/.codex/skills/tutorial/SKILL.md +41 -0
  101. package/runtime/pi/.codex/skills/update/SKILL.md +25 -0
  102. package/runtime/pi/.codex/skills/view/SKILL.md +156 -0
  103. package/runtime/pi/.codex/skills/view/agents/openai.yaml +4 -0
  104. package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +221 -0
  105. package/runtime/pi/.codex/skills/visual-explain/SKILL.md +41 -0
  106. package/runtime/pi/.codex/skills/wrap/SKILL.md +55 -0
  107. package/runtime/pi/.codex/spec-manifest.json +114 -0
  108. package/runtime/pi/.pi/APPEND_SYSTEM.md +255 -0
  109. package/runtime/pi/.pi/extensions/egregore.ts +324 -0
  110. package/runtime/pi/.pi/settings.json +6 -0
  111. package/runtime/pi/.pi/spec-manifest.json +12 -0
  112. package/runtime/pi/AGENTS.md +350 -0
  113. package/runtime/pi/bin/activity-data.sh +242 -0
  114. package/runtime/pi/bin/agent.sh +677 -0
  115. package/runtime/pi/bin/codex-install-skills.sh +78 -0
  116. package/runtime/pi/bin/codex-render-spec.mjs +394 -0
  117. package/runtime/pi/bin/codex-session-start.sh +188 -0
  118. package/runtime/pi/bin/codex-skill-render.mjs +323 -0
  119. package/runtime/pi/bin/codex-sync-skills.sh +227 -0
  120. package/runtime/pi/bin/dashboard-data.sh +178 -0
  121. package/runtime/pi/bin/graph-batch.sh +60 -0
  122. package/runtime/pi/bin/graph-op.sh +431 -0
  123. package/runtime/pi/bin/graph-wal.sh +228 -0
  124. package/runtime/pi/bin/graph.sh +177 -0
  125. package/runtime/pi/bin/handoff-pr-backfill.sh +86 -0
  126. package/runtime/pi/bin/handoff-run.sh +581 -0
  127. package/runtime/pi/bin/handoff-save-egregore.sh +136 -0
  128. package/runtime/pi/bin/index-handoff.sh +312 -0
  129. package/runtime/pi/bin/lib/artifact-id.sh +46 -0
  130. package/runtime/pi/bin/lib/config.sh +71 -0
  131. package/runtime/pi/bin/lib/context.sh +542 -0
  132. package/runtime/pi/bin/lib/dashboard-artifact.sh +142 -0
  133. package/runtime/pi/bin/lib/ensure-develop.sh +183 -0
  134. package/runtime/pi/bin/lib/git-sync.sh +288 -0
  135. package/runtime/pi/bin/lib/greeting.sh +340 -0
  136. package/runtime/pi/bin/lib/hash.sh +13 -0
  137. package/runtime/pi/bin/lib/identity.sh +174 -0
  138. package/runtime/pi/bin/lib/metrics.sh +374 -0
  139. package/runtime/pi/bin/lib/permissions.sh +106 -0
  140. package/runtime/pi/bin/lib/time.sh +56 -0
  141. package/runtime/pi/bin/lib/worktree-links.sh +54 -0
  142. package/runtime/pi/bin/notify.sh +207 -0
  143. package/runtime/pi/bin/pi-branch-policy.mjs +15 -0
  144. package/runtime/pi/bin/pi-product-workflows.mjs +324 -0
  145. package/runtime/pi/bin/pi-render-spec.mjs +114 -0
  146. package/runtime/pi/bin/pi-session-start.sh +24 -0
  147. package/runtime/pi/bin/publish-artifact.sh +201 -0
  148. package/runtime/pi/bin/publish-references.sh +144 -0
  149. package/runtime/pi/bin/repo-state.sh +99 -0
  150. package/runtime/pi/bin/telemetry.sh +391 -0
  151. package/runtime/pi/bin/worktree-create.sh +118 -0
  152. package/runtime/pi/bin/worktree-remove.sh +50 -0
  153. package/runtime/pi/bin/worktree.sh +198 -0
  154. 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";
@@ -48,6 +49,8 @@ const NATIVE_CODEX_SKILLS = [
48
49
  "invite",
49
50
  "ask",
50
51
  "save",
52
+ "view",
53
+ "scroll",
51
54
  ];
52
55
 
53
56
  const PRIMARY_CODEX_SKILLS = [
@@ -343,6 +346,7 @@ function normalizeRuntime(raw) {
343
346
  if (!value) return null;
344
347
  if (value === "claude" || value === "claude-code" || value === "claude_code") return "claude-code";
345
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";
346
350
  return null;
347
351
  }
348
352
 
@@ -353,6 +357,8 @@ function parseRuntimeFlags(argv) {
353
357
  const arg = argv[i];
354
358
  if (arg === "--codex") {
355
359
  runtimeOverride = "codex";
360
+ } else if (arg === "--pi") {
361
+ runtimeOverride = "pi";
356
362
  } else if (arg === "--claude" || arg === "--claude-code") {
357
363
  runtimeOverride = "claude-code";
358
364
  } else if (arg === "--runtime") {
@@ -366,7 +372,7 @@ function parseRuntimeFlags(argv) {
366
372
  }
367
373
  }
368
374
  const normalized = runtimeOverride ? normalizeRuntime(runtimeOverride) : null;
369
- 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`);
370
376
  return { args: rest, runtimeOverride: normalized };
371
377
  }
372
378
 
@@ -383,21 +389,32 @@ function runtimeFor(inst, runtimeOverride) {
383
389
  }
384
390
 
385
391
  function runtimeLabel(runtime) {
386
- return runtime === "codex" ? "Codex" : "Claude Code";
392
+ if (runtime === "codex") return "Codex";
393
+ if (runtime === "pi") return "Pi";
394
+ return "Claude Code";
387
395
  }
388
396
 
389
397
  function runtimeShort(runtime) {
390
- return runtime === "codex" ? "codex" : "claude-code";
398
+ return runtime === "codex" ? "codex" : runtime === "pi" ? "pi" : "claude-code";
391
399
  }
392
400
 
393
401
  function runtimeBinary(runtime) {
394
- return runtime === "codex" ? "codex" : "claude";
402
+ return runtime === "codex" ? "codex" : runtime === "pi" ? "pi" : "claude";
395
403
  }
396
404
 
397
405
  function assistantInstallCommand(runtime) {
398
- return runtime === "codex"
399
- ? "npm install -g @openai/codex"
400
- : "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
+ }
401
418
  }
402
419
 
403
420
  function commandExists(bin) {
@@ -408,13 +425,12 @@ function runtimeChoiceDescription(runtime, inst, stored) {
408
425
  const installed = commandExists(runtimeBinary(runtime));
409
426
  const bits = [];
410
427
  if (!installed) {
411
- return runtime === "codex"
412
- ? "not installed · install Codex, then run egregore"
413
- : "not installed · install Claude Code, then run egregore";
428
+ return `not installed · install ${runtimeLabel(runtime)}, then run egregore`;
414
429
  }
415
430
  bits.push("installed");
416
431
  if (runtime === stored) bits.push("saved default");
417
432
  if (runtime === "codex") bits.push("portable agent protocol");
433
+ else if (runtime === "pi") bits.push("project-local meta-harness");
418
434
  else bits.push("Claude Code integration");
419
435
  return bits.join(" · ");
420
436
  }
@@ -437,7 +453,7 @@ function runtimePickerLines(inst, runtimes, stored, cursor) {
437
453
  lines.push(` ${marker} ${BOLD}${label}${RESET} ${DIM}${desc}${RESET}`);
438
454
  }
439
455
  // Coming-soon harnesses — shown for visibility, not selectable yet.
440
- for (const soon of ["Hermes", "Pi"]) {
456
+ for (const soon of ["Hermes"]) {
441
457
  lines.push(` ${DIM}· ${pad(soon, labelWidth)} coming soon${RESET}`);
442
458
  }
443
459
  lines.push("");
@@ -527,9 +543,7 @@ function runtimeChoicesFor(inst, runtimeOverride) {
527
543
  const override = normalizeRuntime(runtimeOverride);
528
544
  if (override) return [override];
529
545
  const stored = storedRuntimeFor(inst);
530
- const choices = stored === "codex"
531
- ? ["codex", "claude-code"]
532
- : ["claude-code", "codex"];
546
+ const choices = [stored, ...["claude-code", "codex", "pi"].filter((runtime) => runtime !== stored)];
533
547
  return choices.sort((a, b) => Number(commandExists(runtimeBinary(b))) - Number(commandExists(runtimeBinary(a))));
534
548
  }
535
549
 
@@ -635,6 +649,25 @@ function hasCodexCoreSupport(egregoreDir) {
635
649
  );
636
650
  }
637
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
+
638
671
  function installCodexSkills(egregoreDir) {
639
672
  const script = path.join(egregoreDir, "bin", "codex-install-skills.sh");
640
673
  if (!fs.existsSync(script)) return false;
@@ -660,6 +693,7 @@ function runtimeLaunchArgs(runtime, egregoreDir) {
660
693
  "--cd", egregoreDir,
661
694
  ];
662
695
  }
696
+ if (runtime === "pi") return piTrustArgs();
663
697
  return ["start"];
664
698
  }
665
699
 
@@ -693,6 +727,8 @@ async function launch(inst, runtimeOverride) {
693
727
  if (!skillsInstalled || !cardRendered) {
694
728
  die(codexSupportMessage(inst.path));
695
729
  }
730
+ } else if (runtime === "pi") {
731
+ if (!hasPiCoreSupport(inst.path)) die(piSupportMessage(inst.path));
696
732
  }
697
733
  // Replace the current process with the selected assistant in the instance dir.
698
734
  // spawn + detached + exit-on-exit replicates exec() in Node.
@@ -820,10 +856,12 @@ function cmdHelp() {
820
856
  ` ${CYAN}egregore${RESET} pick an instance, then pick a harness`,
821
857
  ` ${CYAN}egregore${RESET} ${DIM}<slug|name>${RESET} pick a harness for the matching instance`,
822
858
  ` ${CYAN}egregore --runtime${RESET} ${DIM}<name> <slug>${RESET} advanced harness override`,
859
+ ` ${DIM}names: claude-code · codex · pi${RESET}`,
823
860
  ` ${CYAN}egregore browse${RESET} force the full-screen browser`,
824
861
  ` ${CYAN}egregore ls${RESET} ${DIM}| list${RESET} list installed instances`,
825
862
  ` ${CYAN}egregore use${RESET} ${DIM}<slug>${RESET} set the default for bare ${BOLD}egregore${RESET}`,
826
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`,
827
865
  ` ${CYAN}egregore prune${RESET} ${DIM}[--dry-run]${RESET} remove entries with missing paths`,
828
866
  ` ${CYAN}egregore rm${RESET} ${DIM}<slug|path>${RESET} remove a specific entry`,
829
867
  ``,
@@ -834,6 +872,7 @@ function cmdHelp() {
834
872
  ` ${DIM}n / +${RESET} create a new Egregore`,
835
873
  ` ${DIM}u${RESET} set selected as default`,
836
874
  ` ${DIM}i${RESET} invite a GitHub user to selected instance`,
875
+ ` ${DIM}s${RESET} settings (hosting on/off, repos, admins)`,
837
876
  ` ${DIM}d${RESET} delete selected entry (offers cleanup of ${RED}✗${RESET} missing ones)`,
838
877
  ` ${DIM}?${RESET} toggle help overlay`,
839
878
  ` ${DIM}q${RESET} quit browser (esc goes back — it never quits)`,
@@ -1249,6 +1288,343 @@ async function cmdInvite(args) {
1249
1288
  return inviteToInstance(inst, username);
1250
1289
  }
1251
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
+
1252
1628
  // ── tiny sync confirm (non-TUI flows) ───────────────────────────────
1253
1629
 
1254
1630
  function syncConfirm(question) {
@@ -1368,11 +1744,11 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
1368
1744
  ? `filter /${filter}`
1369
1745
  : "press ? for help";
1370
1746
  const canShowBigLogo = bodyWidth >= Math.max(...EGREGORE_ART.map((line) => line.length)) && height >= 24;
1371
- const skillStatus = `Codex ${nativeCount} native · ${adapterCount} adapter · ${skillsCount} total · Claude ${claudeCount}`;
1747
+ const skillStatus = `Codex ${nativeCount} native · ${adapterCount} adapter · Pi ${skillsCount} shared · Claude ${claudeCount}`;
1372
1748
  const skillGroups = [
1373
1749
  `${DIM}core${RESET}: ${workflows}`,
1374
1750
  `${DIM}team${RESET}: ${joinShort(["/handoff", "/ask", "/invite", "/quest"], Math.max(16, bodyWidth - 8))}`,
1375
- `${DIM}deeper${RESET}: ${joinShort(["/announce", "/harvest", "/the-spiral", "/deep-reflect"], Math.max(16, bodyWidth - 10))}`,
1751
+ `${DIM}deeper${RESET}: ${joinShort(["/announce", "/harvest", "/the-spiral", "/deep-reflect", "/view", "/scroll"], Math.max(16, bodyWidth - 10))}`,
1376
1752
  `${DIM}fallback${RESET}: ${adapterCount} adapter skills`,
1377
1753
  ];
1378
1754
 
@@ -1409,8 +1785,8 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
1409
1785
  `${BOLD}Harnesses${RESET}`,
1410
1786
  `${DIM}claude-code${RESET}: ${commandExists("claude") ? "installed" : "not installed"}`,
1411
1787
  `${DIM}codex${RESET}: ${commandExists("codex") ? "installed" : "not installed"}`,
1788
+ `${DIM}pi${RESET}: ${commandExists("pi") ? "installed" : "not installed"}`,
1412
1789
  `${DIM}Hermes — Coming Soon${RESET}`,
1413
- `${DIM}Pi — Coming Soon${RESET}`,
1414
1790
  ];
1415
1791
  const rows = Math.max(EGREGORE_SIGIL.length + 2, right.length);
1416
1792
  for (let i = 0; i < rows; i++) {
@@ -1422,11 +1798,11 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
1422
1798
  lines.push(boxLine(`${BOLD}Available Skills${RESET}`, innerWidth, margin));
1423
1799
  for (const group of skillGroups) lines.push(boxLine(group, innerWidth, margin));
1424
1800
  lines.push(emptyBoxLine(innerWidth, margin));
1425
- 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));
1426
1802
  }
1427
1803
 
1428
1804
  lines.push(emptyBoxLine(innerWidth, margin));
1429
- 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));
1430
1806
  lines.push(bottom);
1431
1807
  return lines;
1432
1808
  }
@@ -1459,7 +1835,7 @@ function renderRuntimePanel({ inst, cursor, width, height }) {
1459
1835
  lines.push(boxLine(`${marker} ${pad(labelText, 16)} ${DIM}${desc}${RESET}`, innerWidth, margin));
1460
1836
  }
1461
1837
  // Coming-soon harnesses — shown for visibility, not selectable yet.
1462
- for (const soon of ["Hermes", "Pi"]) {
1838
+ for (const soon of ["Hermes"]) {
1463
1839
  lines.push(boxLine(`${DIM}· ${pad(soon, 16)} coming soon${RESET}`, innerWidth, margin));
1464
1840
  }
1465
1841
 
@@ -1672,7 +2048,7 @@ async function browser(args = {}) {
1672
2048
  const label = (k) => `${DIM}${pad(k, 10)}${RESET}`;
1673
2049
  previewLines.push(` ${label("action")} ${BOLD}Set up a new Egregore${RESET}`);
1674
2050
  previewLines.push(` ${label("flow")} guided install, Telegram, invite, ready/start`);
1675
- previewLines.push(` ${label("harness")} choose Claude Code or Codex during setup`);
2051
+ previewLines.push(` ${label("harness")} choose Claude Code, Codex, or Pi during setup`);
1676
2052
  previewLines.push(` ${label("start")} press enter to begin`);
1677
2053
  } else if (!selected) {
1678
2054
  previewLines.push(`${DIM} (no selection)${RESET}`);
@@ -1695,7 +2071,7 @@ async function browser(args = {}) {
1695
2071
  if (pv.skills) {
1696
2072
  const skillLine = selectedWorkflowLine(pv, Math.max(16, blockWidth - 20));
1697
2073
  previewLines.push(` ${label("invoke")} ${skillLine}`);
1698
- 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}`);
1699
2075
  }
1700
2076
  const modeBits = [];
1701
2077
  modeBits.push(runtimeShort(runtimeFor(selected, args.runtimeOverride)));
@@ -1755,8 +2131,8 @@ async function browser(args = {}) {
1755
2131
  footer = band(`● ${stripAnsi(state.message)}`, bg);
1756
2132
  } else {
1757
2133
  footer = blockWidth < 96
1758
- ? band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("i", "invite")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46)
1759
- : 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);
1760
2136
  }
1761
2137
  out.push(M + footer + "\n");
1762
2138
  if (state.showHelp) {
@@ -2054,6 +2430,13 @@ async function browser(args = {}) {
2054
2430
  render();
2055
2431
  return;
2056
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
+ }
2057
2440
  if (chunk === "u") {
2058
2441
  const pick = visible()[state.cursor]?.e;
2059
2442
  if (!pick) return;
@@ -2172,6 +2555,7 @@ function main() {
2172
2555
  if (cmd === "ls" || cmd === "list") return cmdLs();
2173
2556
  if (cmd === "use") return cmdUse(args[1]);
2174
2557
  if (cmd === "/invite" || cmd === "invite") return cmdInvite(args.slice(1));
2558
+ if (cmd === "settings" || cmd === "config") return cmdSettings(args.slice(1));
2175
2559
  if (cmd === "prune") return cmdPruneNonInteractive(args[1] === "--dry-run" || args[1] === "-n");
2176
2560
  if (cmd === "rm" || cmd === "remove") return cmdRmNonInteractive(args[1]);
2177
2561
  if (cmd === "browse") return browser({ force: true, runtimeOverride });