create-egregore 0.21.4 → 0.21.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/assets/egregore-launcher.js +88 -14
  2. package/bin/cli.js +6 -3
  3. package/lib/api.js +26 -22
  4. package/lib/connect-runtime.js +13 -8
  5. package/lib/hosted-api.js +70 -0
  6. package/lib/local.js +3 -17
  7. package/lib/setup.js +6 -1
  8. package/package.json +2 -2
  9. package/runtime/claude/.claude/context/telemetry.md +8 -18
  10. package/runtime/claude/.claude/skills/emissary/SKILL.md +1 -1
  11. package/runtime/claude/.claude/skills/telemetry/SKILL.md +3 -3
  12. package/runtime/claude/.claude/skills/update/SKILL.md +46 -32
  13. package/runtime/claude/bin/handoff-run.sh +1 -1
  14. package/runtime/claude/bin/hosting-ops.sh +5 -243
  15. package/runtime/claude/bin/lib/framework-update.sh +3 -1
  16. package/runtime/claude/bin/lib/git-sync.sh +2 -2
  17. package/runtime/claude/bin/notify.sh +1 -1
  18. package/runtime/claude/bin/post-update.sh +13 -0
  19. package/runtime/claude/bin/publish-artifact.sh +1 -1
  20. package/runtime/claude/bin/telemetry.sh +7 -10
  21. package/runtime/claude/egregore_runtime/contracts.py +38 -0
  22. package/runtime/claude/egregore_runtime/member_authority.py +405 -0
  23. package/runtime/claude/egregore_runtime/upgrade.py +41 -2
  24. package/runtime/claude/manifest.json +1 -1
  25. package/runtime/codex/bin/handoff-run.sh +1 -1
  26. package/runtime/codex/bin/hosting-ops.sh +5 -243
  27. package/runtime/codex/bin/lib/framework-update.sh +3 -1
  28. package/runtime/codex/bin/lib/git-sync.sh +2 -2
  29. package/runtime/codex/bin/notify.sh +1 -1
  30. package/runtime/codex/bin/post-update.sh +13 -0
  31. package/runtime/codex/bin/publish-artifact.sh +1 -1
  32. package/runtime/codex/bin/telemetry.sh +7 -10
  33. package/runtime/codex/egregore_runtime/contracts.py +38 -0
  34. package/runtime/codex/egregore_runtime/member_authority.py +405 -0
  35. package/runtime/codex/egregore_runtime/upgrade.py +41 -2
  36. package/runtime/codex/manifest.json +1 -1
  37. package/runtime/pi/bin/handoff-run.sh +1 -1
  38. package/runtime/pi/bin/hosting-ops.sh +5 -243
  39. package/runtime/pi/bin/lib/framework-update.sh +3 -1
  40. package/runtime/pi/bin/lib/git-sync.sh +2 -2
  41. package/runtime/pi/bin/notify.sh +1 -1
  42. package/runtime/pi/bin/post-update.sh +13 -0
  43. package/runtime/pi/bin/publish-artifact.sh +1 -1
  44. package/runtime/pi/bin/telemetry.sh +7 -10
  45. package/runtime/pi/egregore_runtime/contracts.py +38 -0
  46. package/runtime/pi/egregore_runtime/member_authority.py +405 -0
  47. package/runtime/pi/egregore_runtime/upgrade.py +41 -2
  48. package/runtime/pi/manifest.json +1 -1
  49. package/runtime/prime/bin/handoff-run.sh +1 -1
  50. package/runtime/prime/bin/hosting-ops.sh +5 -243
  51. package/runtime/prime/bin/lib/framework-update.sh +3 -1
  52. package/runtime/prime/bin/lib/git-sync.sh +2 -2
  53. package/runtime/prime/bin/notify.sh +1 -1
  54. package/runtime/prime/bin/post-update.sh +13 -0
  55. package/runtime/prime/bin/publish-artifact.sh +1 -1
  56. package/runtime/prime/bin/telemetry.sh +7 -10
  57. package/runtime/prime/egregore_runtime/contracts.py +38 -0
  58. package/runtime/prime/egregore_runtime/member_authority.py +405 -0
  59. package/runtime/prime/egregore_runtime/upgrade.py +41 -2
  60. package/runtime/prime/manifest.json +1 -1
  61. package/runtime/claude/bin/build-sealed-dataroom.mjs +0 -722
  62. package/runtime/codex/bin/build-sealed-dataroom.mjs +0 -722
  63. package/runtime/pi/bin/build-sealed-dataroom.mjs +0 -722
  64. package/runtime/prime/bin/build-sealed-dataroom.mjs +0 -722
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // egregore-launcher-version: 0.21.4
2
+ // egregore-launcher-version: 0.21.6
3
3
  //
4
4
  // egregore — launch and browse Egregore instances from your terminal.
5
5
  // Installed by create-egregore. Reads ~/.egregore/instances.json.
@@ -35,7 +35,7 @@ const crypto = require("node:crypto");
35
35
  const { spawnSync, spawn } = require("node:child_process");
36
36
  const readline = require("node:readline");
37
37
 
38
- const VERSION = "0.21.4";
38
+ const VERSION = "0.21.6";
39
39
  const REGISTRY = path.join(os.homedir(), ".egregore", "instances.json");
40
40
  const DEFAULT_FILE = path.join(os.homedir(), ".egregore", "default");
41
41
  const UPDATE_CACHE_FILE = path.join(os.homedir(), ".egregore", "update-check.json");
@@ -46,10 +46,72 @@ const UPDATE_REGISTRY_URL =
46
46
  const UPDATE_AVAILABLE_CACHE_TTL_MS = 6 * 60 * 60 * 1000;
47
47
  const UPDATE_FAILURE_CACHE_TTL_MS = 5 * 60 * 1000;
48
48
  const UPDATE_CHECK_TIMEOUT_MS = 1500;
49
- const CONNECT_API_URL = (
50
- process.env.EGREGORE_CONNECT_API_URL ||
51
- "https://egregore-production-55f2.up.railway.app"
52
- ).replace(/\/+$/, "");
49
+ // Hosted API address. The API moved to api.egregore.xyz after the October 2026
50
+ // security reset; its old origin is retired and must never receive a token or
51
+ // key, even from an egregore.json or environment saved before the move. The
52
+ // launcher is installed standalone, so this is an inline copy of
53
+ // lib/hosted-api.js; test/hosted-api.test.js holds the two in step and both
54
+ // to the rule in bin/tests/fixtures/hosted-api-url-vectors.json.
55
+ const HOSTED_API_ORIGIN = "https://api.egregore.xyz";
56
+ const RETIRED_API_ORIGIN = "https://egregore-production-55f2.up.railway.app";
57
+ const RETIRED_API_HOST = RETIRED_API_ORIGIN.slice("https://".length);
58
+ const REFUSED_API_ADDRESS_NOTICE = "The hosted services address isn't a plain web address, so nothing was sent. "
59
+ + `Correct api_url in egregore.json; the hosted address is ${HOSTED_API_ORIGIN}.`;
60
+ // A plain web address: http or https, an ASCII host of letters, digits, dots,
61
+ // hyphens and underscores, an optional port and a plain path. Every transport
62
+ // reads it the same way. Anything else is refused: curl, urllib and WHATWG
63
+ // parsers disagree about where userinfo, backslashes, percent-encoding, glob
64
+ // characters, queries, whitespace or non-ASCII send a request.
65
+ const PLAIN_API_URL = /^https?:\/\/([A-Za-z0-9._-]+)(:[0-9]{1,5})?(\/[A-Za-z0-9._~/-]*)?$/;
66
+
67
+ // Thrown for a refused address; callers send nothing.
68
+ class RefusedApiAddressError extends Error {
69
+ constructor() {
70
+ super(REFUSED_API_ADDRESS_NOTICE);
71
+ this.name = "RefusedApiAddressError";
72
+ this.code = "EGREGORE_REFUSED_API_ADDRESS";
73
+ }
74
+ }
75
+
76
+ // The saved text without surrounding ASCII whitespace.
77
+ function apiUrlText(value) {
78
+ const text = value === null || value === undefined ? "" : String(value);
79
+ return text.replace(/^[ \t\n\r\f\v]+|[ \t\n\r\f\v]+$/g, "");
80
+ }
81
+
82
+ // The plain address's host and path, the host flagged when it is the retired
83
+ // one (case and trailing dots ignored), or null when the text is not plain.
84
+ function plainApiUrl(text) {
85
+ const plain = PLAIN_API_URL.exec(text);
86
+ if (!plain) return null;
87
+ return { retired: plain[1].toLowerCase().replace(/\.+$/, "") === RETIRED_API_HOST, path: plain[3] || "" };
88
+ }
89
+
90
+ // True when the address is a plain address on the retired origin.
91
+ function retiredApiUrl(value) {
92
+ const plain = plainApiUrl(apiUrlText(value));
93
+ return plain !== null && plain.retired;
94
+ }
95
+
96
+ // The address to send requests to, trailing slashes trimmed: a plain retired
97
+ // address becomes the hosted API with its path kept; any other plain address
98
+ // keeps its text; empty stays empty. Anything else throws
99
+ // RefusedApiAddressError, whose message says why, and nothing is sent.
100
+ function hostedApiUrl(value) {
101
+ const text = apiUrlText(value);
102
+ if (!text) return "";
103
+ const plain = plainApiUrl(text);
104
+ if (plain === null) throw new RefusedApiAddressError();
105
+ return (plain.retired ? HOSTED_API_ORIGIN + plain.path : text).replace(/\/+$/, "");
106
+ }
107
+
108
+ const CONNECT_API_URL = (() => {
109
+ // A refused override leaves no address, so connectedApiRequest sends nothing.
110
+ try { return hostedApiUrl(process.env.EGREGORE_CONNECT_API_URL || HOSTED_API_ORIGIN); } catch (error) {
111
+ if (error instanceof RefusedApiAddressError) return "";
112
+ throw error;
113
+ }
114
+ })();
53
115
 
54
116
  const NATIVE_CODEX_SKILLS = [
55
117
  "activity",
@@ -1482,7 +1544,7 @@ function githubRequest(token, method, apiPath, body) {
1482
1544
  }
1483
1545
 
1484
1546
  function apiRequest(apiUrl, apiKey, apiPath, body) {
1485
- const url = new URL(apiPath, apiUrl.replace(/\/+$/, "") + "/");
1547
+ const url = new URL(apiPath, hostedApiUrl(apiUrl) + "/");
1486
1548
  return requestJson({
1487
1549
  hostname: url.hostname,
1488
1550
  port: url.port || undefined,
@@ -1494,6 +1556,7 @@ function apiRequest(apiUrl, apiKey, apiPath, body) {
1494
1556
  }
1495
1557
 
1496
1558
  function connectedApiRequest(githubToken, method, apiPath, body, headers = {}) {
1559
+ if (!CONNECT_API_URL) return Promise.reject(new RefusedApiAddressError());
1497
1560
  const url = new URL(apiPath, CONNECT_API_URL + "/");
1498
1561
  return requestJson({
1499
1562
  hostname: url.hostname,
@@ -1652,8 +1715,14 @@ function adapterErrorText(detail) {
1652
1715
 
1653
1716
  function connectError(result, fallback) {
1654
1717
  if (!result) return fallback;
1718
+ // Unavailability says nothing about the person's credentials, and an edge
1719
+ // or proxy page (HTML or plain text) is never printed raw.
1720
+ if ([502, 503, 504].includes(result.status)) {
1721
+ return "Egregore's hosted services are temporarily unavailable. Local memory and search still work.";
1722
+ }
1655
1723
  const data = result.data || {};
1656
- return adapterErrorText(data.detail || data.message || (typeof data === "string" ? data : fallback));
1724
+ if (typeof data === "string") return fallback;
1725
+ return adapterErrorText(data.detail || data.message || fallback);
1657
1726
  }
1658
1727
 
1659
1728
  function sleep(ms) {
@@ -1716,7 +1785,12 @@ function validatedConnectedClaim(config, response, expectedOrgIds = []) {
1716
1785
  typeof response?.api_key !== "string" || !response.api_key.trim()) {
1717
1786
  die("Connected did not return usable access settings. Local configuration is unchanged; run the same command to retry.");
1718
1787
  }
1719
- return { orgId, apiUrl: response.api_url, apiKey: response.api_key };
1788
+ let apiUrl = "";
1789
+ try { apiUrl = hostedApiUrl(response.api_url); } catch (error) {
1790
+ if (!(error instanceof RefusedApiAddressError)) throw error;
1791
+ die("Connected returned an address that isn't a plain web address. Local configuration is unchanged, and nothing was sent with its key.");
1792
+ }
1793
+ return { orgId, apiUrl, apiKey: response.api_key };
1720
1794
  }
1721
1795
 
1722
1796
  function hostedMemorySyncEnabled(config) {
@@ -1893,10 +1967,10 @@ async function cmdConnect(args) {
1893
1967
  !intentId ||
1894
1968
  !resumable
1895
1969
  ) {
1896
- // Local installs may have skipped platform registration (DO_NOT_TRACK
1897
- // gates it as telemetry). Connect requires it functionally, and choosing
1898
- // Connect is the consent — register on demand. Idempotent for members of
1899
- // already-registered orgs. Identity conflicts must stop before checkout.
1970
+ // Local installs never register with the platform. Connect requires it
1971
+ // functionally, and choosing Connect is the consent — register on demand.
1972
+ // Idempotent for members of already-registered orgs. Identity conflicts
1973
+ // must stop before checkout.
1900
1974
  const registration = await connectedApiRequest(githubToken, "POST", "/api/org/register", {
1901
1975
  org_name: cfg.org_name || inst.slug,
1902
1976
  github_org: cfg.github_org || "",
@@ -2239,7 +2313,7 @@ function tryNotifyInvite(inst, username, message, cfg, env) {
2239
2313
  }
2240
2314
 
2241
2315
  async function connectedInvite(inst, username, cfg, env) {
2242
- const apiUrl = cfg.api_url || "";
2316
+ const apiUrl = hostedApiUrl(cfg.api_url);
2243
2317
  const apiKey = env.EGREGORE_API_KEY || "";
2244
2318
  const ghToken = env.GITHUB_TOKEN || "";
2245
2319
  if (!apiUrl || !apiKey || !ghToken) return null;
package/bin/cli.js CHANGED
@@ -18,6 +18,7 @@ const path = require("node:path");
18
18
  const { spawnSync } = require("node:child_process");
19
19
  const ui = require("../lib/ui");
20
20
  const { EgregoreAPI } = require("../lib/api");
21
+ const { HOSTED_API_ORIGIN, hostedApiUrl } = require("../lib/hosted-api");
21
22
  const {
22
23
  canUseInteractiveBrowserFlow,
23
24
  deviceFlow,
@@ -40,7 +41,9 @@ const {
40
41
  const { detectContext, isWritable, isSystemDir } = require("../lib/context");
41
42
  const { commandExists } = require("../lib/path-lookup");
42
43
 
43
- const API_URL = process.env.EGREGORE_API_URL || "https://egregore-production-55f2.up.railway.app";
44
+ // Resolved when a hosted flow starts, so a refused EGREGORE_API_URL stops
45
+ // that flow with the reason and never falls back to another address.
46
+ function hostedApiSetting() { return hostedApiUrl(process.env.EGREGORE_API_URL || HOSTED_API_ORIGIN); }
44
47
  const INSTANCES_FILE = path.join(os.homedir(), ".egregore", "instances.json");
45
48
 
46
49
  function readLocalInstances() {
@@ -605,7 +608,7 @@ async function main() {
605
608
 
606
609
  if (args.token) {
607
610
  // ===== Connected mode: token provided =====
608
- const api = new EgregoreAPI(args.api || API_URL);
611
+ const api = new EgregoreAPI(args.api || hostedApiSetting());
609
612
  await tokenFlow(api, args.token, ctx);
610
613
  } else if (args.subcommand === "join") {
611
614
  // ===== Local mode: join existing =====
@@ -617,7 +620,7 @@ async function main() {
617
620
  await localJoinFlow(args.joinOrg, ui, ctx);
618
621
  } else if (args.connected) {
619
622
  // ===== Connected/hosted interactive flow (explicit opt-in) =====
620
- const api = new EgregoreAPI(args.api || API_URL);
623
+ const api = new EgregoreAPI(args.api || hostedApiSetting());
621
624
  await interactiveFlow(api, ctx);
622
625
  } else {
623
626
  // ===== Local founder is the DEFAULT — bare `npx create-egregore` and
package/lib/api.js CHANGED
@@ -5,8 +5,22 @@
5
5
 
6
6
  const https = require("node:https");
7
7
  const { URL } = require("node:url");
8
-
9
- const DEFAULT_API = "https://egregore-production-55f2.up.railway.app";
8
+ const { HOSTED_API_ORIGIN, hostedApiUrl } = require("./hosted-api");
9
+
10
+ const UNAVAILABLE =
11
+ "Egregore's hosted services are temporarily unavailable. Local memory and search still work.";
12
+
13
+ // A failed response becomes one short line. Proxy and edge pages are HTML or
14
+ // plain text, so a body that is not JSON is never shown to the person.
15
+ function responseError(statusCode, data) {
16
+ if ([502, 503, 504].includes(statusCode)) return new Error(UNAVAILABLE);
17
+ let detail = "";
18
+ try {
19
+ const json = JSON.parse(data);
20
+ if (json && typeof json.detail === "string") detail = json.detail;
21
+ } catch {}
22
+ return new Error(detail || `HTTP ${statusCode}`);
23
+ }
10
24
 
11
25
  function request(method, url, { body, headers = {}, timeout = 30000 } = {}) {
12
26
  return new Promise((resolve, reject) => {
@@ -28,19 +42,14 @@ function request(method, url, { body, headers = {}, timeout = 30000 } = {}) {
28
42
  let data = "";
29
43
  res.on("data", (chunk) => (data += chunk));
30
44
  res.on("end", () => {
45
+ if (res.statusCode >= 400) {
46
+ reject(responseError(res.statusCode, data));
47
+ return;
48
+ }
31
49
  try {
32
- const json = JSON.parse(data);
33
- if (res.statusCode >= 400) {
34
- reject(new Error(json.detail || `HTTP ${res.statusCode}`));
35
- } else {
36
- resolve(json);
37
- }
50
+ resolve(JSON.parse(data));
38
51
  } catch {
39
- if (res.statusCode >= 400) {
40
- reject(new Error(`HTTP ${res.statusCode}: ${data}`));
41
- } else {
42
- resolve(data);
43
- }
52
+ resolve(data);
44
53
  }
45
54
  });
46
55
  });
@@ -60,7 +69,9 @@ function request(method, url, { body, headers = {}, timeout = 30000 } = {}) {
60
69
 
61
70
  class EgregoreAPI {
62
71
  constructor(baseUrl) {
63
- this.base = (baseUrl || DEFAULT_API).replace(/\/$/, "");
72
+ // A retired address (an old --api or EGREGORE_API_URL) never receives the
73
+ // GitHub token these calls carry.
74
+ this.base = hostedApiUrl(baseUrl || HOSTED_API_ORIGIN);
64
75
  }
65
76
 
66
77
  async claimToken(token, githubToken) {
@@ -99,13 +110,6 @@ class EgregoreAPI {
99
110
  });
100
111
  }
101
112
 
102
- async registerOrg(githubToken, { org_name, github_org, slug, flow_variant, org_id, repo_name }) {
103
- return request("POST", `${this.base}/api/org/register`, {
104
- headers: { Authorization: `Bearer ${githubToken}` },
105
- body: { org_name, github_org, slug, ...(org_id ? { org_id } : {}), ...(repo_name ? { repo_name } : {}), ...(flow_variant ? { flow_variant } : {}) },
106
- });
107
- }
108
-
109
113
  async exchangeCode(code) {
110
114
  return request("POST", `${this.base}/api/auth/github/callback`, {
111
115
  body: { code },
@@ -117,4 +121,4 @@ class EgregoreAPI {
117
121
  }
118
122
  }
119
123
 
120
- module.exports = { EgregoreAPI, request };
124
+ module.exports = { EgregoreAPI, request, responseError, UNAVAILABLE };
@@ -8,6 +8,7 @@ const path = require("node:path");
8
8
  const { URL } = require("node:url");
9
9
 
10
10
  const PACKAGE = require("../package.json");
11
+ const { RefusedApiAddressError, hostedApiUrl } = require("./hosted-api");
11
12
  const LOCAL_MANIFEST = path.join(".egregore", "connect-runtime-manifest.json");
12
13
  const PACK_PATH = "/api/runtime/connect-pack";
13
14
  const MAX_PACK_BYTES = 12 * 1024 * 1024;
@@ -63,12 +64,14 @@ function connectCredentials(egregoreDir) {
63
64
  }
64
65
  const apiKey = readApiKey(egregoreDir);
65
66
  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
- };
67
+ // An address saved before the October 2026 reset never receives the key,
68
+ // and one that is not plain is refused: nothing is sent.
69
+ let apiUrl;
70
+ try { apiUrl = hostedApiUrl(config.api_url); } catch (error) {
71
+ if (error instanceof RefusedApiAddressError) return { eligible: false, reason: "refused-address" };
72
+ throw error;
73
+ }
74
+ return { eligible: true, reason: "connected", apiUrl, apiKey };
72
75
  }
73
76
 
74
77
  function connectEligibility(egregoreDir) {
@@ -152,8 +155,10 @@ function validateConnectPack(pack) {
152
155
  function requestConnectPack(apiUrl, apiKey, timeout = 30000) {
153
156
  return new Promise((resolve, reject) => {
154
157
  let parsed;
155
- try { parsed = new URL(`${apiUrl}${PACK_PATH}`); }
156
- catch { reject(new Error("Connected instance has an invalid API URL")); return; }
158
+ try { parsed = new URL(`${hostedApiUrl(apiUrl)}${PACK_PATH}`); } catch (error) {
159
+ reject(error instanceof RefusedApiAddressError ? error : new Error("Connected instance has an invalid API URL"));
160
+ return;
161
+ }
157
162
  const transport = parsed.protocol === "http:" ? http : https;
158
163
  const request = transport.request(parsed, {
159
164
  method: "GET",
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+
3
+ // The hosted API moved to api.egregore.xyz after the October 2026 security
4
+ // reset. Its old Railway origin is retired and no longer ours to trust: a
5
+ // token or key sent there could reach whoever holds that address next. Every
6
+ // request that carries a credential resolves its address here, so an
7
+ // egregore.json or environment saved before the move never reaches it.
8
+ //
9
+ // The rule and the table every resolver follows:
10
+ // bin/tests/fixtures/hosted-api-url-vectors.json. The launcher
11
+ // (assets/egregore-launcher.js) is installed standalone and carries an inline
12
+ // copy of the code below, and bin/connector-google/src/hosted-api.ts a typed
13
+ // one; test/hosted-api.test.js holds all three to the table and to each other.
14
+
15
+ const HOSTED_API_ORIGIN = "https://api.egregore.xyz";
16
+ const RETIRED_API_ORIGIN = "https://egregore-production-55f2.up.railway.app";
17
+ const RETIRED_API_HOST = RETIRED_API_ORIGIN.slice("https://".length);
18
+ const REFUSED_API_ADDRESS_NOTICE = "The hosted services address isn't a plain web address, so nothing was sent. "
19
+ + `Correct api_url in egregore.json; the hosted address is ${HOSTED_API_ORIGIN}.`;
20
+ // A plain web address: http or https, an ASCII host of letters, digits, dots,
21
+ // hyphens and underscores, an optional port and a plain path. Every transport
22
+ // reads it the same way. Anything else is refused: curl, urllib and WHATWG
23
+ // parsers disagree about where userinfo, backslashes, percent-encoding, glob
24
+ // characters, queries, whitespace or non-ASCII send a request.
25
+ const PLAIN_API_URL = /^https?:\/\/([A-Za-z0-9._-]+)(:[0-9]{1,5})?(\/[A-Za-z0-9._~/-]*)?$/;
26
+
27
+ // Thrown for a refused address; callers send nothing.
28
+ class RefusedApiAddressError extends Error {
29
+ constructor() {
30
+ super(REFUSED_API_ADDRESS_NOTICE);
31
+ this.name = "RefusedApiAddressError";
32
+ this.code = "EGREGORE_REFUSED_API_ADDRESS";
33
+ }
34
+ }
35
+
36
+ // The saved text without surrounding ASCII whitespace.
37
+ function apiUrlText(value) {
38
+ const text = value === null || value === undefined ? "" : String(value);
39
+ return text.replace(/^[ \t\n\r\f\v]+|[ \t\n\r\f\v]+$/g, "");
40
+ }
41
+
42
+ // The plain address's host and path, the host flagged when it is the retired
43
+ // one (case and trailing dots ignored), or null when the text is not plain.
44
+ function plainApiUrl(text) {
45
+ const plain = PLAIN_API_URL.exec(text);
46
+ if (!plain) return null;
47
+ return { retired: plain[1].toLowerCase().replace(/\.+$/, "") === RETIRED_API_HOST, path: plain[3] || "" };
48
+ }
49
+
50
+ // True when the address is a plain address on the retired origin.
51
+ function retiredApiUrl(value) {
52
+ const plain = plainApiUrl(apiUrlText(value));
53
+ return plain !== null && plain.retired;
54
+ }
55
+
56
+ // The address to send requests to, trailing slashes trimmed: a plain retired
57
+ // address becomes the hosted API with its path kept; any other plain address
58
+ // keeps its text; empty stays empty. Anything else throws
59
+ // RefusedApiAddressError, whose message says why, and nothing is sent.
60
+ function hostedApiUrl(value) {
61
+ const text = apiUrlText(value);
62
+ if (!text) return "";
63
+ const plain = plainApiUrl(text);
64
+ if (plain === null) throw new RefusedApiAddressError();
65
+ return (plain.retired ? HOSTED_API_ORIGIN + plain.path : text).replace(/\/+$/, "");
66
+ }
67
+
68
+ module.exports = {
69
+ HOSTED_API_ORIGIN, RETIRED_API_ORIGIN, REFUSED_API_ADDRESS_NOTICE, RefusedApiAddressError, retiredApiUrl, hostedApiUrl,
70
+ };
package/lib/local.js CHANGED
@@ -35,7 +35,6 @@ const {
35
35
  ensureInstallerMemberRole,
36
36
  run,
37
37
  } = require("./setup");
38
- const { EgregoreAPI } = require("./api");
39
38
  const { joinCommand } = require("./commands");
40
39
 
41
40
  // Template repo for new egregore instances (OSS)
@@ -1157,8 +1156,6 @@ async function localFounderFlow(ui, ctx = {}) {
1157
1156
  base_branch: "main",
1158
1157
  repos: selectedRepos,
1159
1158
  upstream_url: "https://github.com/egregore-labs/egregore.git",
1160
- report_url: "https://xgksfrirtdumacvmfkzj.supabase.co",
1161
- report_key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inhna3NmcmlydGR1bWFjdm1ma3pqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA5OTA1ODksImV4cCI6MjA4NjU2NjU4OX0.1UWNgER6nQazrayly1KDrswClURUkGKxeirZg5L8uk4",
1162
1159
  });
1163
1160
  // Joiners resolve the org by reading egregore.json from the remote — if
1164
1161
  // this push silently fails (observed: transient failure right after
@@ -1179,20 +1176,9 @@ async function localFounderFlow(ui, ctx = {}) {
1179
1176
  ui.info(` Re-run this installer to retry, or push egregore.json from the cloned repo.`);
1180
1177
  }
1181
1178
 
1182
- // 8b. Register org with Egregore API (non-blocking — local mode works without it)
1183
- // Respects DO_NOT_TRACK (https://consult.gov.uk/digital-communications/online-advertising/supporting_documents/DNT%20standard%20v10.pdf)
1184
- if (!process.env.DO_NOT_TRACK && !process.env.EGREGORE_NO_TELEMETRY) {
1185
- try {
1186
- const api = new EgregoreAPI();
1187
- await api.registerOrg(githubToken, {
1188
- org_name: orgName,
1189
- github_org: githubOrg,
1190
- slug,
1191
- org_id: egreConfig.org_id,
1192
- repo_name: repoName,
1193
- });
1194
- } catch {}
1195
- }
1179
+ // A Local install registers nothing with a hosted service. The GitHub token
1180
+ // goes to GitHub only; `egregore connect` registers the organization when a
1181
+ // person explicitly chooses Connected.
1196
1182
 
1197
1183
  // 9. Init memory dirs on remote (so joiners get the structure)
1198
1184
  const memDirs = ["people", "identity", "invites", "handoffs", "knowledge/questions", "knowledge/decisions", "knowledge/patterns", "knowledge/findings", "quests", "wraps"];
package/lib/setup.js CHANGED
@@ -8,6 +8,7 @@ const fs = require("node:fs");
8
8
  const path = require("node:path");
9
9
  const os = require("node:os");
10
10
  const { runRendererInstaller } = require("./renderer-install");
11
+ const { RefusedApiAddressError, hostedApiUrl } = require("./hosted-api");
11
12
  const {
12
13
  hasCodexRuntimeSupport,
13
14
  installCodexRuntime,
@@ -849,7 +850,11 @@ function ensureConnectedConfig(repoDir, data, ui) {
849
850
  }
850
851
 
851
852
  let changed = false;
852
- const tokenApiUrl = data.api_url ? String(data.api_url).replace(/\/$/, "") : "";
853
+ let tokenApiUrl = "";
854
+ try { tokenApiUrl = data.api_url ? hostedApiUrl(data.api_url) : ""; } catch (error) {
855
+ if (!(error instanceof RefusedApiAddressError)) throw error;
856
+ throw new Error("Connected setup returned an address that isn't a plain web address; egregore.json is unchanged.");
857
+ }
853
858
  if (tokenApiUrl && config.api_url !== tokenApiUrl) {
854
859
  config.api_url = tokenApiUrl;
855
860
  changed = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-egregore",
3
- "version": "0.21.4",
3
+ "version": "0.21.6",
4
4
  "description": "Set up Egregore for your team in one command \u2014 installs the egregore launcher and your choice of Claude Code, Codex, or Pi.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "test:e2e": "node --test test/e2e-local.test.js",
20
20
  "test:all": "npm run test:runtime && node --test test/",
21
21
  "test:runtime": "node ../../bin/capability-distribution.mjs validate --root ../.. && node scripts/build-runtime-bundle.mjs --check && node scripts/build-claude-runtime-bundle.mjs --check && node scripts/build-pi-runtime-bundle.mjs --check && node scripts/build-prime-runtime-bundle.mjs --check && node ../../bin/prime-render-spec.mjs --check && node ../../bin/pi-render-spec.mjs --check && node ../../api/scripts/build-connect-runtime-pack.mjs --check && bash ../../bin/tests/test-codex-native-skills.sh && bash ../../bin/tests/test-codex-spec-render.sh && node ../../bin/capability-distribution.mjs skill-contract --root ../.. --strict-contract && bash ../../bin/tests/test-bundle-skill-scripts.sh && node --test test/bundle-skill-scripts.test.js",
22
- "test:package": "node --test --test-concurrency=1 test/invite-install.test.js 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/runtime-identity.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/claude-runtime.test.js test/codex-runtime.test.js test/pi-runtime.test.js test/prime-runtime.test.js test/prime-branch-policy.test.js test/prime-bridge-skill.test.js test/pi-observe-context.test.js test/native-command-observe.test.js test/prime-rpc-driver.test.js test/pi-session-smoke.test.js test/prime-session-smoke.test.js test/pi-product-workflows.test.js test/installer-runtime-source.test.js test/runtime-ownership.test.js test/installer-finalization.test.js test/renderer-install.test.js test/runtime-readiness.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/installer-connect-choice.test.js test/launcher-connected-status.test.js test/launcher-telemetry.test.js test/launcher-runtime.test.js test/launcher-settings.test.js test/launcher-runtime-candidate.test.js test/launcher-permissions.test.js test/launcher-update.test.js test/harness-add.test.js test/connected-config.test.js test/workflow-settings.test.js"
22
+ "test:package": "node --test --test-concurrency=1 test/invite-install.test.js test/capability-distribution.test.js test/connect-runtime.test.js test/hosted-api.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/runtime-identity.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/claude-runtime.test.js test/codex-runtime.test.js test/pi-runtime.test.js test/prime-runtime.test.js test/prime-branch-policy.test.js test/prime-bridge-skill.test.js test/pi-observe-context.test.js test/native-command-observe.test.js test/prime-rpc-driver.test.js test/pi-session-smoke.test.js test/prime-session-smoke.test.js test/pi-product-workflows.test.js test/installer-runtime-source.test.js test/runtime-ownership.test.js test/installer-finalization.test.js test/renderer-install.test.js test/runtime-readiness.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/installer-connect-choice.test.js test/launcher-connected-status.test.js test/launcher-telemetry.test.js test/launcher-runtime.test.js test/launcher-settings.test.js test/launcher-runtime-candidate.test.js test/launcher-permissions.test.js test/launcher-update.test.js test/harness-add.test.js test/connected-config.test.js test/workflow-settings.test.js"
23
23
  },
24
24
  "files": [
25
25
  "bin/",
@@ -162,21 +162,11 @@ can explicitly choose a public upstream report. An internal issue stays within
162
162
  its organization's memory or configured tracker; it does not automatically
163
163
  reach Egregore support. `/wrap` does not collect or submit session feedback.
164
164
 
165
- The older `bin/session-report.sh` is a feedback-storage compatibility helper,
166
- not a support-reporting route. It inserts selected feedback into `session_reports`
167
- when `report_url` and `report_key` are configured. A `submitted` receipt means
168
- only that storage accepted the feedback: no GitHub issue was created and no
169
- Telegram message was sent. The authenticated database callback acknowledges
170
- storage without planning or dispatching a notification to a named teammate.
171
- Transport failure saves feedback locally under `~/.egregore/reports/`; that
172
- does not establish remote storage or delivery. Missing configuration skips
173
- submission.
174
-
175
- Never invoke legacy feedback automatically, generate it from a transcript, or
176
- use it as a fallback when private support fails. Only explicitly selected and
177
- reviewed content may be shared. The helper adds legacy user and organization
178
- labels; it does not guarantee anonymization or scrub arbitrary input.
179
-
180
- Legacy feedback can be disabled with `EGREGORE_NO_REPORTS=1` or
181
- `DO_NOT_TRACK=1` in the process environment, or `"session_reports": false` in
182
- `.egregore-state.json`. These switches do not grant consent to submit content.
165
+ Anonymous session feedback is retired. `bin/session-report.sh` remains only as
166
+ a compatibility notice: it never reads the submitted content, sends a request,
167
+ or saves a report. Old `report_url` and `report_key` configuration fields are
168
+ ignored. Existing local feedback files are left untouched.
169
+
170
+ Never invoke legacy feedback automatically or use it as a fallback when private
171
+ support fails. Use the reviewed issue workflow above; private support retains
172
+ its authenticated organization identity and exact preview/confirmation boundary.
@@ -206,7 +206,7 @@ Below the box, one line:
206
206
  [open emissary →]({url})
207
207
  ```
208
208
 
209
- Share the bare page link — not a `/raw` URL or a "Run this … packet" prompt. With the emissary skill installed, pasting the link is enough; this de-alarmed handover (see the data-room variant-A flow in `bin/build-sealed-dataroom.mjs`) avoids tripping a recipient agent's prompt-injection alarms.
209
+ Share the bare page link — not a `/raw` URL or a "Run this … packet" prompt. With the emissary skill installed, pasting the link is enough; this de-alarmed handover avoids tripping a recipient agent's prompt-injection alarms.
210
210
 
211
211
  If natural, add one short sentence of sign-off — what the recipient should do next, or "shared publicly — anyone with the link can run it."
212
212
 
@@ -86,9 +86,9 @@ command containing its one-time token. A changed target, changed buffer,
86
86
  expired token, or bad token requires a new proposal. Local events remain after
87
87
  a successful share.
88
88
 
89
- Local instances have no default outbound sink. They may export, name an
90
- explicit endpoint, or explicitly opt into the legacy public relay with
91
- `--legacy-public-relay`; never select that relay automatically.
89
+ Local instances have no default outbound sink. They may export or name an
90
+ explicit endpoint. The former public relay is retired: `--legacy-public-relay`
91
+ now refuses and sends nothing.
92
92
 
93
93
  ### clear
94
94
 
@@ -137,32 +137,6 @@ if [ '{in_worktree}' = "true" ]; then
137
137
  fi
138
138
  ```
139
139
 
140
- If `{in_worktree}` is `true`, review the changes and continue the worktree update:
141
-
142
- ```bash
143
- if [ '{in_worktree}' = "true" ]; then
144
- # Show what changed
145
- git -C '{main_dir}' diff --stat HEAD
146
-
147
- # Commit on base branch in main repo (.codex/skills/ carries the
148
- # regenerated adapters)
149
- git -C '{main_dir}' add -A .claude/ .pi/ .prime/ bin/ loom/ CLAUDE.md skills/ AGENTS.md .codex/spec-manifest.json .codex/skills/ 2>/dev/null
150
- if ! git -C '{main_dir}' diff --cached --quiet 2>/dev/null; then
151
- EGREGORE_FRAMEWORK_UPDATE=1 git -C '{main_dir}' commit -m "Update Egregore framework from upstream"
152
- git -C '{main_dir}' push origin '{base}' --quiet 2>/dev/null || true
153
- fi
154
-
155
- # Rebase worktree branch onto updated base
156
- git stash --quiet 2>/dev/null || true
157
- git fetch origin '{base}' --quiet
158
- if ! git rebase 'origin/{base}' --quiet 2>/dev/null; then
159
- git rebase --abort 2>/dev/null || true
160
- echo '⚠ Rebase had conflicts — aborted. Run: git rebase origin/{base} and resolve manually.'
161
- fi
162
- git stash pop --quiet 2>/dev/null || true
163
- fi
164
- ```
165
-
166
140
  ### Non-worktree path
167
141
 
168
142
  If `{in_worktree}` is `false`, use `false` as `{switched}` initially.
@@ -219,30 +193,70 @@ if [ '{in_worktree}' = "false" ]; then
219
193
  fi
220
194
  ```
221
195
 
222
- **Framework paths synced:** `bin/`, `.claude/commands/`, `.claude/skills/`, `.claude/hooks/`, `.claude/context/`, `.claude/agents/`, `.pi/`, `.prime/`, `loom/`, `CLAUDE.md`, `skills/`
196
+ **Framework paths synced:** `bin/`, `egregore_runtime/`, `.claude/commands/`, `.claude/skills/`, `.claude/hooks/`, `.claude/context/`, `.claude/agents/`, `.pi/`, `.prime/`, `loom/`, `CLAUDE.md`, `skills/`
223
197
  **Regenerated:** `AGENTS.md` + `.codex/spec-manifest.json` via `bin/codex-render-spec.mjs`, `.pi/APPEND_SYSTEM.md` + `.pi/spec-manifest.json` via `bin/pi-render-spec.mjs`, `.prime/agent/APPEND_SYSTEM.md` + `.prime/agent/spec-manifest.json` via `bin/prime-render-spec.mjs`, then generated Codex adapter skills via `bin/codex-sync-skills.sh`, so all derived harnesses stay aligned with `CLAUDE.md` and the canonical skills.
224
198
  **Never touched:** `egregore.json`, `.env`, `memory/`, `.egregore-state.json`, `.mcp.json`
225
199
  **Org-owned skills:** names in `egregore.json` → `owned_skills[]` are restored from the org's committed state after the overlay (`bin/restore-owned-skills.sh`) — upstream never overwrites them; collisions are reported instead.
226
200
 
227
201
  ## Step 3: Post-update migrations
228
202
 
229
- After syncing, run `bin/post-update.sh` if it exists. In worktrees, run from `{main_dir}` to ensure the updated version executes even if the rebase didn't complete.
203
+ After syncing, run the updated `bin/post-update.sh` before committing or rebasing.
204
+ The first update through an older overlay can add the missing Runtime core here.
205
+ In worktrees, run in `{main_dir}` so migrations affect the main checkout.
206
+ Stop on migration failure; do not commit or rebase a partial update.
230
207
 
231
208
  ```bash
232
209
  if [ '{in_worktree}' = "true" ]; then
233
- [ -x '{main_dir}/bin/post-update.sh' ] && bash '{main_dir}/bin/post-update.sh'
210
+ if [ -f '{main_dir}/bin/post-update.sh' ]; then
211
+ (cd '{main_dir}' && bash bin/post-update.sh) || exit 1
212
+ fi
234
213
  else
235
- [ -x bin/post-update.sh ] && bash bin/post-update.sh
214
+ if [ -f bin/post-update.sh ]; then
215
+ bash bin/post-update.sh || exit 1
216
+ fi
236
217
  fi
237
218
  ```
238
219
 
239
- ## Step 4: Commit and switch back (non-worktree only)
220
+ ## Step 4: Commit and restore the working branch
221
+
222
+ If `{upstream_url}` is `none`, skip this step and continue with Step 5; no
223
+ framework overlay was applied. Otherwise, commit the overlay and migrations
224
+ together before the working branch receives the update.
225
+
226
+ If `{in_worktree}` is `true`, review the changes and continue the worktree update:
227
+
228
+ ```bash
229
+ if [ '{in_worktree}' = "true" ]; then
230
+ # Show what changed
231
+ git -C '{main_dir}' diff --stat HEAD
232
+
233
+ # Commit on base branch in main repo (.codex/skills/ carries the
234
+ # regenerated adapters)
235
+ git -C '{main_dir}' add -A .claude/ .pi/ .prime/ bin/ loom/ CLAUDE.md skills/ AGENTS.md .codex/spec-manifest.json .codex/skills/ 2>/dev/null
236
+ # Older upstreams lack the Runtime core; a missing pathspec would abort the add.
237
+ [ ! -e '{main_dir}/egregore_runtime' ] || git -C '{main_dir}' add -A -- egregore_runtime/ 2>/dev/null
238
+ if ! git -C '{main_dir}' diff --cached --quiet 2>/dev/null; then
239
+ EGREGORE_FRAMEWORK_UPDATE=1 git -C '{main_dir}' commit -m "Update Egregore framework from upstream"
240
+ git -C '{main_dir}' push origin '{base}' --quiet 2>/dev/null || true
241
+ fi
242
+
243
+ # Rebase worktree branch onto updated base
244
+ git stash --quiet 2>/dev/null || true
245
+ git fetch origin '{base}' --quiet
246
+ if ! git rebase 'origin/{base}' --quiet 2>/dev/null; then
247
+ git rebase --abort 2>/dev/null || true
248
+ echo '⚠ Rebase had conflicts — aborted. Run: git rebase origin/{base} and resolve manually.'
249
+ fi
250
+ git stash pop --quiet 2>/dev/null || true
251
+ fi
252
+ ```
240
253
 
241
- Worktree path already committed in Step 2. This handles the non-worktree case:
254
+ For the non-worktree case:
242
255
 
243
256
  ```bash
244
257
  if [ '{in_worktree}' = "false" ]; then
245
258
  git add -A .claude/ .pi/ .prime/ bin/ loom/ CLAUDE.md skills/ AGENTS.md .codex/spec-manifest.json 2>/dev/null
259
+ [ ! -e egregore_runtime ] || git add -A -- egregore_runtime/ 2>/dev/null
246
260
  if ! git diff --cached --quiet 2>/dev/null; then
247
261
  EGREGORE_FRAMEWORK_UPDATE=1 git commit -m "Update Egregore framework from upstream"
248
262
  git push origin '{base}' --quiet 2>/dev/null || true