@bcmyguest/openmem 0.0.0-stage → 0.8.0

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 (230) hide show
  1. package/.agents/skills/openmem/SKILL.md +34 -0
  2. package/LICENSE +190 -0
  3. package/README.md +140 -2
  4. package/bin/hindsight-project-index +530 -0
  5. package/cli/coding-agent/access-policy.mjs +38 -0
  6. package/cli/coding-agent/agents.mjs +34 -0
  7. package/cli/coding-agent/bank-from-cwd.mjs +140 -0
  8. package/cli/coding-agent/capabilities.mjs +34 -0
  9. package/cli/coding-agent/context-store.mjs +81 -0
  10. package/cli/coding-agent/context-trim.mjs +897 -0
  11. package/cli/coding-agent/file-io.mjs +72 -0
  12. package/cli/coding-agent/hook-harnesses.mjs +88 -0
  13. package/cli/coding-agent/hook-runner.mjs +284 -0
  14. package/cli/coding-agent/install-access.mjs +119 -0
  15. package/cli/coding-agent/install-context-proxy.mjs +333 -0
  16. package/cli/coding-agent/install-rtk.mjs +60 -0
  17. package/cli/coding-agent/install-shim.mjs +201 -0
  18. package/cli/coding-agent/opencode-config.mjs +107 -0
  19. package/cli/coding-agent/opencode-plugin.mjs +132 -0
  20. package/cli/coding-agent/opencode2-plugin/index.js +299 -0
  21. package/cli/coding-agent/recall-frame.mjs +29 -0
  22. package/cli/coding-agent/remove-upstream-access.mjs +122 -0
  23. package/cli/coding-agent/runtime.mjs +40 -0
  24. package/cli/coding-agent/session-db.mjs +626 -0
  25. package/cli/coding-agent/session-facade.mjs +86 -0
  26. package/cli/coding-agent/toml-edit.mjs +161 -0
  27. package/cli/cognee-mcp.mjs +260 -0
  28. package/cli/context-management.mjs +254 -0
  29. package/cli/feed-sync.mjs +27 -0
  30. package/cli/install-coding-agent.mjs +298 -0
  31. package/cli/install-memory-mcp.mjs +426 -0
  32. package/cli/is-main.mjs +18 -0
  33. package/cli/mcp/context-sql-runner.mjs +58 -0
  34. package/cli/mcp/context-store.mjs +670 -0
  35. package/cli/mcp/context.mjs +61 -0
  36. package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
  37. package/cli/mcp/export/cognee.mjs +119 -0
  38. package/cli/mcp/export/hindsight.mjs +48 -0
  39. package/cli/mcp/export/index.mjs +26 -0
  40. package/cli/mcp/export/mem0.mjs +31 -0
  41. package/cli/mcp/knowledge.mjs +333 -0
  42. package/cli/mcp/llm-synthesize.mjs +50 -0
  43. package/cli/mcp/mem0-scope-id.mjs +22 -0
  44. package/cli/mcp/memory-facade.mjs +521 -0
  45. package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
  46. package/cli/mcp/notebooklm-banks.mjs +21 -0
  47. package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
  48. package/cli/mcp/notebooklm-provider.mjs +225 -0
  49. package/cli/mcp/pmm-records.mjs +23 -0
  50. package/cli/mcp/portability.mjs +158 -0
  51. package/cli/mcp/stdio.mjs +65 -0
  52. package/cli/mcp/unified-mcp-install.mjs +65 -0
  53. package/cli/mem0-mcp.mjs +108 -0
  54. package/cli/notebooklm-mcp.mjs +236 -0
  55. package/cli/openmem-mcp.mjs +38 -0
  56. package/cli/openmem.mjs +222 -0
  57. package/cli/roots.mjs +85 -0
  58. package/cli/schedule.mjs +176 -0
  59. package/cli/stack.mjs +438 -0
  60. package/cli/switch-memory-provider.mjs +358 -0
  61. package/cli/wiring-report.mjs +125 -0
  62. package/compose.bench.yaml +12 -0
  63. package/compose.yaml +227 -0
  64. package/config/coding-agent.json +13 -0
  65. package/config/hindsight.env.example +26 -0
  66. package/config/index.yml +54 -0
  67. package/config/litellm.lemond.yaml +40 -0
  68. package/config/litellm.ollama.yaml +33 -0
  69. package/config/notebooklm.yml +33 -0
  70. package/config/openmem.toml.example +124 -0
  71. package/docs/OPENMEM.md +23 -0
  72. package/docs/architecture.md +245 -0
  73. package/docs/assets/README.md +10 -0
  74. package/docs/assets/setup-demo.gif +0 -0
  75. package/docs/coding-agent-hooks.md +75 -0
  76. package/docs/coding-agent-scopes.md +22 -0
  77. package/docs/coding-agents.md +71 -0
  78. package/docs/cognee.md +37 -0
  79. package/docs/compatibility.md +80 -0
  80. package/docs/concepts.md +135 -0
  81. package/docs/context-management.md +255 -0
  82. package/docs/context-proxy.md +115 -0
  83. package/docs/context-research.md +215 -0
  84. package/docs/context-store.md +277 -0
  85. package/docs/hindsight.md +77 -0
  86. package/docs/index-sources-roadmap.md +38 -0
  87. package/docs/index-sources.md +39 -0
  88. package/docs/indexer-sinks.md +122 -0
  89. package/docs/indexing.md +57 -0
  90. package/docs/manual-stack-setup.md +74 -0
  91. package/docs/mcp-install-guide.md +4 -0
  92. package/docs/mcp-setup.md +104 -0
  93. package/docs/mem0.md +141 -0
  94. package/docs/memory-portability.md +73 -0
  95. package/docs/memory-providers.md +246 -0
  96. package/docs/meridian-litellm.md +198 -0
  97. package/docs/model-configuration.md +106 -0
  98. package/docs/notebooklm.md +83 -0
  99. package/docs/npx-install.md +172 -0
  100. package/docs/openmem-config.md +194 -0
  101. package/docs/provider-wizard-hooks.md +222 -0
  102. package/docs/quickstart.md +107 -0
  103. package/docs/setup-wizard.md +117 -0
  104. package/docs/sources-feeds.md +39 -0
  105. package/docs/sources-readwise.md +23 -0
  106. package/docs/sources-zotero.md +22 -0
  107. package/glama.json +4 -0
  108. package/indexer/Dockerfile +38 -0
  109. package/indexer/code_outline.py +146 -0
  110. package/indexer/feed_sync.py +107 -0
  111. package/indexer/index_config.py +290 -0
  112. package/indexer/pytest.ini +5 -0
  113. package/indexer/sinks/__init__.py +148 -0
  114. package/indexer/sinks/_http.py +44 -0
  115. package/indexer/sinks/cognee.py +329 -0
  116. package/indexer/sinks/hindsight.py +107 -0
  117. package/indexer/sinks/mem0.py +247 -0
  118. package/indexer/watch.py +240 -0
  119. package/package.json +96 -4
  120. package/providers/_template/compose.yaml +52 -0
  121. package/providers/cognee/OPERATOR.md +82 -0
  122. package/providers/cognee/compose.yaml +89 -0
  123. package/providers/cognee/lib/clack.mjs +157 -0
  124. package/providers/cognee/lib/coding-agent.mjs +26 -0
  125. package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
  126. package/providers/cognee/lib/config.mjs +98 -0
  127. package/providers/cognee/lib/env-file.mjs +194 -0
  128. package/providers/cognee/lib/env-setup.mjs +83 -0
  129. package/providers/cognee/lib/llm-env.mjs +382 -0
  130. package/providers/cognee/lib/ports.mjs +25 -0
  131. package/providers/cognee/lib/smoke.mjs +246 -0
  132. package/providers/cognee/lib/stack-env.mjs +134 -0
  133. package/providers/cognee/lib/state.mjs +101 -0
  134. package/providers/cognee/lib/summary.mjs +56 -0
  135. package/providers/cognee/lib/validate.mjs +97 -0
  136. package/providers/cognee/manifest.json +56 -0
  137. package/providers/cognee/wizard.mjs +134 -0
  138. package/providers/graphiti/manifest.json +15 -0
  139. package/providers/hindsight/lib/clack.mjs +347 -0
  140. package/providers/hindsight/lib/coding-agent.mjs +204 -0
  141. package/providers/hindsight/lib/config.mjs +79 -0
  142. package/providers/hindsight/lib/env-setup.mjs +72 -0
  143. package/providers/hindsight/lib/litellm-routes.mjs +196 -0
  144. package/providers/hindsight/lib/meridian.mjs +307 -0
  145. package/providers/hindsight/lib/model-backend.mjs +243 -0
  146. package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
  147. package/providers/hindsight/lib/ollama-compose.mjs +51 -0
  148. package/providers/hindsight/lib/ports.mjs +30 -0
  149. package/providers/hindsight/lib/probe.mjs +54 -0
  150. package/providers/hindsight/lib/stack-env.mjs +125 -0
  151. package/providers/hindsight/lib/state.mjs +80 -0
  152. package/providers/hindsight/lib/summary.mjs +57 -0
  153. package/providers/hindsight/lib/validate.mjs +81 -0
  154. package/providers/hindsight/manifest.json +58 -0
  155. package/providers/hindsight/wizard.mjs +150 -0
  156. package/providers/letta/manifest.json +29 -0
  157. package/providers/mem0/OPERATOR.md +143 -0
  158. package/providers/mem0/compose.yaml +79 -0
  159. package/providers/mem0/lib/coding-agent.mjs +58 -0
  160. package/providers/mem0/lib/config.mjs +45 -0
  161. package/providers/mem0/lib/configure.mjs +201 -0
  162. package/providers/mem0/lib/dotenv.mjs +71 -0
  163. package/providers/mem0/lib/env-file.mjs +110 -0
  164. package/providers/mem0/lib/http.mjs +69 -0
  165. package/providers/mem0/lib/litellm.mjs +457 -0
  166. package/providers/mem0/lib/mcp-install.mjs +165 -0
  167. package/providers/mem0/lib/secret.mjs +30 -0
  168. package/providers/mem0/lib/smoke.mjs +179 -0
  169. package/providers/mem0/lib/stack-env.mjs +255 -0
  170. package/providers/mem0/lib/state.mjs +67 -0
  171. package/providers/mem0/lib/summary.mjs +88 -0
  172. package/providers/mem0/lib/url.mjs +106 -0
  173. package/providers/mem0/lib/validate.mjs +158 -0
  174. package/providers/mem0/manifest.json +47 -0
  175. package/providers/mem0/wizard.mjs +146 -0
  176. package/providers/zep/manifest.json +29 -0
  177. package/proxy/config.yaml +33 -0
  178. package/proxy/openmem_context/__init__.py +6 -0
  179. package/proxy/openmem_context/adapters.py +504 -0
  180. package/proxy/openmem_context/compress.py +53 -0
  181. package/proxy/openmem_context/core.py +606 -0
  182. package/proxy/openmem_context/embed.py +212 -0
  183. package/proxy/openmem_context/recall.py +49 -0
  184. package/proxy/openmem_context/session.py +70 -0
  185. package/proxy/openmem_context/sessiondb.py +276 -0
  186. package/proxy/openmem_context/settings.py +134 -0
  187. package/proxy/openmem_context/store.py +129 -0
  188. package/proxy/openmem_context/tokens.py +123 -0
  189. package/proxy/openmem_context/ttl.py +78 -0
  190. package/proxy/openmem_context_hook.py +34 -0
  191. package/scripts/check-pack.mjs +219 -0
  192. package/scripts/gen-masks.mjs +83 -0
  193. package/scripts/gen-masks.sh +4 -0
  194. package/scripts/promotion-provider-smoke.mjs +34 -0
  195. package/scripts/run-setup.sh +30 -0
  196. package/scripts/setup.mjs +107 -0
  197. package/scripts/setup.sh +5 -0
  198. package/scripts/spin-up.sh +45 -0
  199. package/scripts/wizard.sh +5 -0
  200. package/server.json +34 -0
  201. package/sync/Dockerfile +27 -0
  202. package/sync/memory_backend.py +87 -0
  203. package/sync/notebooklm_sync.py +664 -0
  204. package/wizard/bin/pm.mjs +97 -0
  205. package/wizard/bin/setup.mjs +33 -0
  206. package/wizard/lib/compose-version.mjs +32 -0
  207. package/wizard/lib/compose.mjs +70 -0
  208. package/wizard/lib/context-step.mjs +140 -0
  209. package/wizard/lib/curl.mjs +103 -0
  210. package/wizard/lib/docker-build.mjs +148 -0
  211. package/wizard/lib/exec.mjs +41 -0
  212. package/wizard/lib/indexer.mjs +6 -0
  213. package/wizard/lib/litellm-env.mjs +55 -0
  214. package/wizard/lib/load-machine-env.mjs +4 -0
  215. package/wizard/lib/log.mjs +32 -0
  216. package/wizard/lib/notebooklm-agent-access.mjs +92 -0
  217. package/wizard/lib/notebooklm.mjs +214 -0
  218. package/wizard/lib/parse-args.mjs +87 -0
  219. package/wizard/lib/paths.mjs +39 -0
  220. package/wizard/lib/pm-config.mjs +371 -0
  221. package/wizard/lib/ports.mjs +94 -0
  222. package/wizard/lib/prompts.mjs +85 -0
  223. package/wizard/lib/provider-wizard/helpers.mjs +98 -0
  224. package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
  225. package/wizard/lib/providers/registry.mjs +262 -0
  226. package/wizard/lib/run-clack.mjs +108 -0
  227. package/wizard/lib/run-wizard.mjs +750 -0
  228. package/wizard/lib/validate-setup.mjs +62 -0
  229. package/wizard/lib/wizard-action.mjs +34 -0
  230. package/wizard/lib/wizard-conf.mjs +71 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Small dotenv-style file helpers for Cognee's operator env files. They read and replace
3
+ * KEY=value lines and leave every other line (comments, blank lines, unrelated keys) alone.
4
+ * Nothing here logs; callers print key NAMES and paths only.
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+
9
+ const KEY_RE = /^[A-Z][A-Z0-9_]*$/;
10
+
11
+ /**
12
+ * The value of one assignment as Docker Compose reads it: a double-quoted value has `\\`, `\"`
13
+ * and `\$` unescaped (the escapes formatValue writes), a single-quoted value is literal, and
14
+ * anything else is taken as is.
15
+ * @param {string} raw the text after `=`
16
+ */
17
+ function unquote(raw) {
18
+ const dq = /^"(.*)"$/.exec(raw);
19
+ if (dq) return dq[1].replace(/\\(["\\$])/g, "$1");
20
+ const sq = /^'(.*)'$/.exec(raw);
21
+ if (sq) return sq[1];
22
+ return raw;
23
+ }
24
+
25
+ /**
26
+ * Parse a dotenv-style text. Parsing is line-based: a value never spans lines (formatValue
27
+ * refuses to write one), so a line that merely looks like an assignment is an assignment.
28
+ * Accepts `export KEY=value` and CRLF line ends; the last assignment of a key wins.
29
+ * @param {string} text
30
+ * @returns {Map<string, string>}
31
+ */
32
+ export function parseEnvText(text) {
33
+ /** @type {Map<string, string>} */
34
+ const values = new Map();
35
+ for (const raw of text.split(/\r?\n/)) {
36
+ const line = raw.trim().replace(/^export\s+/, "");
37
+ if (!line || line.startsWith("#")) continue;
38
+ const eq = line.indexOf("=");
39
+ if (eq < 1) continue;
40
+ const key = line.slice(0, eq).trim();
41
+ values.set(key, unquote(line.slice(eq + 1).trim()));
42
+ }
43
+ return values;
44
+ }
45
+
46
+ /**
47
+ * @param {string} file
48
+ * @returns {{ exists: boolean, text: string, values: Map<string, string>, error: string }}
49
+ * `error` is set when the file exists but cannot be read
50
+ */
51
+ export function readEnvFile(file) {
52
+ if (!fs.existsSync(file)) return { exists: false, text: "", values: new Map(), error: "" };
53
+ try {
54
+ const text = fs.readFileSync(file, "utf8");
55
+ return { exists: true, text, values: parseEnvText(text), error: "" };
56
+ } catch (e) {
57
+ return {
58
+ exists: true,
59
+ text: "",
60
+ values: new Map(),
61
+ error: /** @type {NodeJS.ErrnoException} */ (e).code || "unreadable",
62
+ };
63
+ }
64
+ }
65
+
66
+ /**
67
+ * True when the file grants any permission to group or others.
68
+ * @param {string} file
69
+ */
70
+ export function tooOpen(file) {
71
+ return (fs.statSync(file).mode & 0o077) !== 0;
72
+ }
73
+
74
+ /**
75
+ * Render one value so Docker Compose's env_file parser reads it back unchanged: plain when
76
+ * it only has safe characters, single-quoted (literal, no interpolation) when it has no
77
+ * single quote and does not end in a backslash (Compose reads `\'` as an unterminated
78
+ * quote), double-quoted with `\`, `"` and `$` escaped otherwise.
79
+ * @param {string} value
80
+ */
81
+ export function formatValue(value) {
82
+ if (/[\r\n\0]/.test(value)) throw new Error("env values must be a single line");
83
+ if (/^[A-Za-z0-9_.:/@%+=,-]*$/.test(value)) return value;
84
+ if (!value.includes("'") && !value.endsWith("\\")) return `'${value}'`;
85
+ return `"${value.replace(/[\\"$]/g, "\\$&")}"`;
86
+ }
87
+
88
+ /**
89
+ * Replace the given keys in `text` (first assignment replaced in place, later duplicates
90
+ * dropped) and append the ones that were absent. Every other line is kept byte for byte.
91
+ * @param {string} text
92
+ * @param {Record<string, string>} updates
93
+ * @returns {string}
94
+ */
95
+ export function mergeEnvText(text, updates) {
96
+ for (const key of Object.keys(updates)) {
97
+ if (!KEY_RE.test(key)) throw new Error(`invalid env key: ${key}`);
98
+ }
99
+ const pending = new Map(Object.entries(updates));
100
+ const seen = new Set();
101
+ /** @type {string[]} */
102
+ const out = [];
103
+ const lines = text === "" ? [] : text.replace(/\n$/, "").split("\n");
104
+ for (const raw of lines) {
105
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(raw);
106
+ const key = m?.[1];
107
+ if (key && pending.has(key)) {
108
+ if (!seen.has(key)) {
109
+ seen.add(key);
110
+ out.push(`${key}=${formatValue(/** @type {string} */ (pending.get(key)))}`);
111
+ }
112
+ continue;
113
+ }
114
+ out.push(raw);
115
+ }
116
+ for (const [key, value] of pending) {
117
+ if (!seen.has(key)) out.push(`${key}=${formatValue(value)}`);
118
+ }
119
+ return `${out.join("\n")}\n`;
120
+ }
121
+
122
+ /**
123
+ * Remove assignment lines for the given keys (first and duplicate assignments). Other
124
+ * lines are kept. Used when switching presets so stale values (e.g. EMBEDDING_ENDPOINT
125
+ * after moving to FastEmbed) do not linger.
126
+ * @param {string} text
127
+ * @param {string[]} keys
128
+ * @returns {string}
129
+ */
130
+ export function dropEnvKeys(text, keys) {
131
+ const drop = new Set(keys);
132
+ for (const key of drop) {
133
+ if (!KEY_RE.test(key)) throw new Error(`invalid env key: ${key}`);
134
+ }
135
+ if (!drop.size) return text.endsWith("\n") || text === "" ? text : `${text}\n`;
136
+ const lines = text === "" ? [] : text.replace(/\n$/, "").split("\n");
137
+ const out = lines.filter((raw) => {
138
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(raw);
139
+ return !(m && drop.has(m[1]));
140
+ });
141
+ return out.length ? `${out.join("\n")}\n` : "";
142
+ }
143
+
144
+ /**
145
+ * Create `dir` (and missing parents) with mode 0700.
146
+ * @param {string} dir
147
+ * @returns {boolean} whether the directory had to be created
148
+ */
149
+ export function ensurePrivateDir(dir) {
150
+ if (fs.existsSync(dir)) return false;
151
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
152
+ return true;
153
+ }
154
+
155
+ /**
156
+ * Where a write to `file` really lands: the target of a symlink (a dotfile-managed link
157
+ * keeps pointing at its file), including a link whose target does not exist yet.
158
+ * @param {string} file
159
+ */
160
+ export function writeTarget(file) {
161
+ try {
162
+ return fs.realpathSync(file);
163
+ } catch {
164
+ try {
165
+ if (fs.lstatSync(file).isSymbolicLink()) {
166
+ return path.resolve(path.dirname(file), fs.readlinkSync(file));
167
+ }
168
+ } catch {
169
+ // not there at all: write the path itself
170
+ }
171
+ return file;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Write `text` to `file` with mode 0600 (temp file in the same directory, then rename, so
177
+ * a reader never sees a partial file and the old mode is never kept). A symlink is
178
+ * followed: the target is replaced and the link stays.
179
+ * @param {string} file
180
+ * @param {string} text
181
+ */
182
+ export function writePrivateFile(file, text) {
183
+ const target = writeTarget(file);
184
+ ensurePrivateDir(path.dirname(target));
185
+ const tmp = `${target}.tmp-${process.pid}`;
186
+ try {
187
+ fs.writeFileSync(tmp, text, { mode: 0o600, flag: "wx" });
188
+ fs.chmodSync(tmp, 0o600);
189
+ fs.renameSync(tmp, target);
190
+ } catch (e) {
191
+ fs.rmSync(tmp, { force: true });
192
+ throw e;
193
+ }
194
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The stack-env handoff and the pre-start step for Cognee.
3
+ *
4
+ * stackEnv: what the generic stack setup script (scripts/setup.mjs) prints and receives.
5
+ * It writes COGNEE_PORT, COMPOSE_PROFILES and LITELLM_CONFIG into the stack .env (see
6
+ * lib/stack-env.mjs), plus the PATHS of env files at a non-default location; no secret
7
+ * and no env file content ever goes there.
8
+ * beforeStack: the env directories must exist before scripts/gen-masks.mjs runs, because
9
+ * it only masks directories that exist; a masked ~/.config/personal-memory keeps
10
+ * cognee.env out of the indexer's read-only $HOME mount.
11
+ */
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { ensurePrivateDir, tooOpen } from "./env-file.mjs";
15
+ import { currentPort, ensureState, isCustomPath } from "./state.mjs";
16
+
17
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
18
+
19
+ /**
20
+ * @param {Record<string, any>} ctx
21
+ * @returns {{ description: string, env: Record<string, string> }}
22
+ */
23
+ export function stackEnv(ctx) {
24
+ ensureState(ctx);
25
+ /** @type {Record<string, string>} */
26
+ const env = { COGNEE_PORT: currentPort(ctx) };
27
+ if (isCustomPath(ctx, "envFile")) env.COGNEE_ENV_FILE = ctx.cognee.envFile;
28
+ if (isCustomPath(ctx, "indexerEnvFile")) env.COGNEE_INDEXER_ENV_FILE = ctx.cognee.indexerEnvFile;
29
+ return {
30
+ description:
31
+ "scripts/setup.sh: write .env (COGNEE_PORT, COMPOSE_PROFILES) and compose.override.yaml",
32
+ env,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * True when `file` sits under $HOME but outside ~/.config/personal-memory, the one directory
38
+ * scripts/gen-masks.mjs hides from the indexer's $HOME mount for these files.
39
+ * @param {Record<string, any>} ctx
40
+ * @param {string} file
41
+ */
42
+ function isUnmasked(ctx, file) {
43
+ if (!ctx.home) return false;
44
+ const inside = (/** @type {string} */ dir, /** @type {string} */ p) => p.startsWith(`${dir}${path.sep}`);
45
+ return inside(ctx.home, file) && !inside(path.join(ctx.home, ".config/personal-memory"), file);
46
+ }
47
+
48
+ /**
49
+ * Create the directories of the env files with mode 0700, tighten a group- or
50
+ * other-readable env file to 0600 (a real run), and warn about env files the indexer's
51
+ * $HOME mount could expose.
52
+ * @param {Record<string, any>} ctx
53
+ * @param {Helpers} h
54
+ */
55
+ export async function beforeStack(ctx, h) {
56
+ ensureState(ctx);
57
+ const files = [...new Set([ctx.cognee.envFile, ctx.cognee.indexerEnvFile])];
58
+ for (const dir of new Set(files.map((f) => path.dirname(f)))) {
59
+ if (ctx.dry && !fs.existsSync(dir)) {
60
+ h.info(`[dry-run] create ${dir} (mode 700, holds the Cognee env files)`);
61
+ continue;
62
+ }
63
+ if (!ctx.dry && ensurePrivateDir(dir)) h.ok(`created ${dir} (mode 700)`);
64
+ else if (fs.statSync(dir).mode & 0o077) {
65
+ h.warn(`${dir} is readable by other users; the env files inside are mode 600, but consider: chmod 700 ${dir}`);
66
+ }
67
+ }
68
+ for (const file of files) {
69
+ if (fs.existsSync(file) && tooOpen(file)) {
70
+ if (ctx.dry) h.warn(`${file} is readable by other users (it may hold API keys); a real run sets mode 600`);
71
+ else {
72
+ fs.chmodSync(file, 0o600);
73
+ h.ok(`tightened ${file} to mode 600 (it may hold API keys)`);
74
+ }
75
+ }
76
+ if (isUnmasked(ctx, file)) {
77
+ h.warn(
78
+ `${file} is under your home directory but outside ~/.config/personal-memory, which the indexer's ` +
79
+ "read-only $HOME mount does not mask; the indexer container could read it. Move it into that directory.",
80
+ );
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Step "LLM / embeddings": Cognee cognify settings go in the operator env file
3
+ * (cognee.env, mode 600, outside the repo). Values are never printed, logged or stored
4
+ * anywhere else; only the path and the key NAMES are reported.
5
+ *
6
+ * Default cognify path (no cloud key): GRAPH_EXTRACTOR=gliner_demo + FastEmbed
7
+ * (BAAI/bge-small-en-v1.5). Alternatives: LLM_PROVIDER=ollama (with ollama or fastembed
8
+ * embeddings), or an OpenAI-compatible LLM_API_KEY.
9
+ *
10
+ * Answers come from the environment, so a run is scriptable:
11
+ * PM_COGNEE_LLM_API_KEY the key (under --yes or --dry-run, plain LLM_API_KEY is read too)
12
+ * PM_COGNEE_<NAME> LLM_PROVIDER, LLM_MODEL, LLM_ENDPOINT, EMBEDDING_PROVIDER,
13
+ * EMBEDDING_MODEL, EMBEDDING_ENDPOINT, EMBEDDING_API_KEY,
14
+ * EMBEDDING_DIMENSIONS, HUGGINGFACE_TOKENIZER,
15
+ * GRAPH_EXTRACTOR, AUTO_FEEDBACK
16
+ *
17
+ * When LLM_PROVIDER=ollama, missing local settings are filled in (LLM_API_KEY=ollama,
18
+ * bare Ollama endpoint, matching ollama embeddings) so cognify works without a cloud key.
19
+ * When nothing cognify-related is given and the env file is not already cognify-ready,
20
+ * the GLiNER + FastEmbed defaults are written.
21
+ */
22
+ import fs from "node:fs";
23
+ import { readEnvFile, mergeEnvText, dropEnvKeys, tooOpen, writePrivateFile } from "./env-file.mjs";
24
+ import { configToml } from "./config.mjs";
25
+ import { currentPort, ensureState } from "./state.mjs";
26
+
27
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
28
+
29
+ export const KEY_NAME = "LLM_API_KEY";
30
+
31
+ /** Value Cognee accepts as LLM_API_KEY when talking to local Ollama (not a cloud secret). */
32
+ export const OLLAMA_API_KEY = "ollama";
33
+
34
+ /**
35
+ * Default cognify path: local GLiNER2 demo extraction + FastEmbed (CPU, no API key).
36
+ * Requires the cognee image extras (gliner2 + fastembed), which the pinned image ships.
37
+ */
38
+ export const GLINER_FASTEMBED_DEFAULTS = {
39
+ GRAPH_EXTRACTOR: "gliner_demo",
40
+ EMBEDDING_PROVIDER: "fastembed",
41
+ EMBEDDING_MODEL: "BAAI/bge-small-en-v1.5",
42
+ EMBEDDING_DIMENSIONS: "384",
43
+ AUTO_FEEDBACK: "false",
44
+ };
45
+
46
+ /** Env keys that are wrong for FastEmbed and should be removed when applying that preset. */
47
+ export const FASTEMBED_DROP_KEYS = ["EMBEDDING_ENDPOINT"];
48
+
49
+ /**
50
+ * Defaults when LLM_PROVIDER=ollama and the operator did not override them.
51
+ * Host Ollama (bridge / all-interfaces) uses host.docker.internal. Compose Ollama published
52
+ * as 127.0.0.1 only cannot be reached that way — use service DNS on a shared network
53
+ * (`PM_COGNEE_OLLAMA_COMPOSE=1` or `MEMORY_OLLAMA_IN_COMPOSE=1` → http://ollama:11434).
54
+ */
55
+ export const OLLAMA_DEFAULTS = {
56
+ LLM_API_KEY: OLLAMA_API_KEY,
57
+ LLM_ENDPOINT: "http://host.docker.internal:11434",
58
+ EMBEDDING_PROVIDER: "ollama",
59
+ EMBEDDING_MODEL: "nomic-embed-text:latest",
60
+ EMBEDDING_ENDPOINT: "http://host.docker.internal:11434/api/embed",
61
+ EMBEDDING_DIMENSIONS: "768",
62
+ };
63
+
64
+ /** @returns {Record<string, string>} */
65
+ export function ollamaDefaultsForEnv(env = process.env) {
66
+ const compose =
67
+ env.PM_COGNEE_OLLAMA_COMPOSE === "1" || env.MEMORY_OLLAMA_IN_COMPOSE === "1";
68
+ if (!compose) return { ...OLLAMA_DEFAULTS };
69
+ return {
70
+ ...OLLAMA_DEFAULTS,
71
+ LLM_ENDPOINT: "http://ollama:11434",
72
+ EMBEDDING_ENDPOINT: "http://ollama:11434/api/embed",
73
+ };
74
+ }
75
+
76
+ /** Optional keys passed through when set as PM_COGNEE_<NAME>. */
77
+ export const PASSTHROUGH = [
78
+ "LLM_PROVIDER",
79
+ "LLM_MODEL",
80
+ "LLM_ENDPOINT",
81
+ "EMBEDDING_PROVIDER",
82
+ "EMBEDDING_MODEL",
83
+ "EMBEDDING_ENDPOINT",
84
+ "EMBEDDING_API_KEY",
85
+ "EMBEDDING_DIMENSIONS",
86
+ "HUGGINGFACE_TOKENIZER",
87
+ "GRAPH_EXTRACTOR",
88
+ "AUTO_FEEDBACK",
89
+ ];
90
+
91
+ /** Answers that are secrets: env-file key -> [PM_COGNEE_ variable, plain variable used under --yes]. */
92
+ const SECRET_KEYS = /** @type {const} */ ({
93
+ LLM_API_KEY: ["PM_COGNEE_LLM_API_KEY", "LLM_API_KEY"],
94
+ EMBEDDING_API_KEY: ["PM_COGNEE_EMBEDDING_API_KEY", "EMBEDDING_API_KEY"],
95
+ });
96
+
97
+ /**
98
+ * Secret answers taken out of the environment, per wizard context. Module-private on
99
+ * purpose: nothing outside this file can read them, they are never put on `ctx` (which is
100
+ * handed to every hook and helper), and a later `process.env` copy cannot contain them.
101
+ * @type {WeakMap<object, Record<string, string>>}
102
+ */
103
+ const captured = new WeakMap();
104
+
105
+ /**
106
+ * Take the secret answers out of `process.env` before the wizard starts any child process
107
+ * (docker, curl, scripts/setup.mjs inherit the environment). Called from the `init` hook,
108
+ * which runs before the first docker call. PM_COGNEE_* secrets are always taken; plain
109
+ * LLM_API_KEY / EMBEDDING_API_KEY / COGNEE_API_KEY are always scrubbed from the environment
110
+ * (so children never inherit them). Plain LLM/EMBEDDING values are only *consumed* as answers
111
+ * under --yes or --dry-run (Clack / interactive use PM_COGNEE_* or the masked UI). Calling it
112
+ * again keeps what an earlier call captured.
113
+ * @param {Record<string, any>} ctx
114
+ */
115
+ export function captureSecrets(ctx) {
116
+ const mine = captured.get(ctx) || {};
117
+ const consumePlain = Boolean(ctx.yes || ctx.dry);
118
+ for (const [key, [pmName, plainName]] of Object.entries(SECRET_KEYS)) {
119
+ const fromPm = process.env[pmName]?.trim();
120
+ delete process.env[pmName];
121
+ const plain = process.env[plainName]?.trim();
122
+ delete process.env[plainName];
123
+ let value = fromPm;
124
+ if (consumePlain) value ||= plain;
125
+ if (value) mine[key] = value;
126
+ }
127
+ // Indexer auth key: scrub always; keep for this run's smoke (file is preferred at read time).
128
+ const cogneeKey = process.env.COGNEE_API_KEY?.trim();
129
+ delete process.env.COGNEE_API_KEY;
130
+ if (cogneeKey) mine.COGNEE_API_KEY = cogneeKey;
131
+ captured.set(ctx, mine);
132
+ return mine;
133
+ }
134
+
135
+ /**
136
+ * COGNEE_API_KEY captured in `init` (already removed from process.env). Prefer the indexer
137
+ * env file when present; this is the fallback for a one-shot env under --yes.
138
+ * @param {Record<string, any>} ctx
139
+ */
140
+ export function capturedCogneeApiKey(ctx) {
141
+ return (captured.get(ctx) || {}).COGNEE_API_KEY || "";
142
+ }
143
+
144
+ /**
145
+ * Fill FastEmbed embedding model/dims when EMBEDDING_PROVIDER=fastembed and fields are unset.
146
+ * Does not set GRAPH_EXTRACTOR (LLM extraction can still run with FastEmbed vectors).
147
+ * @param {Record<string, string>} answers
148
+ * @returns {Record<string, string>}
149
+ */
150
+ export function applyFastembedEmbeddingDefaults(answers) {
151
+ /** @type {Record<string, string>} */
152
+ const out = { ...answers };
153
+ for (const name of ["EMBEDDING_PROVIDER", "EMBEDDING_MODEL", "EMBEDDING_DIMENSIONS"]) {
154
+ if (!out[name]?.trim()) out[name] = GLINER_FASTEMBED_DEFAULTS[name];
155
+ }
156
+ return out;
157
+ }
158
+
159
+ /**
160
+ * Fill Ollama local defaults when LLM_PROVIDER is ollama and the operator left a field unset.
161
+ * Does not overwrite an explicit answer. LLM_API_KEY becomes "ollama" when missing
162
+ * (Cognee still requires the variable for its Ollama client). Explicit EMBEDDING_PROVIDER=
163
+ * fastembed is kept (Ollama LLM + FastEmbed embeddings is a supported local pair) without
164
+ * forcing GRAPH_EXTRACTOR=gliner_demo.
165
+ * @param {Record<string, string>} answers
166
+ * @returns {Record<string, string>}
167
+ */
168
+ export function applyOllamaPreset(answers, env = process.env) {
169
+ const provider = (answers.LLM_PROVIDER || "").trim().toLowerCase();
170
+ if (provider !== "ollama") return answers;
171
+ /** @type {Record<string, string>} */
172
+ const out = { ...answers };
173
+ const emb = (out.EMBEDDING_PROVIDER || "").trim().toLowerCase();
174
+ const defaults = ollamaDefaultsForEnv(env);
175
+ for (const [name, value] of Object.entries(defaults)) {
176
+ if (emb === "fastembed" && name.startsWith("EMBEDDING_")) continue;
177
+ if (!out[name]?.trim()) out[name] = value;
178
+ }
179
+ if (emb === "fastembed") return applyFastembedEmbeddingDefaults(out);
180
+ return out;
181
+ }
182
+
183
+ /**
184
+ * Fill GLiNER + FastEmbed defaults (path B / wizard default). Does not set LLM_API_KEY.
185
+ * @param {Record<string, string>} answers
186
+ * @returns {Record<string, string>}
187
+ */
188
+ export function applyGlinerFastembedPreset(answers) {
189
+ /** @type {Record<string, string>} */
190
+ const out = applyFastembedEmbeddingDefaults(answers);
191
+ for (const name of ["GRAPH_EXTRACTOR", "AUTO_FEEDBACK"]) {
192
+ if (!out[name]?.trim()) out[name] = GLINER_FASTEMBED_DEFAULTS[name];
193
+ }
194
+ return out;
195
+ }
196
+
197
+ /**
198
+ * True when `answers` has any cognify-related key (secrets or pass-through).
199
+ * @param {Record<string, string>} answers
200
+ */
201
+ function hasCognifyAnswers(answers) {
202
+ return Object.keys(answers).some((k) => k === KEY_NAME || PASSTHROUGH.includes(k) || k in SECRET_KEYS);
203
+ }
204
+
205
+ /**
206
+ * Answers for the env file, by key: the secrets captured in `init` plus the non-secret
207
+ * PM_COGNEE_<NAME> pass-through variables, with the matching local preset filled in.
208
+ * @param {Record<string, any>} ctx
209
+ * @returns {Record<string, string>}
210
+ */
211
+ export function answersFromEnv(ctx) {
212
+ /** @type {Record<string, string>} */
213
+ const answers = { ...(captured.get(ctx) || captureSecrets(ctx)) };
214
+ // COGNEE_API_KEY is for smoke / the indexer file, not cognee.env.
215
+ delete answers.COGNEE_API_KEY;
216
+ for (const name of PASSTHROUGH) {
217
+ if (name in SECRET_KEYS) continue;
218
+ const value = process.env[`PM_COGNEE_${name}`];
219
+ if (value?.trim()) answers[name] = value.trim();
220
+ }
221
+ const llm = (answers.LLM_PROVIDER || "").trim().toLowerCase();
222
+ const emb = (answers.EMBEDDING_PROVIDER || "").trim().toLowerCase();
223
+ const extractor = (answers.GRAPH_EXTRACTOR || "").trim().toLowerCase();
224
+ if (llm === "ollama") return applyOllamaPreset(answers);
225
+ if (extractor === "gliner_demo") return applyGlinerFastembedPreset(answers);
226
+ if (emb === "fastembed") return applyFastembedEmbeddingDefaults(answers);
227
+ return answers;
228
+ }
229
+
230
+ /**
231
+ * Read a value from a Map or plain object of env settings.
232
+ * @param {Map<string, string> | Record<string, string>} values
233
+ * @param {string} k
234
+ */
235
+ function envGet(values, k) {
236
+ return (values instanceof Map ? values.get(k) || "" : values[k] || "").trim();
237
+ }
238
+
239
+ /**
240
+ * Whether the merged env values look cognify-ready (graph build can run).
241
+ * Ready when: GLiNER + FastEmbed (no LLM key), or a usable LLM key with compatible embeddings.
242
+ * @param {Map<string, string> | Record<string, string>} values
243
+ */
244
+ export function isCognifyReady(values) {
245
+ const emb = envGet(values, "EMBEDDING_PROVIDER").toLowerCase();
246
+ const extractor = envGet(values, "GRAPH_EXTRACTOR").toLowerCase();
247
+ const embModel = envGet(values, "EMBEDDING_MODEL");
248
+ if (emb === "fastembed" && embModel) return true;
249
+ if (extractor === "gliner_demo" && (!emb || emb === "fastembed")) return true;
250
+
251
+ const key = envGet(values, "LLM_API_KEY");
252
+ if (!key) return false;
253
+ const llm = envGet(values, "LLM_PROVIDER").toLowerCase() || "openai";
254
+ const embOrDefault = emb || "openai";
255
+ if (llm === "ollama") {
256
+ if (embOrDefault === "fastembed") return Boolean(embModel);
257
+ return embOrDefault === "ollama" && Boolean(envGet(values, "EMBEDDING_ENDPOINT") || embModel);
258
+ }
259
+ return true;
260
+ }
261
+
262
+ /**
263
+ * Warn when LLM and embedding providers are a known-bad pair.
264
+ * FastEmbed is allowed with any LLM (including none). Same-provider pairs are fine.
265
+ * @param {Map<string, string> | Record<string, string>} values
266
+ * @returns {string | ""}
267
+ */
268
+ export function providerMismatchWarning(values) {
269
+ const llm = envGet(values, "LLM_PROVIDER").toLowerCase();
270
+ const emb = envGet(values, "EMBEDDING_PROVIDER").toLowerCase();
271
+ if (!llm || !emb) return "";
272
+ if (llm === emb) return "";
273
+ if (emb === "fastembed") return "";
274
+ return (
275
+ `LLM_PROVIDER=${llm} but EMBEDDING_PROVIDER=${emb}: ` +
276
+ "Cognee may fall back to OpenAI for the other half — use matching local providers, " +
277
+ "or EMBEDDING_PROVIDER=fastembed"
278
+ );
279
+ }
280
+
281
+ const COGNIFY_WARNING =
282
+ "no cognify-ready config: documents will be stored but not searchable until " +
283
+ "GRAPH_EXTRACTOR=gliner_demo with FastEmbed embeddings, LLM_PROVIDER=ollama, or LLM_API_KEY works";
284
+
285
+ /**
286
+ * Collect the answers, merge them into the env file, and record the port.
287
+ * @param {Record<string, any>} ctx
288
+ * @param {Helpers} h
289
+ */
290
+ export async function configureLlm(ctx, h) {
291
+ ensureState(ctx);
292
+ const file = ctx.cognee.envFile;
293
+ const existing = readEnvFile(file);
294
+ if (existing.error) h.die(`cannot read ${file} (${existing.error}); fix its permissions and rerun`);
295
+ let answers = answersFromEnv(ctx);
296
+ // Fresh / incomplete setup with no answers: default to GLiNER + FastEmbed.
297
+ if (!hasCognifyAnswers(answers) && !isCognifyReady(existing.values)) {
298
+ answers = { ...GLINER_FASTEMBED_DEFAULTS };
299
+ }
300
+ for (const [name, value] of Object.entries(answers)) {
301
+ if (/[\r\n\0]/.test(value)) h.die(`the answer for ${name} must be a single line`);
302
+ }
303
+
304
+ const hasStored = Boolean(existing.values.get(KEY_NAME));
305
+ const emb = (answers.EMBEDDING_PROVIDER || "").trim().toLowerCase();
306
+ const extractor = (answers.GRAPH_EXTRACTOR || "").trim().toLowerCase();
307
+ const usingFastembed = emb === "fastembed" || extractor === "gliner_demo";
308
+ // No helpers.ask for the key: it echoes what is typed. Clack uses a masked password prompt;
309
+ // otherwise the operator puts LLM_API_KEY in cognee.env by hand (same as mem0).
310
+ if (!answers[KEY_NAME] && !hasStored && !usingFastembed && !ctx.yes && !ctx.skipBackendStep && !ctx.dry) {
311
+ h.info(
312
+ `no LLM_API_KEY given. Cognee cognify defaults to local GLiNER + FastEmbed, or set ` +
313
+ `LLM_API_KEY in ${file} (mode 600), or PM_COGNEE_LLM_PROVIDER=ollama / PM_COGNEE_LLM_API_KEY.`,
314
+ );
315
+ }
316
+
317
+ const changes = Object.fromEntries(
318
+ Object.entries(answers).filter(([k, v]) => existing.values.get(k) !== v),
319
+ );
320
+ const names = Object.keys(changes);
321
+ const dropKeys = usingFastembed
322
+ ? FASTEMBED_DROP_KEYS.filter((k) => existing.values.has(k) && !(k in answers))
323
+ : [];
324
+ /** Effective values after this write (answers overlay existing; drops removed). */
325
+ const merged = new Map(existing.values);
326
+ for (const [k, v] of Object.entries(answers)) merged.set(k, v);
327
+ for (const k of dropKeys) merged.delete(k);
328
+ const cognifyReady = isCognifyReady(merged);
329
+
330
+ if (ctx.dry) {
331
+ if (names.length || dropKeys.length) {
332
+ const dropNote = dropKeys.length ? `; drop ${dropKeys.join(", ")}` : "";
333
+ h.info(
334
+ `[dry-run] write ${file} (mode 600, directory 700): set ${names.join(", ") || "(no sets)"}${dropNote}`,
335
+ );
336
+ } else if (!existing.exists) {
337
+ h.info(`[dry-run] ${file} would not be created (no LLM settings given)`);
338
+ } else h.info(`[dry-run] keep ${file} unchanged`);
339
+ } else if (names.length || dropKeys.length) {
340
+ let text = mergeEnvText(existing.text, changes);
341
+ if (dropKeys.length) text = dropEnvKeys(text, dropKeys);
342
+ writePrivateFile(file, text);
343
+ const dropNote = dropKeys.length ? `, dropped ${dropKeys.join(", ")}` : "";
344
+ h.ok(`wrote ${file} (mode 600): ${names.join(", ") || "updated"}${dropNote}`);
345
+ } else if (existing.exists && tooOpen(file)) {
346
+ fs.chmodSync(file, 0o600);
347
+ h.ok(`kept ${file}; tightened its mode to 600`);
348
+ } else if (existing.exists) {
349
+ h.ok(`kept ${file}${hasStored ? ` (${KEY_NAME} is set)` : ""}`);
350
+ }
351
+
352
+ const mismatch = providerMismatchWarning(merged);
353
+ if (mismatch) h.warn(mismatch);
354
+ if (!cognifyReady) h.warn(COGNIFY_WARNING);
355
+ else if (usingFastembed) {
356
+ h.info(
357
+ "Cognee cognify uses local GLiNER extraction + FastEmbed embeddings " +
358
+ "(first run downloads ~800 MB of models into the container HF cache).",
359
+ );
360
+ }
361
+ h.info(
362
+ "Cognee runs in single-user mode: ENABLE_BACKEND_ACCESS_CONTROL is pinned to false in " +
363
+ "providers/cognee/compose.yaml and overrides the env file, so REQUIRE_AUTHENTICATION alone " +
364
+ "cannot turn auth on. To enable auth, change that compose pin and set REQUIRE_AUTHENTICATION=true " +
365
+ "in cognee.env plus COGNEE_API_KEY in cognee-indexer.env.",
366
+ );
367
+
368
+ saveConf(ctx, h);
369
+ }
370
+
371
+ /**
372
+ * Record the non-secret choices in wizard.conf and config.toml.
373
+ * @param {Record<string, any>} ctx
374
+ * @param {Helpers} h
375
+ */
376
+ function saveConf(ctx, h) {
377
+ if (ctx.dry) {
378
+ h.info(`[dry-run] record COGNEE_PORT=${currentPort(ctx)} in ${ctx.wizardConf}`);
379
+ return;
380
+ }
381
+ h.saveWizardConf(ctx, { COGNEE_PORT: currentPort(ctx) }, { pmSections: configToml(ctx) });
382
+ }
@@ -0,0 +1,25 @@
1
+ /** Cognee's one host port (COGNEE_PORT, default 8766; the container listens on 8000). */
2
+ import path from "node:path";
3
+ import { ensureState } from "./state.mjs";
4
+
5
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
6
+
7
+ /**
8
+ * Choose the host port (environment, TOML table, stack .env, the port saved in
9
+ * wizard.conf, then 8766), offer the next free one when it is taken, and export it for
10
+ * compose and the stack setup script. wizard.conf is read here because `hydrate` runs after
11
+ * the ports are resolved.
12
+ * @param {Record<string, any>} ctx
13
+ * @param {Helpers} h
14
+ */
15
+ export async function resolvePorts(ctx, h) {
16
+ ensureState(ctx);
17
+ if (!process.env.COGNEE_PORT && !h.envValue(path.join(ctx.repoRoot, ".env"), "COGNEE_PORT")) {
18
+ const saved = h.envValue(ctx.wizardConf, "COGNEE_PORT");
19
+ if (/^\d+$/.test(saved)) process.env.COGNEE_PORT = saved;
20
+ }
21
+ const port = String(await h.resolveHostPort(ctx, "Cognee API"));
22
+ ctx.portCognee = port;
23
+ process.env.COGNEE_PORT = port;
24
+ ctx.cogneeUrl = `http://127.0.0.1:${port}`;
25
+ }