create-egregore 0.19.5 → 0.19.7

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 +411 -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.2
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.2";
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";
@@ -263,6 +264,10 @@ function readRegistry() {
263
264
  for (const e of entries) {
264
265
  if (!e || typeof e.path !== "string" || !e.path) continue;
265
266
  const key = path.resolve(e.path);
267
+ // Worktrees live under a parent instance's .claude/worktrees/ — they are
268
+ // NOT their own egregore. A session that ran inside one may have registered
269
+ // its cwd; keep those out of the instance list (browser, ls, default).
270
+ if (key.includes("/.claude/worktrees/")) continue;
266
271
  if (seen.has(key)) continue;
267
272
  seen.add(key);
268
273
  result.push(e);
@@ -345,6 +350,7 @@ function normalizeRuntime(raw) {
345
350
  if (!value) return null;
346
351
  if (value === "claude" || value === "claude-code" || value === "claude_code") return "claude-code";
347
352
  if (value === "codex" || value === "openai-codex" || value === "openai_codex") return "codex";
353
+ if (value === "pi" || value === "pi-coding-agent" || value === "pi_coding_agent") return "pi";
348
354
  return null;
349
355
  }
350
356
 
@@ -355,6 +361,8 @@ function parseRuntimeFlags(argv) {
355
361
  const arg = argv[i];
356
362
  if (arg === "--codex") {
357
363
  runtimeOverride = "codex";
364
+ } else if (arg === "--pi") {
365
+ runtimeOverride = "pi";
358
366
  } else if (arg === "--claude" || arg === "--claude-code") {
359
367
  runtimeOverride = "claude-code";
360
368
  } else if (arg === "--runtime") {
@@ -368,7 +376,7 @@ function parseRuntimeFlags(argv) {
368
376
  }
369
377
  }
370
378
  const normalized = runtimeOverride ? normalizeRuntime(runtimeOverride) : null;
371
- if (runtimeOverride && !normalized) die(`unknown runtime: ${runtimeOverride}\n expected claude-code or codex`);
379
+ if (runtimeOverride && !normalized) die(`unknown runtime: ${runtimeOverride}\n expected claude-code, codex, or pi`);
372
380
  return { args: rest, runtimeOverride: normalized };
373
381
  }
374
382
 
@@ -385,21 +393,32 @@ function runtimeFor(inst, runtimeOverride) {
385
393
  }
386
394
 
387
395
  function runtimeLabel(runtime) {
388
- return runtime === "codex" ? "Codex" : "Claude Code";
396
+ if (runtime === "codex") return "Codex";
397
+ if (runtime === "pi") return "Pi";
398
+ return "Claude Code";
389
399
  }
390
400
 
391
401
  function runtimeShort(runtime) {
392
- return runtime === "codex" ? "codex" : "claude-code";
402
+ return runtime === "codex" ? "codex" : runtime === "pi" ? "pi" : "claude-code";
393
403
  }
394
404
 
395
405
  function runtimeBinary(runtime) {
396
- return runtime === "codex" ? "codex" : "claude";
406
+ return runtime === "codex" ? "codex" : runtime === "pi" ? "pi" : "claude";
397
407
  }
398
408
 
399
409
  function assistantInstallCommand(runtime) {
400
- return runtime === "codex"
401
- ? "npm install -g @openai/codex"
402
- : "npm install -g @anthropic-ai/claude-code";
410
+ if (runtime === "codex") return "npm install -g @openai/codex";
411
+ if (runtime === "pi") return "npm install -g --ignore-scripts @earendil-works/pi-coding-agent";
412
+ return "npm install -g @anthropic-ai/claude-code";
413
+ }
414
+
415
+ function piTrustArgs() {
416
+ try {
417
+ const help = spawnSync("pi", ["--help"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
418
+ return `${help.stdout || ""}\n${help.stderr || ""}`.includes("--approve") ? ["--approve"] : [];
419
+ } catch {
420
+ return [];
421
+ }
403
422
  }
404
423
 
405
424
  function commandExists(bin) {
@@ -410,13 +429,12 @@ function runtimeChoiceDescription(runtime, inst, stored) {
410
429
  const installed = commandExists(runtimeBinary(runtime));
411
430
  const bits = [];
412
431
  if (!installed) {
413
- return runtime === "codex"
414
- ? "not installed · install Codex, then run egregore"
415
- : "not installed · install Claude Code, then run egregore";
432
+ return `not installed · install ${runtimeLabel(runtime)}, then run egregore`;
416
433
  }
417
434
  bits.push("installed");
418
435
  if (runtime === stored) bits.push("saved default");
419
436
  if (runtime === "codex") bits.push("portable agent protocol");
437
+ else if (runtime === "pi") bits.push("project-local meta-harness");
420
438
  else bits.push("Claude Code integration");
421
439
  return bits.join(" · ");
422
440
  }
@@ -439,7 +457,7 @@ function runtimePickerLines(inst, runtimes, stored, cursor) {
439
457
  lines.push(` ${marker} ${BOLD}${label}${RESET} ${DIM}${desc}${RESET}`);
440
458
  }
441
459
  // Coming-soon harnesses — shown for visibility, not selectable yet.
442
- for (const soon of ["Hermes", "Pi"]) {
460
+ for (const soon of ["Hermes"]) {
443
461
  lines.push(` ${DIM}· ${pad(soon, labelWidth)} coming soon${RESET}`);
444
462
  }
445
463
  lines.push("");
@@ -529,9 +547,7 @@ function runtimeChoicesFor(inst, runtimeOverride) {
529
547
  const override = normalizeRuntime(runtimeOverride);
530
548
  if (override) return [override];
531
549
  const stored = storedRuntimeFor(inst);
532
- const choices = stored === "codex"
533
- ? ["codex", "claude-code"]
534
- : ["claude-code", "codex"];
550
+ const choices = [stored, ...["claude-code", "codex", "pi"].filter((runtime) => runtime !== stored)];
535
551
  return choices.sort((a, b) => Number(commandExists(runtimeBinary(b))) - Number(commandExists(runtimeBinary(a))));
536
552
  }
537
553
 
@@ -637,6 +653,25 @@ function hasCodexCoreSupport(egregoreDir) {
637
653
  );
638
654
  }
639
655
 
656
+ function hasPiCoreSupport(egregoreDir) {
657
+ return (
658
+ fs.existsSync(path.join(egregoreDir, "bin", "pi-session-start.sh")) &&
659
+ fs.existsSync(path.join(egregoreDir, ".pi", "APPEND_SYSTEM.md")) &&
660
+ fs.existsSync(path.join(egregoreDir, ".pi", "settings.json")) &&
661
+ fs.existsSync(path.join(egregoreDir, ".pi", "extensions", "egregore.ts")) &&
662
+ fs.existsSync(path.join(egregoreDir, ".codex", "skills"))
663
+ );
664
+ }
665
+
666
+ function piSupportMessage(egregoreDir) {
667
+ return [
668
+ "Pi meta-harness support is missing from this Egregore checkout.",
669
+ ` path: ${egregoreDir}`,
670
+ "",
671
+ "Run npx -y create-egregore@latest to install the packaged Pi runtime, then rerun egregore.",
672
+ ].join("\n");
673
+ }
674
+
640
675
  function installCodexSkills(egregoreDir) {
641
676
  const script = path.join(egregoreDir, "bin", "codex-install-skills.sh");
642
677
  if (!fs.existsSync(script)) return false;
@@ -662,6 +697,7 @@ function runtimeLaunchArgs(runtime, egregoreDir) {
662
697
  "--cd", egregoreDir,
663
698
  ];
664
699
  }
700
+ if (runtime === "pi") return piTrustArgs();
665
701
  return ["start"];
666
702
  }
667
703
 
@@ -695,6 +731,8 @@ async function launch(inst, runtimeOverride) {
695
731
  if (!skillsInstalled || !cardRendered) {
696
732
  die(codexSupportMessage(inst.path));
697
733
  }
734
+ } else if (runtime === "pi") {
735
+ if (!hasPiCoreSupport(inst.path)) die(piSupportMessage(inst.path));
698
736
  }
699
737
  // Replace the current process with the selected assistant in the instance dir.
700
738
  // spawn + detached + exit-on-exit replicates exec() in Node.
@@ -822,10 +860,12 @@ function cmdHelp() {
822
860
  ` ${CYAN}egregore${RESET} pick an instance, then pick a harness`,
823
861
  ` ${CYAN}egregore${RESET} ${DIM}<slug|name>${RESET} pick a harness for the matching instance`,
824
862
  ` ${CYAN}egregore --runtime${RESET} ${DIM}<name> <slug>${RESET} advanced harness override`,
863
+ ` ${DIM}names: claude-code · codex · pi${RESET}`,
825
864
  ` ${CYAN}egregore browse${RESET} force the full-screen browser`,
826
865
  ` ${CYAN}egregore ls${RESET} ${DIM}| list${RESET} list installed instances`,
827
866
  ` ${CYAN}egregore use${RESET} ${DIM}<slug>${RESET} set the default for bare ${BOLD}egregore${RESET}`,
828
867
  ` ${CYAN}egregore /invite${RESET} ${DIM}<user> [slug]${RESET} invite a GitHub user`,
868
+ ` ${CYAN}egregore settings${RESET} ${DIM}[slug]${RESET} hosting on/off, repos, admins`,
829
869
  ` ${CYAN}egregore prune${RESET} ${DIM}[--dry-run]${RESET} remove entries with missing paths`,
830
870
  ` ${CYAN}egregore rm${RESET} ${DIM}<slug|path>${RESET} remove a specific entry`,
831
871
  ``,
@@ -836,6 +876,7 @@ function cmdHelp() {
836
876
  ` ${DIM}n / +${RESET} create a new Egregore`,
837
877
  ` ${DIM}u${RESET} set selected as default`,
838
878
  ` ${DIM}i${RESET} invite a GitHub user to selected instance`,
879
+ ` ${DIM}s${RESET} settings (hosting on/off, repos, admins)`,
839
880
  ` ${DIM}d${RESET} delete selected entry (offers cleanup of ${RED}✗${RESET} missing ones)`,
840
881
  ` ${DIM}?${RESET} toggle help overlay`,
841
882
  ` ${DIM}q${RESET} quit browser (esc goes back — it never quits)`,
@@ -1251,6 +1292,343 @@ async function cmdInvite(args) {
1251
1292
  return inviteToInstance(inst, username);
1252
1293
  }
1253
1294
 
1295
+ // ── settings ───────────────────────────────────────────────────────
1296
+ //
1297
+ // The launcher stays a THIN caller: every config change shells out to the
1298
+ // instance's deterministic `bin/settings.sh` verbs (one source of truth, no
1299
+ // JS reimplementation to drift from the script). This is the reliability point
1300
+ // — the correctness lives in the tested script; the screen below just drives it.
1301
+
1302
+ function settingsScriptPath(inst) {
1303
+ return path.join(inst.path, "bin", "settings.sh");
1304
+ }
1305
+
1306
+ function runSettings(inst, args) {
1307
+ const script = settingsScriptPath(inst);
1308
+ if (!fs.existsSync(script)) {
1309
+ return { status: 127, stdout: "", stderr: "bin/settings.sh not found — update this egregore (run /pull) to get settings.\n", missing: true };
1310
+ }
1311
+ try {
1312
+ return spawnSync("bash", [script, ...args], { cwd: inst.path, stdio: "pipe", encoding: "utf-8", timeout: 15000 });
1313
+ } catch (err) {
1314
+ return { status: 1, stdout: "", stderr: String(err) };
1315
+ }
1316
+ }
1317
+
1318
+ function settingsDump(inst) {
1319
+ const r = runSettings(inst, ["dump"]);
1320
+ if (!r || r.status !== 0) return null;
1321
+ try { return JSON.parse(r.stdout); } catch { return null; }
1322
+ }
1323
+
1324
+ // Nested raw-mode list editor for a domain (repo | admin | people): hover an
1325
+ // item and press `d` to remove it, `a` to open an inline add field, esc to go
1326
+ // back. Mirrors the main browser's affordances. Its own raw-mode loop; resolves
1327
+ // when the user backs out. Each mutation is ONE deterministic settings.sh verb;
1328
+ // validation, the last-admin guard, and GitHub-access notes surface as notices.
1329
+ function listEditor(inst, domain, label) {
1330
+ const fieldFor = (d) => {
1331
+ if (domain === "repo") return (d.repos || []).map((r) => r.name || r);
1332
+ if (domain === "admin") return d.admins || [];
1333
+ return d.people || [];
1334
+ };
1335
+ const noun = domain === "repo" ? "repo name" : "github handle";
1336
+
1337
+ return new Promise((resolve) => {
1338
+ let items = fieldFor(settingsDump(inst) || {});
1339
+ let cursor = 0;
1340
+ let mode = "browse"; // browse | add | confirm
1341
+ let input = "";
1342
+ let notice = "";
1343
+ let rendered = 0;
1344
+ let done = false;
1345
+ const previousRaw = !!process.stdin.isRaw;
1346
+ const isMouseEvent = makeMouseFilter();
1347
+
1348
+ const refresh = () => {
1349
+ items = fieldFor(settingsDump(inst) || {});
1350
+ if (cursor >= items.length) cursor = Math.max(0, items.length - 1);
1351
+ };
1352
+
1353
+ const erase = () => {
1354
+ if (!rendered) return;
1355
+ process.stdout.write(`\x1b[${rendered}A` + ANSI.clearDown);
1356
+ rendered = 0;
1357
+ };
1358
+
1359
+ const buildLines = () => {
1360
+ const out = [];
1361
+ out.push(``);
1362
+ out.push(` ${BOLD}${label}${RESET}`);
1363
+ out.push(` ${DIM}────────────────────────────────────────${RESET}`);
1364
+ if (items.length === 0) {
1365
+ out.push(` ${DIM}(none)${RESET}`);
1366
+ } else {
1367
+ items.forEach((it, i) => {
1368
+ const sel = i === cursor;
1369
+ out.push(` ${sel ? CYAN + "❯" + RESET : " "} ${sel ? BOLD + it + RESET : it}`);
1370
+ });
1371
+ }
1372
+ out.push(``);
1373
+ if (notice) out.push(` ${notice}`);
1374
+ if (mode === "add") {
1375
+ out.push(` ${CYAN}add ${noun}:${RESET} ${input}${CYAN}▌${RESET} ${DIM}enter save · esc cancel${RESET}`);
1376
+ } else if (mode === "confirm") {
1377
+ out.push(` ${YELLOW}remove ${items[cursor]}?${RESET} ${DIM}y / N${RESET}`);
1378
+ } else {
1379
+ out.push(` ${DIM}↑↓ nav · ${RESET}${BOLD}a${RESET}${DIM} add · ${RESET}${BOLD}d${RESET}${DIM} delete · esc back${RESET}`);
1380
+ }
1381
+ return out;
1382
+ };
1383
+
1384
+ const render = () => {
1385
+ erase();
1386
+ const lines = buildLines();
1387
+ rendered = lines.length;
1388
+ process.stdout.write(lines.map((l) => l + ANSI.clearLine).join("\n") + "\n");
1389
+ };
1390
+
1391
+ const cleanup = () => {
1392
+ if (done) return;
1393
+ done = true;
1394
+ process.stdin.removeListener("keypress", onKey);
1395
+ process.stdout.write(ANSI.mouseOff);
1396
+ if (process.stdin.setRawMode) process.stdin.setRawMode(previousRaw);
1397
+ if (!previousRaw) process.stdin.pause();
1398
+ erase();
1399
+ };
1400
+
1401
+ // Run a verb (spawnSync blocks briefly — people ops hit GitHub). Surface the
1402
+ // last stdout/stderr line as a notice; refresh the list from disk after.
1403
+ const runVerb = (args) => {
1404
+ const r = runSettings(inst, args);
1405
+ const msg = ((r.stdout || "") + " " + (r.stderr || "")).trim().split("\n").map((s) => s.trim()).filter(Boolean).pop() || "";
1406
+ notice = r && r.status === 0 ? msg : `${YELLOW}${msg}${RESET}`;
1407
+ refresh();
1408
+ };
1409
+
1410
+ function onKey(chunk, key = {}) {
1411
+ if (isMouseEvent(chunk, key)) return;
1412
+ if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
1413
+
1414
+ if (mode === "add") {
1415
+ if (key.name === "escape") { mode = "browse"; input = ""; render(); return; }
1416
+ if (key.name === "return") {
1417
+ const v = input.trim().split(/\s+/)[0];
1418
+ mode = "browse"; input = "";
1419
+ if (v) runVerb([domain, "add", v]);
1420
+ render();
1421
+ return;
1422
+ }
1423
+ if (key.name === "backspace") { input = input.slice(0, -1); render(); return; }
1424
+ if (chunk && chunk.length === 1 && chunk >= " " && !key.ctrl && !key.meta) { input += chunk; render(); return; }
1425
+ return;
1426
+ }
1427
+
1428
+ if (mode === "confirm") {
1429
+ if (chunk === "y" || chunk === "Y") {
1430
+ const victim = items[cursor];
1431
+ mode = "browse";
1432
+ if (victim) runVerb([domain, "remove", victim]);
1433
+ } else { mode = "browse"; }
1434
+ render();
1435
+ return;
1436
+ }
1437
+
1438
+ // browse
1439
+ if (key.name === "escape" || key.name === "left") { cleanup(); resolve(); return; }
1440
+ if (key.name === "up" || key.name === "k") { if (items.length) cursor = (cursor - 1 + items.length) % items.length; render(); return; }
1441
+ if (key.name === "down" || key.name === "j") { if (items.length) cursor = (cursor + 1) % items.length; render(); return; }
1442
+ if (chunk === "a") { mode = "add"; input = ""; notice = ""; render(); return; }
1443
+ if (chunk === "d") { if (items.length) { mode = "confirm"; render(); } return; }
1444
+ }
1445
+
1446
+ readline.emitKeypressEvents(process.stdin);
1447
+ if (process.stdin.setRawMode) process.stdin.setRawMode(true);
1448
+ process.stdin.resume();
1449
+ process.stdout.write(ANSI.mouseOff);
1450
+ process.stdin.on("keypress", onKey);
1451
+ render();
1452
+ });
1453
+ }
1454
+
1455
+ // The `s`-key settings screen: a raw-mode menu (hosting toggle + repo/admin
1456
+ // editors) over bin/settings.sh. Modeled on pickRuntimeInteractive.
1457
+ function settingsScreen(inst) {
1458
+ const title = inst.slug || inst.name || path.basename(inst.path);
1459
+
1460
+ if (!fs.existsSync(settingsScriptPath(inst))) {
1461
+ console.log(`\n ${YELLOW}Settings unavailable${RESET} — ${BOLD}${title}${RESET} has no ${DIM}bin/settings.sh${RESET}.`);
1462
+ console.log(` Update it: open the egregore and run ${CYAN}/pull${RESET}.\n`);
1463
+ return Promise.resolve();
1464
+ }
1465
+
1466
+ let data = settingsDump(inst) || { hosting: true, repos: [], admins: [], people: [] };
1467
+
1468
+ const rows = [
1469
+ {
1470
+ label: () => `Hosted Artifacts`,
1471
+ value: () => (data.hosting ? `${GREEN}ON${RESET}` : `${YELLOW}OFF${RESET}`),
1472
+ detailLines: () => [data.hosting
1473
+ ? ` ${DIM}Enter toggles. When OFF, artifacts never upload to egregore.xyz.${RESET}`
1474
+ : ` ${DIM}Artifacts stay local. /view still renders them on localhost.${RESET}`],
1475
+ },
1476
+ {
1477
+ label: () => `Managed repos`,
1478
+ value: () => `${DIM}${data.repos.length}${RESET}`,
1479
+ detailLines: () => data.repos.length
1480
+ ? data.repos.map((r) => ` ${DIM}·${RESET} ${r.name || r}`)
1481
+ : [` ${DIM}(none)${RESET}`],
1482
+ },
1483
+ {
1484
+ label: () => `Admins`,
1485
+ value: () => `${DIM}${data.admins.length}${RESET}`,
1486
+ detailLines: () => data.admins.length
1487
+ ? [` ${DIM}${data.admins.join(", ")}${RESET}`]
1488
+ : [` ${DIM}(none)${RESET}`],
1489
+ },
1490
+ {
1491
+ label: () => `People`,
1492
+ value: () => `${DIM}${(data.people || []).length}${RESET}`,
1493
+ detailLines: () => (data.people && data.people.length)
1494
+ ? [` ${DIM}${data.people.join(", ")}${RESET}`]
1495
+ : [` ${DIM}(none)${RESET}`],
1496
+ },
1497
+ ];
1498
+
1499
+ return new Promise((resolve) => {
1500
+ let cursor = 0;
1501
+ let rendered = 0;
1502
+ let done = false;
1503
+ let notice = "";
1504
+ const previousRaw = !!process.stdin.isRaw;
1505
+ const isMouseEvent = makeMouseFilter();
1506
+
1507
+ const erase = () => {
1508
+ if (!rendered) return;
1509
+ // Move to the TOP of the last-rendered block, then wipe everything below
1510
+ // in one shot. Clearing line-by-line leaves the cursor at the BOTTOM, so
1511
+ // the next redraw starts a row lower each time — that's the drift that
1512
+ // pushes blank "margins" on top. Up-N + clearDown returns to the top and
1513
+ // clears cleanly, so every render repaints in place.
1514
+ process.stdout.write(`\x1b[${rendered}A` + ANSI.clearDown);
1515
+ rendered = 0;
1516
+ };
1517
+
1518
+ const buildLines = () => {
1519
+ const out = [];
1520
+ out.push(``);
1521
+ out.push(` ${BOLD}Settings${RESET} ${DIM}·${RESET} ${BOLD}${title}${RESET}`);
1522
+ out.push(` ${DIM}────────────────────────────────────────${RESET}`);
1523
+ rows.forEach((row, i) => {
1524
+ const sel = i === cursor;
1525
+ const marker = sel ? `${CYAN}❯${RESET}` : ` `;
1526
+ const name = sel ? `${BOLD}${row.label()}${RESET}` : row.label();
1527
+ out.push(` ${marker} ${name} ${row.value()}`);
1528
+ if (sel) for (const dl of row.detailLines()) out.push(dl);
1529
+ });
1530
+ out.push(``);
1531
+ if (notice) out.push(` ${notice}`);
1532
+ out.push(` ${DIM}↑↓ nav · enter ${cursor === 0 ? "toggle" : "edit"} · esc back · q quit${RESET}`);
1533
+ return out;
1534
+ };
1535
+
1536
+ const render = () => {
1537
+ erase();
1538
+ const lines = buildLines();
1539
+ rendered = lines.length;
1540
+ process.stdout.write(lines.map((l) => l + ANSI.clearLine).join("\n") + "\n");
1541
+ };
1542
+
1543
+ const teardownRaw = () => {
1544
+ process.stdin.removeListener("keypress", onKey);
1545
+ process.stdout.write(ANSI.mouseOff);
1546
+ if (process.stdin.setRawMode) process.stdin.setRawMode(previousRaw);
1547
+ if (!previousRaw) process.stdin.pause();
1548
+ };
1549
+
1550
+ const setupRaw = () => {
1551
+ readline.emitKeypressEvents(process.stdin);
1552
+ if (process.stdin.setRawMode) process.stdin.setRawMode(true);
1553
+ process.stdin.resume();
1554
+ process.stdout.write(ANSI.mouseOff);
1555
+ process.stdin.on("keypress", onKey);
1556
+ };
1557
+
1558
+ const cleanup = () => {
1559
+ if (done) return;
1560
+ done = true;
1561
+ teardownRaw();
1562
+ erase();
1563
+ };
1564
+
1565
+ const toggleHosting = () => {
1566
+ const r = runSettings(inst, ["hosting", data.hosting ? "off" : "on"]);
1567
+ if (!r || r.status !== 0) {
1568
+ notice = `${YELLOW}${((r && r.stderr) || "could not change hosting").trim().split("\n")[0]}${RESET}`;
1569
+ } else {
1570
+ data = settingsDump(inst) || data;
1571
+ notice = `hosting ${data.hosting ? `${GREEN}ON${RESET}` : `${YELLOW}OFF${RESET}`}`;
1572
+ }
1573
+ render();
1574
+ };
1575
+
1576
+ // Hand off to the nested raw-mode list editor, then repaint this screen with
1577
+ // refreshed counts when the user backs out.
1578
+ const openList = (domain, label) => {
1579
+ erase();
1580
+ teardownRaw();
1581
+ listEditor(inst, domain, label).then(() => {
1582
+ data = settingsDump(inst) || data;
1583
+ notice = "";
1584
+ setupRaw();
1585
+ render();
1586
+ });
1587
+ };
1588
+
1589
+ function onKey(chunk, key = {}) {
1590
+ if (isMouseEvent(chunk, key)) return;
1591
+ if (key.ctrl && key.name === "c") { cleanup(); process.exit(130); }
1592
+ if (key.name === "q") { cleanup(); resolve(); return; }
1593
+ // Esc / ← = back (esc never quits).
1594
+ if (key.name === "escape" || key.name === "left") { cleanup(); resolve(); return; }
1595
+ if (key.name === "up" || key.name === "k") { cursor = (cursor - 1 + rows.length) % rows.length; render(); return; }
1596
+ if (key.name === "down" || key.name === "j") { cursor = (cursor + 1) % rows.length; render(); return; }
1597
+ if (key.name === "return" || chunk === " ") {
1598
+ if (cursor === 0) toggleHosting();
1599
+ else if (cursor === 1) openList("repo", "Managed repos");
1600
+ else if (cursor === 2) openList("admin", "Admins");
1601
+ else if (cursor === 3) openList("people", "People");
1602
+ }
1603
+ }
1604
+
1605
+ setupRaw();
1606
+ render();
1607
+ });
1608
+ }
1609
+
1610
+ async function cmdSettings(args) {
1611
+ const registry = readRegistry();
1612
+ if (registry.length === 0) die("no egregores installed yet.\n run: npx -y create-egregore@latest");
1613
+ const DOMAINS = new Set(["hosting", "repo", "admin", "people", "dump"]);
1614
+ let rest = args.slice();
1615
+ let slug = "";
1616
+ if (rest.length && !DOMAINS.has(rest[0])) { slug = rest[0]; rest = rest.slice(1); }
1617
+ const inst = resolveInviteInstance(registry, slug);
1618
+ if (!inst) {
1619
+ die(`multiple egregores installed; choose one:\n egregore settings <slug> [hosting|repo|admin]\n run: egregore ls`);
1620
+ }
1621
+ // No verb + a TTY → interactive screen. Otherwise forward straight to the verb.
1622
+ if (rest.length === 0) {
1623
+ if (isTTY && canPrompt()) return settingsScreen(inst);
1624
+ rest = ["dump"];
1625
+ }
1626
+ const r = runSettings(inst, rest);
1627
+ if (r.stdout) process.stdout.write(r.stdout.endsWith("\n") ? r.stdout : r.stdout + "\n");
1628
+ if (r.stderr) process.stderr.write(r.stderr);
1629
+ process.exitCode = r.status || 0;
1630
+ }
1631
+
1254
1632
  // ── tiny sync confirm (non-TUI flows) ───────────────────────────────
1255
1633
 
1256
1634
  function syncConfirm(question) {
@@ -1370,7 +1748,7 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
1370
1748
  ? `filter /${filter}`
1371
1749
  : "press ? for help";
1372
1750
  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}`;
1751
+ const skillStatus = `Codex ${nativeCount} native · ${adapterCount} adapter · Pi ${skillsCount} shared · Claude ${claudeCount}`;
1374
1752
  const skillGroups = [
1375
1753
  `${DIM}core${RESET}: ${workflows}`,
1376
1754
  `${DIM}team${RESET}: ${joinShort(["/handoff", "/ask", "/invite", "/quest"], Math.max(16, bodyWidth - 8))}`,
@@ -1411,8 +1789,8 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
1411
1789
  `${BOLD}Harnesses${RESET}`,
1412
1790
  `${DIM}claude-code${RESET}: ${commandExists("claude") ? "installed" : "not installed"}`,
1413
1791
  `${DIM}codex${RESET}: ${commandExists("codex") ? "installed" : "not installed"}`,
1792
+ `${DIM}pi${RESET}: ${commandExists("pi") ? "installed" : "not installed"}`,
1414
1793
  `${DIM}Hermes — Coming Soon${RESET}`,
1415
- `${DIM}Pi — Coming Soon${RESET}`,
1416
1794
  ];
1417
1795
  const rows = Math.max(EGREGORE_SIGIL.length + 2, right.length);
1418
1796
  for (let i = 0; i < rows; i++) {
@@ -1424,11 +1802,11 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
1424
1802
  lines.push(boxLine(`${BOLD}Available Skills${RESET}`, innerWidth, margin));
1425
1803
  for (const group of skillGroups) lines.push(boxLine(group, innerWidth, margin));
1426
1804
  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));
1805
+ 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
1806
  }
1429
1807
 
1430
1808
  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));
1809
+ lines.push(boxLine(`${DIM}enter selects an instance, then choose Claude Code, Codex, or Pi without leaving this screen${RESET}`, innerWidth, margin));
1432
1810
  lines.push(bottom);
1433
1811
  return lines;
1434
1812
  }
@@ -1461,7 +1839,7 @@ function renderRuntimePanel({ inst, cursor, width, height }) {
1461
1839
  lines.push(boxLine(`${marker} ${pad(labelText, 16)} ${DIM}${desc}${RESET}`, innerWidth, margin));
1462
1840
  }
1463
1841
  // Coming-soon harnesses — shown for visibility, not selectable yet.
1464
- for (const soon of ["Hermes", "Pi"]) {
1842
+ for (const soon of ["Hermes"]) {
1465
1843
  lines.push(boxLine(`${DIM}· ${pad(soon, 16)} coming soon${RESET}`, innerWidth, margin));
1466
1844
  }
1467
1845
 
@@ -1674,7 +2052,7 @@ async function browser(args = {}) {
1674
2052
  const label = (k) => `${DIM}${pad(k, 10)}${RESET}`;
1675
2053
  previewLines.push(` ${label("action")} ${BOLD}Set up a new Egregore${RESET}`);
1676
2054
  previewLines.push(` ${label("flow")} guided install, Telegram, invite, ready/start`);
1677
- previewLines.push(` ${label("harness")} choose Claude Code or Codex during setup`);
2055
+ previewLines.push(` ${label("harness")} choose Claude Code, Codex, or Pi during setup`);
1678
2056
  previewLines.push(` ${label("start")} press enter to begin`);
1679
2057
  } else if (!selected) {
1680
2058
  previewLines.push(`${DIM} (no selection)${RESET}`);
@@ -1697,7 +2075,7 @@ async function browser(args = {}) {
1697
2075
  if (pv.skills) {
1698
2076
  const skillLine = selectedWorkflowLine(pv, Math.max(16, blockWidth - 20));
1699
2077
  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}`);
2078
+ 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
2079
  }
1702
2080
  const modeBits = [];
1703
2081
  modeBits.push(runtimeShort(runtimeFor(selected, args.runtimeOverride)));
@@ -1757,8 +2135,8 @@ async function browser(args = {}) {
1757
2135
  footer = band(`● ${stripAnsi(state.message)}`, bg);
1758
2136
  } else {
1759
2137
  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);
2138
+ ? band(`${key("↑↓", "nav")} ${key("enter", "select")} ${key("n/+", "new")} ${key("i", "invite")} ${key("s", "settings")} ${key("d", "delete")} ${key("?", "help")} ${key("q", "quit")}`, 46)
2139
+ : 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
2140
  }
1763
2141
  out.push(M + footer + "\n");
1764
2142
  if (state.showHelp) {
@@ -2056,6 +2434,13 @@ async function browser(args = {}) {
2056
2434
  render();
2057
2435
  return;
2058
2436
  }
2437
+ if (chunk === "s") {
2438
+ const pick = visible()[state.cursor]?.e;
2439
+ if (!pick) return;
2440
+ cleanup();
2441
+ resolve(settingsScreen(pick));
2442
+ return;
2443
+ }
2059
2444
  if (chunk === "u") {
2060
2445
  const pick = visible()[state.cursor]?.e;
2061
2446
  if (!pick) return;
@@ -2174,6 +2559,7 @@ function main() {
2174
2559
  if (cmd === "ls" || cmd === "list") return cmdLs();
2175
2560
  if (cmd === "use") return cmdUse(args[1]);
2176
2561
  if (cmd === "/invite" || cmd === "invite") return cmdInvite(args.slice(1));
2562
+ if (cmd === "settings" || cmd === "config") return cmdSettings(args.slice(1));
2177
2563
  if (cmd === "prune") return cmdPruneNonInteractive(args[1] === "--dry-run" || args[1] === "-n");
2178
2564
  if (cmd === "rm" || cmd === "remove") return cmdRmNonInteractive(args[1]);
2179
2565
  if (cmd === "browse") return browser({ force: true, runtimeOverride });