create-egregore 0.19.12 → 0.19.15

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // egregore-launcher-version: 0.8.0
2
+ // egregore-launcher-version: 0.19.15
3
3
  //
4
4
  // egregore — launch and browse Egregore instances from your terminal.
5
5
  // Installed by create-egregore. Reads ~/.egregore/instances.json.
@@ -33,10 +33,17 @@ 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.8.0";
36
+ const VERSION = "0.19.15";
37
37
  const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
38
38
  const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
39
+ const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
39
40
  const PUBLIC_NPX_PACKAGE = "create-egregore";
41
+ const UPDATE_REGISTRY_URL =
42
+ process.env.EGREGORE_UPDATE_REGISTRY_URL ||
43
+ `https://registry.npmjs.org/${PUBLIC_NPX_PACKAGE}/latest`;
44
+ const UPDATE_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
45
+ const UPDATE_FAILURE_TTL_MS = 30 * 60 * 1000;
46
+ const UPDATE_CHECK_TIMEOUT_MS = 1500;
40
47
  const CONNECT_API_URL = (
41
48
  process.env.EGREGORE_CONNECT_API_URL ||
42
49
  "https://egregore-production-55f2.up.railway.app"
@@ -694,6 +701,24 @@ function installCodexSkills(egregoreDir) {
694
701
  }
695
702
  }
696
703
 
704
+ function claudeLaunchArgs(egregoreDir) {
705
+ const args = ["start"];
706
+ const memoryLink = path.join(egregoreDir, "memory");
707
+ try {
708
+ const memoryDir = fs.realpathSync(memoryLink);
709
+ if (fs.statSync(memoryDir).isDirectory()) {
710
+ // Egregore memory is intentionally a sibling Git repository. Declare it
711
+ // as part of Claude's workspace so auto mode does not classify normal
712
+ // organizational recall as an unexplained cross-project read.
713
+ args.push("--add-dir", memoryDir);
714
+ }
715
+ } catch {
716
+ // A missing/dangling memory link is diagnosed by session startup. Do not
717
+ // prevent the harness from opening.
718
+ }
719
+ return args;
720
+ }
721
+
697
722
  function runtimeLaunchArgs(runtime, egregoreDir) {
698
723
  if (runtime === "codex") {
699
724
  return [
@@ -704,7 +729,7 @@ function runtimeLaunchArgs(runtime, egregoreDir) {
704
729
  ];
705
730
  }
706
731
  if (runtime === "pi") return piTrustArgs();
707
- return ["start"];
732
+ return claudeLaunchArgs(egregoreDir);
708
733
  }
709
734
 
710
735
  async function launch(inst, runtimeOverride) {
@@ -976,11 +1001,145 @@ function requestJson(options, body) {
976
1001
  });
977
1002
  });
978
1003
  req.on("error", (err) => resolve({ status: 0, data: { message: err.message } }));
1004
+ if (options.unref) {
1005
+ req.on("socket", (socket) => socket.unref());
1006
+ }
1007
+ if (options.timeoutMs) {
1008
+ req.setTimeout(options.timeoutMs, () => req.destroy(new Error("request timed out")));
1009
+ }
979
1010
  if (payload) req.write(payload);
980
1011
  req.end();
981
1012
  });
982
1013
  }
983
1014
 
1015
+ function parseReleaseVersion(value) {
1016
+ const match = String(value || "").trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
1017
+ if (!match) return null;
1018
+ return {
1019
+ parts: match.slice(1, 4).map(Number),
1020
+ prerelease: match[4] || "",
1021
+ };
1022
+ }
1023
+
1024
+ function compareReleaseVersions(a, b) {
1025
+ const left = parseReleaseVersion(a);
1026
+ const right = parseReleaseVersion(b);
1027
+ if (!left || !right) return 0;
1028
+ for (let i = 0; i < 3; i++) {
1029
+ if (left.parts[i] !== right.parts[i]) return left.parts[i] > right.parts[i] ? 1 : -1;
1030
+ }
1031
+ if (left.prerelease === right.prerelease) return 0;
1032
+ if (!left.prerelease) return 1;
1033
+ if (!right.prerelease) return -1;
1034
+ return left.prerelease.localeCompare(right.prerelease);
1035
+ }
1036
+
1037
+ function cliUpdateState(latest) {
1038
+ if (!parseReleaseVersion(latest)) return null;
1039
+ return {
1040
+ current: VERSION,
1041
+ latest,
1042
+ available: compareReleaseVersions(latest, VERSION) > 0,
1043
+ };
1044
+ }
1045
+
1046
+ function readUpdateCache() {
1047
+ try {
1048
+ const cache = JSON.parse(fs.readFileSync(UPDATE_CACHE_FILE, "utf-8"));
1049
+ if (!cache || !cache.checked_at) return null;
1050
+ const age = Date.now() - new Date(cache.checked_at).getTime();
1051
+ const ttl = cache.latest ? UPDATE_CACHE_TTL_MS : UPDATE_FAILURE_TTL_MS;
1052
+ if (!Number.isFinite(age) || age < 0 || age > ttl) return null;
1053
+ if (!cache.latest) return { current: VERSION, latest: null, available: false };
1054
+ return cliUpdateState(cache.latest);
1055
+ } catch {
1056
+ return null;
1057
+ }
1058
+ }
1059
+
1060
+ function writeUpdateCache(latest) {
1061
+ try {
1062
+ fs.mkdirSync(path.dirname(UPDATE_CACHE_FILE), { recursive: true });
1063
+ atomicWrite(
1064
+ UPDATE_CACHE_FILE,
1065
+ JSON.stringify({ checked_at: new Date().toISOString(), latest: latest || null }, null, 2) + "\n",
1066
+ 0o600,
1067
+ );
1068
+ } catch {}
1069
+ }
1070
+
1071
+ async function checkForCliUpdate() {
1072
+ if (process.env.EGREGORE_DISABLE_UPDATE_CHECK === "1") return null;
1073
+ if (process.env.EGREGORE_UPDATE_LATEST_VERSION) {
1074
+ return cliUpdateState(process.env.EGREGORE_UPDATE_LATEST_VERSION);
1075
+ }
1076
+
1077
+ const cached = readUpdateCache();
1078
+ if (cached) return cached;
1079
+
1080
+ try {
1081
+ const url = new URL(UPDATE_REGISTRY_URL);
1082
+ const result = await requestJson({
1083
+ hostname: url.hostname,
1084
+ port: url.port || undefined,
1085
+ path: `${url.pathname}${url.search}`,
1086
+ method: "GET",
1087
+ protocol: url.protocol,
1088
+ timeoutMs: UPDATE_CHECK_TIMEOUT_MS,
1089
+ unref: true,
1090
+ headers: { Accept: "application/json" },
1091
+ });
1092
+ const latest = result.status === 200 && result.data && result.data.version;
1093
+ const update = cliUpdateState(latest);
1094
+ writeUpdateCache(update?.latest || null);
1095
+ return update;
1096
+ } catch {
1097
+ return null;
1098
+ }
1099
+ }
1100
+
1101
+ function cmdCliUpdate(latest) {
1102
+ if (!parseReleaseVersion(latest)) {
1103
+ console.error(`\n ${YELLOW}!${RESET} Could not verify the CLI release version.\n`);
1104
+ process.exitCode = 1;
1105
+ return;
1106
+ }
1107
+
1108
+ console.log("");
1109
+ console.log(` ${BOLD}${CYAN}↑ CLI UPDATE${RESET}`);
1110
+ console.log(` ${DIM}${VERSION} → ${latest}${RESET}`);
1111
+ console.log("");
1112
+ process.stdout.write(` [1/2] Installing ${PUBLIC_NPX_PACKAGE}@${latest}…`);
1113
+ const result = spawnSync(
1114
+ "npx",
1115
+ ["-y", `${PUBLIC_NPX_PACKAGE}@${latest}`, "--refresh-launcher"],
1116
+ { encoding: "utf-8", stdio: ["inherit", "pipe", "pipe"], env: { ...process.env } },
1117
+ );
1118
+ if (result.status !== 0) {
1119
+ console.log(`\r${ANSI.clearLine} [1/2] ${YELLOW}!${RESET} Install failed`);
1120
+ const detail = String(result.stderr || result.stdout || "").trim().split("\n").filter(Boolean).pop();
1121
+ if (detail) console.error(` ${DIM}${detail}${RESET}`);
1122
+ console.error(` Retry: npx -y ${PUBLIC_NPX_PACKAGE}@${latest} --refresh-launcher\n`);
1123
+ process.exitCode = 1;
1124
+ return;
1125
+ }
1126
+ console.log(`\r${ANSI.clearLine} [1/2] ${GREEN}✓${RESET} CLI ${latest} installed`);
1127
+
1128
+ if (process.env.EGREGORE_UPDATE_NO_RELAUNCH === "1") {
1129
+ console.log(` [2/2] ${GREEN}✓${RESET} Ready for the next launch\n`);
1130
+ return;
1131
+ }
1132
+
1133
+ console.log(` [2/2] ${GREEN}✓${RESET} Reopening Egregore\n`);
1134
+ const relaunched = spawnSync(process.execPath, [process.argv[1], "browse"], {
1135
+ stdio: "inherit",
1136
+ env: { ...process.env },
1137
+ });
1138
+ if (relaunched.error) {
1139
+ console.error(` ${YELLOW}!${RESET} Updated successfully. Run ${BOLD}egregore${RESET} to reopen.\n`);
1140
+ }
1141
+ }
1142
+
984
1143
  function githubRequest(token, method, apiPath, body) {
985
1144
  return requestJson({
986
1145
  hostname: "api.github.com",
@@ -1109,12 +1268,42 @@ async function cmdConnect(args) {
1109
1268
 
1110
1269
  let intentId = activation.intent_id || "";
1111
1270
  let connectUrl = activation.connect_url || "";
1112
- if (!intentId || !["checkout_pending", "entitled", "backfilling"].includes(activation.status)) {
1271
+ let ticketExpiresAt = Number(activation.connect_ticket_expires_at || 0) * 1000;
1272
+ const resumable = ["created", "checkout_pending", "entitled", "provisioning", "backfilling"].includes(
1273
+ activation.status,
1274
+ );
1275
+ if (
1276
+ intentId &&
1277
+ resumable &&
1278
+ (!activation.connect_ticket || !ticketExpiresAt || ticketExpiresAt <= Date.now() + 60_000)
1279
+ ) {
1280
+ const refreshed = await connectedApiRequest(
1281
+ githubToken,
1282
+ "POST",
1283
+ `/api/billing/connect/${encodeURIComponent(intentId)}/ticket`,
1284
+ {},
1285
+ );
1286
+ if (refreshed.status !== 200) {
1287
+ die(`could not refresh Connected link: ${connectError(refreshed, `HTTP ${refreshed.status}`)}`);
1288
+ }
1289
+ connectUrl = refreshed.data.connect_url;
1290
+ ticketExpiresAt = Number(refreshed.data.connect_ticket_expires_at || 0) * 1000;
1291
+ persistConnectState(inst, {
1292
+ connect_ticket: refreshed.data.connect_ticket,
1293
+ connect_ticket_expires_at: refreshed.data.connect_ticket_expires_at,
1294
+ connect_url: connectUrl,
1295
+ status: refreshed.data.status || activation.status,
1296
+ });
1297
+ }
1298
+ if (
1299
+ !intentId ||
1300
+ !resumable
1301
+ ) {
1113
1302
  console.log(` [1/5] ${DIM}Creating connection intent…${RESET}`);
1114
1303
  const checkout = await connectedApiRequest(
1115
1304
  githubToken,
1116
1305
  "POST",
1117
- "/api/billing/checkout",
1306
+ "/api/billing/connect-intents",
1118
1307
  {
1119
1308
  org_slug: cfg.slug || inst.slug,
1120
1309
  plan: "connect",
@@ -1134,9 +1323,10 @@ async function cmdConnect(args) {
1134
1323
  connectUrl = checkout.data.connect_url;
1135
1324
  persistConnectState(inst, {
1136
1325
  intent_id: intentId,
1137
- checkout_session_id: checkout.data.checkout_session_id,
1326
+ connect_ticket: checkout.data.connect_ticket,
1327
+ connect_ticket_expires_at: checkout.data.connect_ticket_expires_at,
1138
1328
  connect_url: connectUrl,
1139
- status: "checkout_pending",
1329
+ status: checkout.data.status || "created",
1140
1330
  });
1141
1331
  console.log(` [1/5] ${GREEN}✓${RESET} Connection intent`);
1142
1332
  } else {
@@ -1228,12 +1418,17 @@ async function cmdConnect(args) {
1228
1418
  let backfillSummary = { replayed: true };
1229
1419
  try { backfillSummary = JSON.parse((sync.stdout || "").trim()); } catch {}
1230
1420
 
1231
- const complete = await connectedApiRequest(
1232
- githubToken,
1233
- "POST",
1234
- `/api/billing/connect/${encodeURIComponent(intentId)}/complete`,
1235
- { backfill_summary: backfillSummary },
1236
- );
1421
+ let complete;
1422
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1423
+ complete = await connectedApiRequest(
1424
+ githubToken,
1425
+ "POST",
1426
+ `/api/billing/connect/${encodeURIComponent(intentId)}/complete`,
1427
+ { backfill_summary: backfillSummary },
1428
+ );
1429
+ if (complete.status === 200 || complete.status !== 0) break;
1430
+ await sleep(250 * (attempt + 1));
1431
+ }
1237
1432
  if (complete.status !== 200) {
1238
1433
  die(`graph replay finished, but activation could not be finalized: ${connectError(complete, `HTTP ${complete.status}`)}`);
1239
1434
  }
@@ -1689,7 +1884,7 @@ function selectedWorkflowLine(pv, width) {
1689
1884
  return joinShort(available.map(skillToken), Math.max(12, width));
1690
1885
  }
1691
1886
 
1692
- function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, filterMode }) {
1887
+ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, filterMode, cliUpdate }) {
1693
1888
  const outerWidth = Math.min(Math.max(50, width - 2), 126);
1694
1889
  const innerWidth = Math.max(48, outerWidth - 2);
1695
1890
  const bodyWidth = Math.max(8, innerWidth - 2);
@@ -1698,6 +1893,11 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
1698
1893
  const top = `${margin}${DIM}┌${"─".repeat(2)}${label}${"─".repeat(Math.max(0, innerWidth - label.length - 2))}┐${RESET}`;
1699
1894
  const bottom = `${margin}${DIM}└${"─".repeat(innerWidth)}┘${RESET}`;
1700
1895
  const mid = `${margin}${DIM}├${"─".repeat(innerWidth)}┤${RESET}`;
1896
+ const updateBand = cliUpdate?.available
1897
+ ? (colorOn
1898
+ ? `${margin}${DIM}│${RESET}${E(43)}${E(30)}${pad(` CLI UPDATE AVAILABLE · ${cliUpdate.current} → ${cliUpdate.latest} · ${E(1)}U${E(22)} INSTALL`, innerWidth)}${RESET}${DIM}│${RESET}`
1899
+ : `${margin}│${pad(` CLI UPDATE AVAILABLE · ${cliUpdate.current} → ${cliUpdate.latest} · U INSTALL`, innerWidth)}│`)
1900
+ : null;
1701
1901
  const lines = [top];
1702
1902
 
1703
1903
  const title = selected ? `${selected.slug || selected.name || "egregore"}` : "egregore";
@@ -1738,7 +1938,7 @@ function renderHeroPanel({ selected, pv, width, height, countSuffix, filter, fil
1738
1938
  lines.push(boxLine(`${BOLD}${CYAN}EGREGORE${RESET} ${DIM}v${VERSION} · ${countSuffix} · ${filterText}${RESET}`, innerWidth, margin));
1739
1939
  }
1740
1940
  lines.push(emptyBoxLine(innerWidth, margin));
1741
- lines.push(mid);
1941
+ lines.push(updateBand || mid);
1742
1942
  lines.push(boxLine(`${BOLD}${title}${RESET} ${DIM}· ${org} · ${runtime} · ${countSuffix} · ${filterText}${RESET}`, innerWidth, margin));
1743
1943
  lines.push(boxLine(`${DIM}skills${RESET} ${skillStatus}`, innerWidth, margin));
1744
1944
  lines.push(emptyBoxLine(innerWidth, margin));
@@ -1938,6 +2138,7 @@ async function browser(args = {}) {
1938
2138
  message: null, // ephemeral info line (e.g. "default set to X")
1939
2139
  messageKind: "info", // "info" | "success" | "warn"
1940
2140
  messageUntil: 0,
2141
+ cliUpdate: null,
1941
2142
  };
1942
2143
 
1943
2144
  const newAction = {
@@ -2041,6 +2242,7 @@ async function browser(args = {}) {
2041
2242
  countSuffix,
2042
2243
  filter: state.filter,
2043
2244
  filterMode: state.filterMode,
2245
+ cliUpdate: state.cliUpdate,
2044
2246
  });
2045
2247
  for (const line of heroLines) out.push(line + ANSI.clearLine + "\n");
2046
2248
  out.push(ANSI.clearLine + "\n");
@@ -2571,6 +2773,12 @@ async function browser(args = {}) {
2571
2773
  resolve(cmdConnect([pick.slug]));
2572
2774
  return;
2573
2775
  }
2776
+ if (chunk === "U" && state.cliUpdate?.available) {
2777
+ const latest = state.cliUpdate.latest;
2778
+ cleanup();
2779
+ resolve(cmdCliUpdate(latest));
2780
+ return;
2781
+ }
2574
2782
  if (chunk === "i") {
2575
2783
  const pick = visible()[state.cursor]?.e;
2576
2784
  if (!pick) return;
@@ -2657,6 +2865,11 @@ async function browser(args = {}) {
2657
2865
  });
2658
2866
 
2659
2867
  render();
2868
+ checkForCliUpdate().then((update) => {
2869
+ if (done || !update) return;
2870
+ state.cliUpdate = update;
2871
+ if (update.available) render();
2872
+ });
2660
2873
  });
2661
2874
  }
2662
2875
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # egregore-launcher-version: 0.4.9
2
+ # egregore-launcher-version: 0.4.10
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.9"
25
+ VERSION="0.4.10"
26
26
  REGISTRY="${HOME}/.egregore/instances.json"
27
27
  DEFAULT_FILE="${HOME}/.egregore/default"
28
28
 
@@ -93,7 +93,11 @@ launch() {
93
93
  printf ' %s→%s launching %s%s%s ...\n\n' "$DIM" "$RESET" "$BOLD" "$target_slug" "$RESET"
94
94
  fi
95
95
  cd "$target_path" || die "Could not cd into $target_path"
96
- exec claude start
96
+ local -a claude_args=("start")
97
+ if [ -d "$target_path/memory" ]; then
98
+ claude_args+=("--add-dir" "$target_path/memory")
99
+ fi
100
+ exec claude "${claude_args[@]}"
97
101
  }
98
102
 
99
103
  cmd_ls() {
package/bin/cli.js CHANGED
@@ -223,6 +223,8 @@ function parseArgs(argv) {
223
223
  // already served as the entry screen — a second alt-screen welcome
224
224
  // would otherwise demand a redundant Enter and churn the alt buffer.
225
225
  args.noWelcome = true;
226
+ } else if (argv[i] === "--refresh-launcher") {
227
+ args.refreshLauncher = true;
226
228
  } else if (argv[i] === "--codex") {
227
229
  args.runtime = "codex";
228
230
  } else if (argv[i] === "--pi") {
@@ -395,6 +397,7 @@ function showHelp() {
395
397
  ui.info(" --force Skip the in-repo detection check");
396
398
  ui.info(" --no-autolaunch Don't launch the harness at the end (for CI)");
397
399
  ui.info(" --no-screens Skip the full-screen welcome/ready bookends");
400
+ ui.info(" --refresh-launcher Refresh only the Egregore CLI, then exit");
398
401
  ui.info(" -h, --help Show this help");
399
402
  ui.info("");
400
403
  }
@@ -443,6 +446,21 @@ async function main() {
443
446
  }
444
447
  }
445
448
 
449
+ if (args.refreshLauncher) {
450
+ const instances = readLocalInstances();
451
+ if (instances.length === 0) {
452
+ ui.error("No installed Egregore instance was found. Run create-egregore without --refresh-launcher.");
453
+ process.exit(1);
454
+ }
455
+ const alias = await installLauncherBinary(instances[0].path, ui);
456
+ if (!alias || !alias.binaryPath) {
457
+ ui.error("Could not refresh the Egregore CLI.");
458
+ process.exit(1);
459
+ }
460
+ ui.success(`Egregore CLI is current at ${alias.binaryPath}`);
461
+ return;
462
+ }
463
+
446
464
  // Local-first product surface: if the user already has egregores installed
447
465
  // and didn't pass an explicit mode flag, refresh the launcher and open the
448
466
  // same TUI as bare `egregore`. With no installed instances, make bare
package/lib/setup.js CHANGED
@@ -426,6 +426,15 @@ async function install(data, ui, targetDir, opts = {}) {
426
426
  ui.success("Linked");
427
427
  }
428
428
 
429
+ // The connected config is required infrastructure, not an optional overlay.
430
+ // A setup race once left develop behind main, so a newly-created work branch
431
+ // could lose egregore.json even though the default branch contained it.
432
+ // Recover that exact file from origin/main when possible, then validate it
433
+ // before writing credentials or registering a launcher.
434
+ if (api_key || data.api_url) {
435
+ ensureConnectedConfig(egregoreDir, data, ui);
436
+ }
437
+
429
438
  // 4. Write .env (secrets only — never committed to git)
430
439
  ui.step(4, totalSteps, "Writing credentials...");
431
440
  const envPath = path.join(egregoreDir, ".env");
@@ -450,19 +459,7 @@ async function install(data, ui, targetDir, opts = {}) {
450
459
  fs.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n");
451
460
  try { ensurePersonOnboardingWitness(memoryDir, state); } catch {}
452
461
 
453
- // 4c. Ensure mode is set in egregore.json (connected mode for API-based setup)
454
- const configPath = path.join(egregoreDir, "egregore.json");
455
- if (fs.existsSync(configPath)) {
456
- try {
457
- const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
458
- if (!config.mode) {
459
- config.mode = "connected";
460
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
461
- }
462
- } catch {}
463
- }
464
-
465
- // 4d. Overlay the selected shell-runtime support. Claude Code reads
462
+ // 4c. Overlay the selected shell-runtime support. Claude Code reads
466
463
  // CLAUDE.md, not AGENTS.md; overlaying the Codex AGENTS.md/.codex into a Claude
467
464
  // workspace gives a wandering agent a competing startup protocol (the
468
465
  // xenocracy onboarding regression). Claude joiners keep the cloned repo as-is —
@@ -584,6 +581,74 @@ async function install(data, ui, targetDir, opts = {}) {
584
581
  await launchOrInstruct(path.join(base, forkDirName), alias, ui, opts);
585
582
  }
586
583
 
584
+ /**
585
+ * Require and validate the connected instance configuration.
586
+ *
587
+ * If the current branch is missing egregore.json, restore it from origin/main
588
+ * without changing branches or disturbing existing work. A missing/invalid
589
+ * remote config aborts setup before credentials and launcher registration.
590
+ */
591
+ function ensureConnectedConfig(repoDir, data, ui) {
592
+ const configPath = path.join(repoDir, "egregore.json");
593
+
594
+ if (!fs.existsSync(configPath)) {
595
+ try {
596
+ execFileSync(
597
+ "git",
598
+ ["-C", repoDir, "fetch", "origin", "+main:refs/remotes/origin/main"],
599
+ {
600
+ stdio: "pipe", encoding: "utf-8", timeout: 30000,
601
+ },
602
+ );
603
+ const remoteConfig = execFileSync(
604
+ "git",
605
+ ["-C", repoDir, "show", "origin/main:egregore.json"],
606
+ { stdio: "pipe", encoding: "utf-8", timeout: 10000 },
607
+ );
608
+ JSON.parse(remoteConfig);
609
+ fs.writeFileSync(configPath, remoteConfig.endsWith("\n") ? remoteConfig : `${remoteConfig}\n`);
610
+ if (ui && typeof ui.warn === "function") {
611
+ ui.warn("Restored missing egregore.json from origin/main");
612
+ }
613
+ } catch {
614
+ throw new Error(
615
+ "Connected setup is incomplete: egregore.json is missing from this checkout and origin/main. Retry setup from egregore.xyz/setup."
616
+ );
617
+ }
618
+ }
619
+
620
+ let config;
621
+ try {
622
+ config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
623
+ } catch {
624
+ throw new Error("Connected setup is incomplete: egregore.json is not valid JSON.");
625
+ }
626
+
627
+ const required = ["org_name", "github_org", "memory_repo", "api_url", "slug"];
628
+ const missing = required.filter((key) => !config[key]);
629
+ if (missing.length) {
630
+ throw new Error(
631
+ `Connected setup is incomplete: egregore.json is missing ${missing.join(", ")}. Retry setup from egregore.xyz/setup.`
632
+ );
633
+ }
634
+ if (data.slug && config.slug !== data.slug) {
635
+ throw new Error(
636
+ `Connected setup mismatch: expected org ${data.slug}, but egregore.json belongs to ${config.slug}.`
637
+ );
638
+ }
639
+ if (data.github_org && config.github_org.toLowerCase() !== data.github_org.toLowerCase()) {
640
+ throw new Error(
641
+ `Connected setup mismatch: expected GitHub owner ${data.github_org}, but egregore.json uses ${config.github_org}.`
642
+ );
643
+ }
644
+
645
+ if (config.mode !== "connected") {
646
+ config.mode = "connected";
647
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
648
+ }
649
+ return config;
650
+ }
651
+
587
652
  /**
588
653
  * Ensure a cloned repo has a develop branch. Creates from default branch and pushes if missing.
589
654
  * Idempotent — safe to call on repos that already have develop.
@@ -752,6 +817,20 @@ function piTrustArgs() {
752
817
  }
753
818
  }
754
819
 
820
+ function claudeLaunchArgs(egregoreDir) {
821
+ const args = ["start"];
822
+ const memoryLink = path.join(egregoreDir, "memory");
823
+ try {
824
+ const memoryDir = fs.realpathSync(memoryLink);
825
+ if (fs.statSync(memoryDir).isDirectory()) {
826
+ args.push("--add-dir", memoryDir);
827
+ }
828
+ } catch {
829
+ // Session startup owns the actionable missing-memory diagnostic.
830
+ }
831
+ return args;
832
+ }
833
+
755
834
  function runtimeLaunchArgs(runtime, egregoreDir) {
756
835
  if (normalizeRuntime(runtime) === "codex") {
757
836
  return [
@@ -762,7 +841,7 @@ function runtimeLaunchArgs(runtime, egregoreDir) {
762
841
  ];
763
842
  }
764
843
  if (normalizeRuntime(runtime) === "pi") return piTrustArgs();
765
- return ["start"];
844
+ return claudeLaunchArgs(egregoreDir);
766
845
  }
767
846
 
768
847
  function startSpinner(label) {
@@ -1353,5 +1432,6 @@ module.exports = {
1353
1432
  assistantInstallCommand,
1354
1433
  initMemoryDirs,
1355
1434
  ensureDevelop,
1435
+ ensureConnectedConfig,
1356
1436
  run,
1357
1437
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.19.12",
3
+ "version": "0.19.15",
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": {
@@ -12,7 +12,7 @@
12
12
  "scripts": {
13
13
  "build:runtime": "node scripts/build-runtime-bundle.mjs && node scripts/build-pi-runtime-bundle.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/parse-join-target.test.js test/skip-repos.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",
15
+ "test": "node scripts/build-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node --test test/context.test.js test/parse-join-target.test.js test/skip-repos.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-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
  },
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Mirror of .claude/hooks/search-hint.sh: when the user's prompt is
6
6
  // recall-shaped ("find the handoff about…", "did we decide…"), inject a
7
- // routing hint toward `bin/agent.sh search` — one ranked call over
7
+ // routing hint toward `bash bin/search.sh query` — one ranked call over
8
8
  // memory/ — instead of improvised ls/grep/graph exploration.
9
9
  //
10
10
  // Advisory only, never blocks, rate-limited once per session, exits 0.
@@ -43,6 +43,8 @@ const RECALL = new RegExp(
43
43
  "\\bwho worked on\\b",
44
44
  "\\bdidn.?t (someone|we) already\\b",
45
45
  "\\bwas there (a|any) (handoff|decision|discussion|meeting)\\b",
46
+ "\\b(our|company|org|organization|team)\\b.*\\b(pricing|price|tiers?|plans?|paid|free|strategy|policy|positioning|ownership|decision)\\b",
47
+ "\\b(pricing|price|tiers?|plans?|paid|free)\\b.*\\b(our|company|org|organization|team)\\b",
46
48
  ].join("|"),
47
49
  "i"
48
50
  );
@@ -59,7 +61,7 @@ process.stdout.write(JSON.stringify({
59
61
  hookSpecificOutput: {
60
62
  hookEventName: "UserPromptSubmit",
61
63
  additionalContext:
62
- "Routing hint: this prompt asks for recall from org memory. FIRST action: `bin/agent.sh search \"<the concept, not the literal sentence>\"` — one ranked call over all of memory/ (hybrid keyword+semantic; graph-state annotations attach automatically in connected mode). Read the top hits and cite paths. Do NOT improvise ls/grep/graph exploration first — hand-written graph queries against invented labels (e.g. :Handoff — handoffs are :Session nodes) return empty. Full spec: the search skill.",
64
+ "Routing hint: this prompt asks for recall from org memory. FIRST action: `bash bin/search.sh query \"<the concept, not the literal sentence>\"` — one ranked call over all of memory/ (hybrid keyword+semantic; graph-state annotations attach automatically in connected mode). Read the top hits and cite paths. Do NOT improvise ls/grep/graph exploration first — hand-written graph queries against invented labels (e.g. :Handoff — handoffs are :Session nodes) return empty. Full spec: the search skill.",
63
65
  },
64
66
  }) + "\n");
65
67
  process.exit(0);
@@ -11,12 +11,35 @@ search before answering from conversation context alone.
11
11
 
12
12
  ## Flow
13
13
 
14
- 1. Run the search:
14
+ 1. Organizational recall must begin with the Egregore search entry point in
15
+ step 2. Do not resolve `memory/` to its sibling Git repository, change into
16
+ that absolute path, or improvise raw grep/listing as the first retrieval
17
+ action. Use direct file reads or grep only on evidence paths returned by the
18
+ ranked search.
19
+
20
+ Before any shell, file-read, grep, or other retrieval tool call, send exactly
21
+ one standalone commentary line:
22
+
23
+ - local/filesystem-only:
24
+ `⌕ Egregore · searching your organization’s memory`
25
+ - connected mode with automatic graph enrichment:
26
+ `⌕ Egregore Connect · searching your organization’s memory and relationships`
27
+
28
+ Send the line verbatim. Do not replace it with generic narration such as
29
+ “I’ll search org memory.” It must be visible before the tool call; captured
30
+ command output does not satisfy this requirement. Emit it once even when the
31
+ retrieval needs multiple queries or source reads.
32
+
33
+ 2. Run the search:
15
34
 
16
35
  ```bash
17
- bin/agent.sh search "your query" -n 6
36
+ bash bin/search.sh query "your query" -n 6
18
37
  ```
19
38
 
39
+ - The command also prints the attribution as a direct-shell fallback. That
40
+ captured banner is diagnostic redundancy, not the user-visible product beat.
41
+ If the answer instead begins with a graph-only named read, follow the same
42
+ global Egregore Retrieval Beat before that query.
20
43
  - Default is hybrid (BM25 + vector + rerank); it falls back to keyword
21
44
  search automatically when local models aren't built, with a note.
22
45
  - `--fast` — keyword only (BM25), instant. Use for exact terms: names,
@@ -27,15 +50,15 @@ bin/agent.sh search "your query" -n 6
27
50
  state lines (status, recipient, artifact type, quest) with no flag.
28
51
  `--no-enrich` skips it when speed matters; local mode never attempts it.
29
52
 
30
- 2. Search the *concept*, not the user's literal sentence. If the first query
53
+ 3. Search the *concept*, not the user's literal sentence. If the first query
31
54
  returns weak scores, run a second with different wording — one
32
55
  keyword-ish, one conceptual.
33
56
 
34
- 3. Results are snippets with `memory/...` file paths. Read the source file
57
+ 4. Results are snippets with `memory/...` file paths. Read the source file
35
58
  for anything you will cite or act on — snippets truncate, and memory
36
59
  files carry dates, authors, and caveats that change the answer.
37
60
 
38
- 4. Answer directly, citing file paths. If results are empty or weak, say so
61
+ 5. Answer directly, citing file paths. If results are empty or weak, say so
39
62
  plainly — "nothing in memory on X" is a real answer. Do not present
40
63
  conversation-context guesses as memory.
41
64