create-egregore 0.19.23 → 0.19.24

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 (119) hide show
  1. package/assets/egregore-launcher.js +50 -7
  2. package/assets/egregore-launcher.sh +10 -2
  3. package/bin/cli.js +21 -3
  4. package/lib/codex-runtime.js +25 -3
  5. package/lib/connect-runtime.js +329 -0
  6. package/lib/pi-runtime.js +23 -3
  7. package/lib/setup.js +13 -0
  8. package/package.json +3 -3
  9. package/runtime/codex/.codex/skills/handoff/SKILL.md +35 -11
  10. package/runtime/codex/.codex/skills/save/SKILL.md +39 -4
  11. package/runtime/codex/.codex/spec-manifest.json +2 -2
  12. package/runtime/codex/AGENTS.md +18 -1
  13. package/runtime/codex/bin/agent.sh +28 -14
  14. package/runtime/codex/bin/attendant.sh +169 -6
  15. package/runtime/codex/bin/codex-render-spec.mjs +18 -1
  16. package/runtime/codex/bin/codex-session-start.sh +13 -0
  17. package/runtime/codex/bin/codex-sync-skills.sh +1 -0
  18. package/runtime/codex/bin/graph.sh +21 -1
  19. package/runtime/codex/bin/handoff-run.sh +25 -5
  20. package/runtime/codex/bin/lib/context.sh +220 -172
  21. package/runtime/codex/bin/lib/dashboard-artifact.sh +113 -13
  22. package/runtime/codex/bin/lib/git-sync.sh +44 -8
  23. package/runtime/codex/bin/lib/greeting.sh +34 -11
  24. package/runtime/codex/bin/lib/identity.sh +32 -14
  25. package/runtime/codex/bin/person.py +20 -0
  26. package/runtime/codex/manifest.json +3 -72
  27. package/runtime/pi/.codex/skills/handoff/SKILL.md +35 -11
  28. package/runtime/pi/.codex/skills/save/SKILL.md +39 -4
  29. package/runtime/pi/.codex/spec-manifest.json +2 -2
  30. package/runtime/pi/.pi/APPEND_SYSTEM.md +18 -1
  31. package/runtime/pi/.pi/spec-manifest.json +2 -2
  32. package/runtime/pi/AGENTS.md +18 -1
  33. package/runtime/pi/bin/agent.sh +28 -14
  34. package/runtime/pi/bin/attendant.sh +169 -6
  35. package/runtime/pi/bin/codex-render-spec.mjs +18 -1
  36. package/runtime/pi/bin/codex-session-start.sh +13 -0
  37. package/runtime/pi/bin/codex-sync-skills.sh +1 -0
  38. package/runtime/pi/bin/graph.sh +21 -1
  39. package/runtime/pi/bin/handoff-run.sh +25 -5
  40. package/runtime/pi/bin/lib/context.sh +220 -172
  41. package/runtime/pi/bin/lib/dashboard-artifact.sh +113 -13
  42. package/runtime/pi/bin/lib/git-sync.sh +44 -8
  43. package/runtime/pi/bin/lib/greeting.sh +34 -11
  44. package/runtime/pi/bin/lib/identity.sh +32 -14
  45. package/runtime/pi/bin/person.py +20 -0
  46. package/runtime/pi/bin/pi-product-workflows.mjs +5 -1
  47. package/runtime/pi/manifest.json +3 -72
  48. package/runtime/codex/.codex/skills/add/SKILL.md +0 -29
  49. package/runtime/codex/.codex/skills/answer/SKILL.md +0 -29
  50. package/runtime/codex/.codex/skills/ask/SKILL.md +0 -67
  51. package/runtime/codex/.codex/skills/audit/SKILL.md +0 -29
  52. package/runtime/codex/.codex/skills/autosave/SKILL.md +0 -29
  53. package/runtime/codex/.codex/skills/branch/SKILL.md +0 -29
  54. package/runtime/codex/.codex/skills/commit/SKILL.md +0 -29
  55. package/runtime/codex/.codex/skills/demo-mode/SKILL.md +0 -29
  56. package/runtime/codex/.codex/skills/deploy-bridge/SKILL.md +0 -29
  57. package/runtime/codex/.codex/skills/design/SKILL.md +0 -29
  58. package/runtime/codex/.codex/skills/egregore-voice/SKILL.md +0 -29
  59. package/runtime/codex/.codex/skills/emissary/SKILL.md +0 -45
  60. package/runtime/codex/.codex/skills/env/SKILL.md +0 -29
  61. package/runtime/codex/.codex/skills/ingest-google/SKILL.md +0 -29
  62. package/runtime/codex/.codex/skills/minion/SKILL.md +0 -29
  63. package/runtime/codex/.codex/skills/mock/SKILL.md +0 -29
  64. package/runtime/codex/.codex/skills/pr/SKILL.md +0 -29
  65. package/runtime/codex/.codex/skills/product-voice/SKILL.md +0 -29
  66. package/runtime/codex/.codex/skills/project/SKILL.md +0 -45
  67. package/runtime/codex/.codex/skills/pull/SKILL.md +0 -29
  68. package/runtime/codex/.codex/skills/push/SKILL.md +0 -29
  69. package/runtime/codex/.codex/skills/quest-suggest/SKILL.md +0 -29
  70. package/runtime/codex/.codex/skills/review-pr/SKILL.md +0 -45
  71. package/runtime/codex/.codex/skills/scroll/SKILL.md +0 -288
  72. package/runtime/codex/.codex/skills/scroll/agents/openai.yaml +0 -4
  73. package/runtime/codex/.codex/skills/scroll/assets/shell.html +0 -871
  74. package/runtime/codex/.codex/skills/search/SKILL.md +0 -79
  75. package/runtime/codex/.codex/skills/session-view/SKILL.md +0 -29
  76. package/runtime/codex/.codex/skills/setup/SKILL.md +0 -29
  77. package/runtime/codex/.codex/skills/summon/SKILL.md +0 -45
  78. package/runtime/codex/.codex/skills/task/SKILL.md +0 -29
  79. package/runtime/codex/.codex/skills/teams-connect/SKILL.md +0 -29
  80. package/runtime/codex/.codex/skills/telegram-connect/SKILL.md +0 -29
  81. package/runtime/codex/.codex/skills/the-spiral/SKILL.md +0 -84
  82. package/runtime/codex/.codex/skills/thread/SKILL.md +0 -29
  83. package/runtime/codex/.codex/skills/todo/SKILL.md +0 -45
  84. package/runtime/pi/.codex/skills/add/SKILL.md +0 -29
  85. package/runtime/pi/.codex/skills/answer/SKILL.md +0 -29
  86. package/runtime/pi/.codex/skills/ask/SKILL.md +0 -67
  87. package/runtime/pi/.codex/skills/audit/SKILL.md +0 -29
  88. package/runtime/pi/.codex/skills/autosave/SKILL.md +0 -29
  89. package/runtime/pi/.codex/skills/branch/SKILL.md +0 -29
  90. package/runtime/pi/.codex/skills/commit/SKILL.md +0 -29
  91. package/runtime/pi/.codex/skills/demo-mode/SKILL.md +0 -29
  92. package/runtime/pi/.codex/skills/deploy-bridge/SKILL.md +0 -29
  93. package/runtime/pi/.codex/skills/design/SKILL.md +0 -29
  94. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +0 -29
  95. package/runtime/pi/.codex/skills/emissary/SKILL.md +0 -45
  96. package/runtime/pi/.codex/skills/env/SKILL.md +0 -29
  97. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +0 -29
  98. package/runtime/pi/.codex/skills/minion/SKILL.md +0 -29
  99. package/runtime/pi/.codex/skills/mock/SKILL.md +0 -29
  100. package/runtime/pi/.codex/skills/pr/SKILL.md +0 -29
  101. package/runtime/pi/.codex/skills/product-voice/SKILL.md +0 -29
  102. package/runtime/pi/.codex/skills/project/SKILL.md +0 -45
  103. package/runtime/pi/.codex/skills/pull/SKILL.md +0 -29
  104. package/runtime/pi/.codex/skills/push/SKILL.md +0 -29
  105. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +0 -29
  106. package/runtime/pi/.codex/skills/review-pr/SKILL.md +0 -45
  107. package/runtime/pi/.codex/skills/scroll/SKILL.md +0 -288
  108. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +0 -4
  109. package/runtime/pi/.codex/skills/scroll/assets/shell.html +0 -871
  110. package/runtime/pi/.codex/skills/search/SKILL.md +0 -79
  111. package/runtime/pi/.codex/skills/session-view/SKILL.md +0 -29
  112. package/runtime/pi/.codex/skills/setup/SKILL.md +0 -29
  113. package/runtime/pi/.codex/skills/summon/SKILL.md +0 -45
  114. package/runtime/pi/.codex/skills/task/SKILL.md +0 -29
  115. package/runtime/pi/.codex/skills/teams-connect/SKILL.md +0 -29
  116. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +0 -29
  117. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +0 -84
  118. package/runtime/pi/.codex/skills/thread/SKILL.md +0 -29
  119. package/runtime/pi/.codex/skills/todo/SKILL.md +0 -45
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // egregore-launcher-version: 0.19.23
2
+ // egregore-launcher-version: 0.19.24
3
3
  //
4
4
  // egregore — launch and browse Egregore instances from your terminal.
5
5
  // Installed by create-egregore. Reads ~/.egregore/instances.json.
@@ -33,7 +33,7 @@ const crypto = require("node:crypto");
33
33
  const { spawnSync, spawn } = require("node:child_process");
34
34
  const readline = require("node:readline");
35
35
 
36
- const VERSION = "0.19.23";
36
+ const VERSION = "0.19.24";
37
37
  const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
38
38
  const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
39
39
  const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
@@ -670,6 +670,27 @@ async function renderCodexStartupCard(egregoreDir) {
670
670
  return false;
671
671
  }
672
672
 
673
+ function renderClaudeGreetingCard(egregoreDir) {
674
+ // Claude Code counterpart of renderCodexStartupCard: prints the card the
675
+ // previous boot cached (bin/greeting-card.sh) instead of re-rendering —
676
+ // must stay instant. Never fatal: a miss (first boot, stale cache, old
677
+ // core) just means the session renders the full greeting itself.
678
+ const script = path.join(egregoreDir, "bin", "greeting-card.sh");
679
+ if (!fs.existsSync(script)) return false;
680
+ try {
681
+ const result = spawnSync("bash", [script], {
682
+ cwd: egregoreDir,
683
+ encoding: "utf8",
684
+ stdio: ["ignore", "pipe", "pipe"],
685
+ });
686
+ if (result.status === 0 && result.stdout) {
687
+ process.stdout.write(result.stdout);
688
+ return true;
689
+ }
690
+ } catch {}
691
+ return false;
692
+ }
693
+
673
694
  function codexSupportMessage(egregoreDir) {
674
695
  return [
675
696
  "Codex harness support is missing from this Egregore checkout.",
@@ -802,16 +823,22 @@ async function launch(inst, runtimeOverride) {
802
823
  } else if (runtime === "pi") {
803
824
  if (!hasPiCoreSupport(inst.path)) die(piSupportMessage(inst.path));
804
825
  }
826
+ // Print the cached greeting card for Claude Code (instant; at most one
827
+ // session stale) and tell the session to skip the in-chat re-render.
828
+ const claudeCardShown =
829
+ runtime === "claude-code" && renderClaudeGreetingCard(inst.path);
805
830
  // Replace the current process with the selected assistant in the instance dir.
806
831
  // spawn + detached + exit-on-exit replicates exec() in Node.
832
+ const launchEnv = {
833
+ ...process.env,
834
+ EGREGORE_RUNTIME: runtime,
835
+ EGREGORE_CODEX_TRUSTED: runtime === "codex" ? "1" : process.env.EGREGORE_CODEX_TRUSTED,
836
+ };
837
+ if (claudeCardShown) launchEnv.EGREGORE_CARD_SHOWN = "1";
807
838
  const child = spawn(binary, runtimeLaunchArgs(runtime, inst.path), {
808
839
  cwd: inst.path,
809
840
  stdio: "inherit",
810
- env: {
811
- ...process.env,
812
- EGREGORE_RUNTIME: runtime,
813
- EGREGORE_CODEX_TRUSTED: runtime === "codex" ? "1" : process.env.EGREGORE_CODEX_TRUSTED,
814
- },
841
+ env: launchEnv,
815
842
  });
816
843
  child.on("exit", (code, signal) => {
817
844
  process.exit(signal ? 1 : (code ?? 0));
@@ -1608,6 +1635,22 @@ async function cmdConnect(args) {
1608
1635
  for (const line of backfillSummaryLines(backfillSummary)) {
1609
1636
  console.log(` ${DIM}${line}${RESET}`);
1610
1637
  }
1638
+ const runtimeRefresh = spawnSync(
1639
+ "npx",
1640
+ ["-y", `${PUBLIC_NPX_PACKAGE}@latest`, "--refresh-launcher"],
1641
+ {
1642
+ cwd: inst.path,
1643
+ encoding: "utf-8",
1644
+ stdio: "pipe",
1645
+ timeout: 3 * 60 * 1000,
1646
+ },
1647
+ );
1648
+ if (runtimeRefresh.status === 0) {
1649
+ console.log(` ${GREEN}✓${RESET} Connect features installed`);
1650
+ } else {
1651
+ console.log(` ${YELLOW}!${RESET} Connect features will retry on the next update`);
1652
+ console.log(` ${DIM}npx -y ${PUBLIC_NPX_PACKAGE}@latest --refresh-launcher${RESET}`);
1653
+ }
1611
1654
  console.log("");
1612
1655
  console.log(` ${GREEN}✓${RESET} ${BOLD}${inst.slug}${RESET} is connected. Local files remain the source of truth.`);
1613
1656
  console.log("");
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # egregore-launcher-version: 0.4.10
2
+ # egregore-launcher-version: 0.4.11
3
3
  # egregore — launch an Egregore instance in Claude Code.
4
4
  # Reads ~/.egregore/instances.json for the registry of installed egregores.
5
5
  #
@@ -22,7 +22,7 @@
22
22
 
23
23
  set -u
24
24
 
25
- VERSION="0.4.10"
25
+ VERSION="0.4.11"
26
26
  REGISTRY="${HOME}/.egregore/instances.json"
27
27
  DEFAULT_FILE="${HOME}/.egregore/default"
28
28
 
@@ -97,6 +97,14 @@ launch() {
97
97
  if [ -d "$target_path/memory" ]; then
98
98
  claude_args+=("--add-dir" "$target_path/memory")
99
99
  fi
100
+ # Print the cached greeting card (instant; at most one session stale) and
101
+ # tell the session to skip the in-chat re-render. greeting-card.sh exits
102
+ # non-zero silently on a miss (first boot, stale cache, old core) — the
103
+ # session then renders the full greeting itself, exactly as before.
104
+ if [ -f "$target_path/bin/greeting-card.sh" ] \
105
+ && bash "$target_path/bin/greeting-card.sh" 2>/dev/null; then
106
+ export EGREGORE_CARD_SHOWN=1
107
+ fi
100
108
  exec claude "${claude_args[@]}"
101
109
  }
102
110
 
package/bin/cli.js CHANGED
@@ -35,6 +35,12 @@ const {
35
35
  installPiRuntime,
36
36
  reportPiRuntimeSummary,
37
37
  } = require("../lib/pi-runtime");
38
+ const {
39
+ connectEligibility,
40
+ fetchConnectRuntimePack,
41
+ installConnectRuntime,
42
+ reportConnectRuntimeSummary,
43
+ } = require("../lib/connect-runtime");
38
44
  const {
39
45
  install,
40
46
  ghApi,
@@ -108,10 +114,17 @@ function assistantLabel(runtime) {
108
114
  return "Claude Code";
109
115
  }
110
116
 
111
- function upgradePortableRuntimesForExisting(instances) {
117
+ async function upgradePortableRuntimesForExisting(instances) {
112
118
  for (const inst of instances) {
113
119
  if (!inst || !inst.path || !fs.existsSync(inst.path)) continue;
114
120
  try {
121
+ // Fetch and validate the private overlay before pruning the old public
122
+ // bundle. During the one-time ownership migration, an API outage must
123
+ // leave an existing Connect installation untouched.
124
+ const eligibility = connectEligibility(inst.path);
125
+ const connectPack = eligibility.eligible
126
+ ? await fetchConnectRuntimePack(inst.path)
127
+ : null;
115
128
  const summary = installCodexRuntime(inst.path);
116
129
  const ready = hasCodexRuntimeSupport(inst.path);
117
130
  reportCodexRuntimeSummary(summary, ui, {
@@ -124,6 +137,10 @@ function upgradePortableRuntimesForExisting(instances) {
124
137
  label: `Pi meta-harness for ${inst.slug || inst.name || path.basename(inst.path)}`,
125
138
  includeConflicts: !piReady,
126
139
  });
140
+ const connectSummary = await installConnectRuntime(inst.path, { validatedPack: connectPack });
141
+ reportConnectRuntimeSummary(connectSummary, ui, {
142
+ label: `Connect features for ${inst.slug || inst.name || path.basename(inst.path)}`,
143
+ });
127
144
  } catch (err) {
128
145
  ui.warn(`Could not refresh shell-runtime support for ${inst.path}: ${err.message}`);
129
146
  }
@@ -169,7 +186,7 @@ async function chooseRuntime(args) {
169
186
  }
170
187
 
171
188
  async function upgradeLauncherFromExisting(instances) {
172
- upgradePortableRuntimesForExisting(instances);
189
+ await upgradePortableRuntimesForExisting(instances);
173
190
  const inst = instances.find((entry) => entry && entry.path && fs.existsSync(entry.path));
174
191
  if (!inst) return null;
175
192
  try {
@@ -398,7 +415,7 @@ function showHelp() {
398
415
  ui.info(" --force Skip the in-repo detection check");
399
416
  ui.info(" --no-autolaunch Don't launch the harness at the end (for CI)");
400
417
  ui.info(" --no-screens Skip the full-screen welcome/ready bookends");
401
- ui.info(" --refresh-launcher Refresh only the Egregore CLI, then exit");
418
+ ui.info(" --refresh-launcher Refresh the Egregore CLI and runtime packs, then exit");
402
419
  ui.info(" -h, --help Show this help");
403
420
  ui.info("");
404
421
  }
@@ -451,6 +468,7 @@ async function main() {
451
468
  ui.error("No installed Egregore instance was found. Run create-egregore without --refresh-launcher.");
452
469
  process.exit(1);
453
470
  }
471
+ await upgradePortableRuntimesForExisting(instances);
454
472
  const alias = await installLauncherBinary(instances[0].path, ui);
455
473
  if (!alias || !alias.binaryPath) {
456
474
  ui.error("Could not refresh the Egregore CLI.");
@@ -151,6 +151,7 @@ function installCodexRuntime(egregoreDir) {
151
151
  const summary = {
152
152
  installed: 0,
153
153
  updated: 0,
154
+ removed: 0,
154
155
  skipped: 0,
155
156
  conflicted: 0,
156
157
  conflicts: [],
@@ -172,8 +173,29 @@ function installCodexRuntime(egregoreDir) {
172
173
  files: {},
173
174
  };
174
175
  const previousFiles = previous.files || {};
176
+ const currentBundleFiles = bundleFiles();
177
+ const currentBundleSet = new Set(currentBundleFiles);
175
178
 
176
- for (const rel of bundleFiles()) {
179
+ // Remove installer-owned files that moved to queued, Connect, or internal.
180
+ // Preserve anything the user modified and keep its ownership record.
181
+ for (const [rel, record] of Object.entries(previousFiles)) {
182
+ if (currentBundleSet.has(rel)) continue;
183
+ const dest = path.join(egregoreDir, rel);
184
+ if (!pathExistsOrSymlink(dest)) continue;
185
+ let stat;
186
+ try { stat = fs.lstatSync(dest); } catch { continue; }
187
+ if (stat.isFile() && hashBuffer(fs.readFileSync(dest)) === record.hash) {
188
+ fs.unlinkSync(dest);
189
+ try { fs.rmdirSync(path.dirname(dest)); } catch {}
190
+ summary.removed += 1;
191
+ } else {
192
+ summary.conflicted += 1;
193
+ summary.conflicts.push(rel);
194
+ next.files[rel] = { hash: record.hash };
195
+ }
196
+ }
197
+
198
+ for (const rel of currentBundleFiles) {
177
199
  const src = path.join(BUNDLE_DIR, rel);
178
200
  const dest = path.join(egregoreDir, rel);
179
201
  const sourceBuffer = fs.readFileSync(src);
@@ -240,10 +262,10 @@ function reportCodexRuntimeSummary(summary, ui, opts = {}) {
240
262
  if (!summary || !ui) return;
241
263
  const label = opts.label || "Codex runtime";
242
264
  const includeConflicts = opts.includeConflicts !== false;
243
- const changed = summary.installed + summary.updated;
265
+ const changed = summary.installed + summary.updated + (summary.removed || 0);
244
266
  if (changed > 0) {
245
267
  ui.success(
246
- `${label} ready (${summary.installed} installed, ${summary.updated} updated, ${summary.skipped} unchanged)`,
268
+ `${label} ready (${summary.installed} installed, ${summary.updated} updated, ${summary.removed || 0} removed, ${summary.skipped} unchanged)`,
247
269
  );
248
270
  }
249
271
  if (includeConflicts && summary.conflicted > 0) {
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+
3
+ const crypto = require("node:crypto");
4
+ const fs = require("node:fs");
5
+ const http = require("node:http");
6
+ const https = require("node:https");
7
+ const path = require("node:path");
8
+ const { URL } = require("node:url");
9
+
10
+ const PACKAGE = require("../package.json");
11
+ const LOCAL_MANIFEST = path.join(".egregore", "connect-runtime-manifest.json");
12
+ const PACK_PATH = "/api/runtime/connect-pack";
13
+ const MAX_PACK_BYTES = 12 * 1024 * 1024;
14
+ const MAX_FILE_BYTES = 2 * 1024 * 1024;
15
+ const MAX_FILES = 500;
16
+
17
+ function hashBuffer(buffer) {
18
+ return crypto.createHash("sha256").update(buffer).digest("hex");
19
+ }
20
+
21
+ function readJson(file, fallback) {
22
+ try { return JSON.parse(fs.readFileSync(file, "utf8")); }
23
+ catch { return fallback; }
24
+ }
25
+
26
+ function pathExists(file) {
27
+ try { fs.lstatSync(file); return true; }
28
+ catch { return false; }
29
+ }
30
+
31
+ function gitDirFor(egregoreDir) {
32
+ const gitPath = path.join(egregoreDir, ".git");
33
+ try {
34
+ const stat = fs.lstatSync(gitPath);
35
+ if (stat.isDirectory()) return gitPath;
36
+ if (stat.isFile()) {
37
+ const match = fs.readFileSync(gitPath, "utf8").trim().match(/^gitdir:\s*(.+)$/);
38
+ if (match) return path.isAbsolute(match[1]) ? match[1] : path.resolve(egregoreDir, match[1]);
39
+ }
40
+ } catch {}
41
+ return null;
42
+ }
43
+
44
+ function installManifestPath(egregoreDir) {
45
+ const gitDir = gitDirFor(egregoreDir);
46
+ return gitDir
47
+ ? path.join(gitDir, "info", "egregore-connect-runtime.json")
48
+ : path.join(egregoreDir, LOCAL_MANIFEST);
49
+ }
50
+
51
+ function readApiKey(egregoreDir) {
52
+ try {
53
+ const env = fs.readFileSync(path.join(egregoreDir, ".env"), "utf8");
54
+ const match = env.match(/^EGREGORE_API_KEY=(.*)$/m);
55
+ return match && match[1].trim() ? match[1].trim() : "";
56
+ } catch { return ""; }
57
+ }
58
+
59
+ function connectCredentials(egregoreDir) {
60
+ const config = readJson(path.join(egregoreDir, "egregore.json"), null);
61
+ if (!config || config.mode !== "connected" || !config.api_url) {
62
+ return { eligible: false, reason: "not-connected" };
63
+ }
64
+ const apiKey = readApiKey(egregoreDir);
65
+ if (!apiKey) return { eligible: false, reason: "missing-api-key" };
66
+ return {
67
+ eligible: true,
68
+ reason: "connected",
69
+ apiUrl: String(config.api_url).replace(/\/$/, ""),
70
+ apiKey,
71
+ };
72
+ }
73
+
74
+ function connectEligibility(egregoreDir) {
75
+ const { eligible, reason } = connectCredentials(egregoreDir);
76
+ return { eligible, reason };
77
+ }
78
+
79
+ function validateConnectPack(pack) {
80
+ if (!pack || typeof pack !== "object" || Array.isArray(pack)) {
81
+ throw new Error("Connect API returned an invalid runtime pack");
82
+ }
83
+ if (pack.schemaVersion !== 1 || pack.availability !== "connect") {
84
+ throw new Error(`Unsupported Connect runtime pack schema: ${pack.schemaVersion}`);
85
+ }
86
+ if (!Array.isArray(pack.skills) || pack.skills.length === 0) {
87
+ throw new Error("Connect runtime pack has no skill inventory");
88
+ }
89
+ const skills = [...new Set(pack.skills)];
90
+ if (skills.length !== pack.skills.length || skills.some((name) => !/^[a-z0-9-]+$/.test(name))) {
91
+ throw new Error("Connect runtime pack has an invalid skill inventory");
92
+ }
93
+ if (!Array.isArray(pack.files) || pack.files.length === 0 || pack.files.length > MAX_FILES) {
94
+ throw new Error("Connect runtime pack has an invalid file inventory");
95
+ }
96
+ if (!/^[a-f0-9]{64}$/.test(pack.packVersion || "")) {
97
+ throw new Error("Connect runtime pack has an invalid version");
98
+ }
99
+
100
+ const skillSet = new Set(skills);
101
+ const seen = new Set();
102
+ let totalBytes = 0;
103
+ const files = pack.files.map((entry) => {
104
+ if (!entry || typeof entry !== "object" || typeof entry.path !== "string") {
105
+ throw new Error("Connect runtime pack contains an invalid file record");
106
+ }
107
+ const rel = entry.path;
108
+ const match = rel.match(/^\.(claude|codex)\/skills\/([^/]+)\/(.+)$/);
109
+ if (!match || !skillSet.has(match[2]) || rel.includes("\\") || path.posix.normalize(rel) !== rel || seen.has(rel)) {
110
+ throw new Error(`Connect runtime pack contains an unsafe path: ${rel}`);
111
+ }
112
+ seen.add(rel);
113
+ if (!Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o777) {
114
+ throw new Error(`Connect runtime pack contains an invalid mode: ${rel}`);
115
+ }
116
+ if (typeof entry.content !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(entry.content)) {
117
+ throw new Error(`Connect runtime pack contains invalid content: ${rel}`);
118
+ }
119
+ const buffer = Buffer.from(entry.content, "base64");
120
+ if (buffer.length > MAX_FILE_BYTES || buffer.toString("base64") !== entry.content) {
121
+ throw new Error(`Connect runtime pack file exceeds safety limits: ${rel}`);
122
+ }
123
+ totalBytes += buffer.length;
124
+ if (totalBytes > MAX_PACK_BYTES) throw new Error("Connect runtime pack exceeds safety limits");
125
+ if (!/^[a-f0-9]{64}$/.test(entry.sha256 || "") || hashBuffer(buffer) !== entry.sha256) {
126
+ throw new Error(`Connect runtime pack failed integrity validation: ${rel}`);
127
+ }
128
+ return { path: rel, mode: entry.mode, sha256: entry.sha256, buffer };
129
+ });
130
+
131
+ for (const skill of skills) {
132
+ for (const runtime of ["claude", "codex"]) {
133
+ if (!seen.has(`.${runtime}/skills/${skill}/SKILL.md`)) {
134
+ throw new Error(`Connect runtime pack is incomplete for ${skill}`);
135
+ }
136
+ }
137
+ }
138
+ const unsigned = {
139
+ schemaVersion: pack.schemaVersion,
140
+ availability: pack.availability,
141
+ skills: pack.skills,
142
+ files: pack.files,
143
+ };
144
+ if (hashBuffer(Buffer.from(JSON.stringify(unsigned))) !== pack.packVersion) {
145
+ throw new Error("Connect runtime pack version does not match its contents");
146
+ }
147
+ return { schemaVersion: pack.schemaVersion, availability: pack.availability, packVersion: pack.packVersion, skills, files };
148
+ }
149
+
150
+ function requestConnectPack(apiUrl, apiKey, timeout = 30000) {
151
+ return new Promise((resolve, reject) => {
152
+ let parsed;
153
+ try { parsed = new URL(`${apiUrl}${PACK_PATH}`); }
154
+ catch { reject(new Error("Connected instance has an invalid API URL")); return; }
155
+ const transport = parsed.protocol === "http:" ? http : https;
156
+ const request = transport.request(parsed, {
157
+ method: "GET",
158
+ headers: {
159
+ Accept: "application/json",
160
+ Authorization: `Bearer ${apiKey}`,
161
+ "User-Agent": `${PACKAGE.name}/${PACKAGE.version}`,
162
+ },
163
+ timeout,
164
+ }, (response) => {
165
+ const chunks = [];
166
+ let size = 0;
167
+ response.on("error", reject);
168
+ response.on("data", (chunk) => {
169
+ size += chunk.length;
170
+ if (size > MAX_PACK_BYTES * 2) {
171
+ response.destroy(new Error("Connect runtime response exceeds safety limits"));
172
+ return;
173
+ }
174
+ chunks.push(chunk);
175
+ });
176
+ response.on("end", () => {
177
+ const body = Buffer.concat(chunks).toString("utf8");
178
+ if (response.statusCode !== 200) {
179
+ reject(new Error(`Connect runtime download failed (HTTP ${response.statusCode})`));
180
+ return;
181
+ }
182
+ try { resolve(JSON.parse(body)); }
183
+ catch { reject(new Error("Connect API returned an invalid runtime response")); }
184
+ });
185
+ });
186
+ request.on("error", reject);
187
+ request.on("timeout", () => request.destroy(new Error("Connect runtime download timed out")));
188
+ request.end();
189
+ });
190
+ }
191
+
192
+ async function fetchConnectRuntimePack(egregoreDir, opts = {}) {
193
+ const credentials = connectCredentials(egregoreDir);
194
+ if (!credentials.eligible) return null;
195
+ const raw = opts.pack || await (opts.requestPack || requestConnectPack)(
196
+ credentials.apiUrl, credentials.apiKey, opts.timeout,
197
+ );
198
+ return validateConnectPack(raw);
199
+ }
200
+
201
+ function copyPackFile(file, dest) {
202
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
203
+ fs.writeFileSync(dest, file.buffer, { mode: file.mode });
204
+ try { fs.chmodSync(dest, file.mode); } catch {}
205
+ }
206
+
207
+ function recordGitExcludes(egregoreDir, skills) {
208
+ const gitDir = gitDirFor(egregoreDir);
209
+ if (!gitDir) return;
210
+ const excludePath = path.join(gitDir, "info", "exclude");
211
+ fs.mkdirSync(path.dirname(excludePath), { recursive: true });
212
+ const current = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, "utf8") : "";
213
+ const wanted = [];
214
+ for (const skill of skills) wanted.push(`/.claude/skills/${skill}/`, `/.codex/skills/${skill}/`);
215
+ const lines = current.split(/\r?\n/);
216
+ const missing = wanted.filter((line) => !lines.includes(line));
217
+ if (!missing.length) return;
218
+ const prefix = current && !current.endsWith("\n") ? "\n" : "";
219
+ const header = current.includes("# Egregore Connect feature pack")
220
+ ? ""
221
+ : "# Egregore Connect feature pack (authenticated local overlay)\n";
222
+ fs.appendFileSync(excludePath, `${prefix}${header}${missing.join("\n")}\n`);
223
+ }
224
+
225
+ async function installConnectRuntime(egregoreDir, opts = {}) {
226
+ const eligibility = connectEligibility(egregoreDir);
227
+ const summary = {
228
+ ...eligibility, installed: 0, updated: 0, removed: 0, skipped: 0,
229
+ conflicted: 0, conflicts: [], files: [], bundleSkills: [], bundleSkillCount: 0, packVersion: null,
230
+ };
231
+ if (!eligibility.eligible) return summary;
232
+
233
+ // Validate the complete response before touching the checkout. A failed
234
+ // refresh leaves the last known-good Connect overlay intact.
235
+ const pack = opts.validatedPack || await fetchConnectRuntimePack(egregoreDir, opts);
236
+ summary.bundleSkills = pack.skills;
237
+ summary.bundleSkillCount = pack.skills.length;
238
+ summary.packVersion = pack.packVersion;
239
+
240
+ const manifestPath = installManifestPath(egregoreDir);
241
+ const previous = readJson(manifestPath, { files: {} });
242
+ const previousFiles = previous.files || {};
243
+ const currentSet = new Set(pack.files.map((file) => file.path));
244
+ const next = {
245
+ package: PACKAGE.name, version: PACKAGE.version, schemaVersion: pack.schemaVersion,
246
+ packVersion: pack.packVersion, installedAt: new Date().toISOString(), skills: pack.skills, files: {},
247
+ };
248
+
249
+ for (const [rel, record] of Object.entries(previousFiles)) {
250
+ if (currentSet.has(rel)) continue;
251
+ const dest = path.join(egregoreDir, rel);
252
+ if (!pathExists(dest)) continue;
253
+ let stat;
254
+ try { stat = fs.lstatSync(dest); } catch { continue; }
255
+ if (stat.isFile() && hashBuffer(fs.readFileSync(dest)) === record.hash) {
256
+ fs.unlinkSync(dest);
257
+ try { fs.rmdirSync(path.dirname(dest)); } catch {}
258
+ summary.removed += 1;
259
+ } else {
260
+ summary.conflicted += 1;
261
+ summary.conflicts.push(rel);
262
+ next.files[rel] = { hash: record.hash };
263
+ }
264
+ }
265
+
266
+ for (const file of pack.files) {
267
+ const rel = file.path;
268
+ const dest = path.join(egregoreDir, rel);
269
+ const previousHash = previousFiles[rel] && previousFiles[rel].hash;
270
+ const record = () => {
271
+ next.files[rel] = { hash: file.sha256 };
272
+ summary.files.push(rel);
273
+ };
274
+ if (!pathExists(dest)) {
275
+ copyPackFile(file, dest);
276
+ summary.installed += 1;
277
+ record();
278
+ continue;
279
+ }
280
+ let stat;
281
+ try { stat = fs.lstatSync(dest); } catch { stat = null; }
282
+ if (!stat || !stat.isFile()) {
283
+ summary.conflicted += 1;
284
+ summary.conflicts.push(rel);
285
+ if (previousHash) next.files[rel] = { hash: previousHash };
286
+ continue;
287
+ }
288
+ const currentHash = hashBuffer(fs.readFileSync(dest));
289
+ if (currentHash === file.sha256) {
290
+ try { fs.chmodSync(dest, file.mode); } catch {}
291
+ summary.skipped += 1;
292
+ record();
293
+ } else if (previousHash && currentHash === previousHash) {
294
+ copyPackFile(file, dest);
295
+ summary.updated += 1;
296
+ record();
297
+ } else {
298
+ summary.conflicted += 1;
299
+ summary.conflicts.push(rel);
300
+ if (previousHash) next.files[rel] = { hash: previousHash };
301
+ }
302
+ }
303
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
304
+ fs.writeFileSync(manifestPath, `${JSON.stringify(next, null, 2)}\n`);
305
+ recordGitExcludes(egregoreDir, pack.skills);
306
+ return summary;
307
+ }
308
+
309
+ function reportConnectRuntimeSummary(summary, ui, opts = {}) {
310
+ if (!summary || !ui || !summary.eligible) return;
311
+ const label = opts.label || "Connect features";
312
+ const changed = summary.installed + summary.updated + summary.removed;
313
+ if (changed > 0) {
314
+ ui.success(`${label} ready (${summary.installed} installed, ${summary.updated} updated, ${summary.removed} removed)`);
315
+ }
316
+ if (summary.conflicted > 0) {
317
+ ui.warn(`${label}: kept ${summary.conflicted} locally modified file${summary.conflicted === 1 ? "" : "s"}.`);
318
+ }
319
+ }
320
+
321
+ module.exports = {
322
+ connectEligibility,
323
+ fetchConnectRuntimePack,
324
+ installConnectRuntime,
325
+ installManifestPath,
326
+ reportConnectRuntimeSummary,
327
+ requestConnectPack,
328
+ validateConnectPack,
329
+ };
package/lib/pi-runtime.js CHANGED
@@ -101,6 +101,7 @@ function installPiRuntime(egregoreDir) {
101
101
  const summary = {
102
102
  installed: 0,
103
103
  updated: 0,
104
+ removed: 0,
104
105
  skipped: 0,
105
106
  conflicted: 0,
106
107
  conflicts: [],
@@ -108,8 +109,27 @@ function installPiRuntime(egregoreDir) {
108
109
  bundleSkills: bundleManifest().skills || [],
109
110
  bundleSkillCount: bundleManifest().skillCount || 0,
110
111
  };
112
+ const currentBundleFiles = bundleFiles();
113
+ const currentBundleSet = new Set(currentBundleFiles);
111
114
 
112
- for (const rel of bundleFiles()) {
115
+ for (const [rel, record] of Object.entries(previousFiles)) {
116
+ if (currentBundleSet.has(rel)) continue;
117
+ const dest = path.join(egregoreDir, rel);
118
+ if (!pathExists(dest)) continue;
119
+ let stat;
120
+ try { stat = fs.lstatSync(dest); } catch { continue; }
121
+ if (stat.isFile() && hashBuffer(fs.readFileSync(dest)) === record.hash) {
122
+ fs.unlinkSync(dest);
123
+ try { fs.rmdirSync(path.dirname(dest)); } catch {}
124
+ summary.removed += 1;
125
+ } else {
126
+ summary.conflicted += 1;
127
+ summary.conflicts.push(rel);
128
+ next.files[rel] = { hash: record.hash };
129
+ }
130
+ }
131
+
132
+ for (const rel of currentBundleFiles) {
113
133
  const src = path.join(BUNDLE_DIR, rel);
114
134
  const dest = path.join(egregoreDir, rel);
115
135
  const sourceHash = hashBuffer(fs.readFileSync(src));
@@ -163,9 +183,9 @@ function hasPiRuntimeSupport(egregoreDir) {
163
183
  function reportPiRuntimeSummary(summary, ui, opts = {}) {
164
184
  if (!summary || !ui) return;
165
185
  const label = opts.label || "Pi meta-harness";
166
- const changed = summary.installed + summary.updated;
186
+ const changed = summary.installed + summary.updated + (summary.removed || 0);
167
187
  if (changed > 0) {
168
- ui.success(`${label} ready (${summary.installed} installed, ${summary.updated} updated, ${summary.skipped} unchanged)`);
188
+ ui.success(`${label} ready (${summary.installed} installed, ${summary.updated} updated, ${summary.removed || 0} removed, ${summary.skipped} unchanged)`);
169
189
  }
170
190
  if (opts.includeConflicts !== false && summary.conflicted > 0) {
171
191
  const count = summary.conflicted;
package/lib/setup.js CHANGED
@@ -17,6 +17,10 @@ const {
17
17
  installPiRuntime,
18
18
  reportPiRuntimeSummary,
19
19
  } = require("./pi-runtime");
20
+ const {
21
+ installConnectRuntime,
22
+ reportConnectRuntimeSummary,
23
+ } = require("./connect-runtime");
20
24
  const { commandExists } = require("./path-lookup");
21
25
 
22
26
  const https = require("node:https");
@@ -522,6 +526,15 @@ async function install(data, ui, targetDir, opts = {}) {
522
526
  reportPiRuntimeSummary(piRuntime, ui);
523
527
  }
524
528
 
529
+ // Connect is an authenticated additive overlay on the exact OSS runtime.
530
+ // Local instances return immediately; network failure never breaks setup.
531
+ try {
532
+ const connectRuntime = await installConnectRuntime(egregoreDir);
533
+ reportConnectRuntimeSummary(connectRuntime, ui);
534
+ } catch (err) {
535
+ ui.warn(`Connect features were not refreshed: ${err.message}`);
536
+ }
537
+
525
538
  // Reconcile the installer-captured identity through the same portable writer
526
539
  // used by /me and onboarding. Failure is recoverable: the state + person
527
540
  // profile already exist and the next session retries `bin/person.sh sync`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.19.23",
3
+ "version": "0.19.24",
4
4
  "description": "Set up Egregore for your team in one command — installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -10,9 +10,9 @@
10
10
  "node": ">=18"
11
11
  },
12
12
  "scripts": {
13
- "build:runtime": "node scripts/build-runtime-bundle.mjs && node scripts/build-pi-runtime-bundle.mjs",
13
+ "build:runtime": "node ../../bin/capability-distribution.mjs generate-skill-view --root ../.. && node scripts/build-runtime-bundle.mjs && node scripts/build-pi-runtime-bundle.mjs && node ../../api/scripts/build-connect-runtime-pack.mjs",
14
14
  "build:pi-runtime": "node scripts/build-pi-runtime-bundle.mjs",
15
- "test": "node scripts/build-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node --test test/context.test.js test/path-lookup.test.js test/nixos-portability.test.js test/parse-join-target.test.js test/skip-repos.test.js test/install-origin-guard.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/pi-runtime.test.js test/pi-product-workflows.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js test/back-navigation.test.js test/manifest-form.test.js test/launcher-delete.test.js test/launcher-connect.test.js test/launcher-connected-status.test.js test/launcher-permissions.test.js test/launcher-update.test.js test/connected-config.test.js",
15
+ "test": "node ../../bin/capability-distribution.mjs validate --root ../.. && node scripts/build-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node ../../api/scripts/build-connect-runtime-pack.mjs --check && node --test test/capability-distribution.test.js test/connect-runtime.test.js test/context.test.js test/path-lookup.test.js test/nixos-portability.test.js test/parse-join-target.test.js test/skip-repos.test.js test/install-origin-guard.test.js test/onboarding-shortcircuit.test.js test/welcome-screen.test.js test/runtime-selection.test.js test/command-generation.test.js test/auth-gh.test.js test/gauntlet-fixes.test.js test/codex-runtime.test.js test/pi-runtime.test.js test/pi-product-workflows.test.js test/installer-runtime-source.test.js test/scroll-repro.test.js test/launcher-refocus-redraw.test.js test/multiselect-remote-search.test.js test/back-navigation.test.js test/manifest-form.test.js test/launcher-delete.test.js test/launcher-connect.test.js test/launcher-connected-status.test.js test/launcher-permissions.test.js test/launcher-update.test.js test/connected-config.test.js",
16
16
  "test:e2e": "node --test test/e2e-local.test.js",
17
17
  "test:all": "node scripts/build-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node --test test/"
18
18
  },