@bcmyguest/openmem 0.0.0-stage → 0.6.3

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 (191) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +136 -2
  3. package/bin/hindsight-project-index +530 -0
  4. package/cli/coding-agent/access-policy.mjs +38 -0
  5. package/cli/coding-agent/agents.mjs +34 -0
  6. package/cli/coding-agent/bank-from-cwd.mjs +140 -0
  7. package/cli/coding-agent/capabilities.mjs +23 -0
  8. package/cli/coding-agent/hook-harnesses.mjs +87 -0
  9. package/cli/coding-agent/hook-runner.mjs +260 -0
  10. package/cli/coding-agent/install-access.mjs +93 -0
  11. package/cli/coding-agent/install-shim.mjs +97 -0
  12. package/cli/coding-agent/opencode-config.mjs +91 -0
  13. package/cli/coding-agent/opencode-plugin.mjs +96 -0
  14. package/cli/coding-agent/runtime.mjs +39 -0
  15. package/cli/coding-agent/session-facade.mjs +62 -0
  16. package/cli/cognee-mcp.mjs +260 -0
  17. package/cli/feed-sync.mjs +11 -0
  18. package/cli/install-coding-agent.mjs +165 -0
  19. package/cli/install-memory-mcp.mjs +426 -0
  20. package/cli/is-main.mjs +18 -0
  21. package/cli/mcp/context.mjs +61 -0
  22. package/cli/mcp/export/cognee-dataset-name.mjs +12 -0
  23. package/cli/mcp/export/cognee.mjs +119 -0
  24. package/cli/mcp/export/hindsight.mjs +48 -0
  25. package/cli/mcp/export/index.mjs +26 -0
  26. package/cli/mcp/export/mem0.mjs +31 -0
  27. package/cli/mcp/llm-synthesize.mjs +50 -0
  28. package/cli/mcp/mem0-scope-id.mjs +22 -0
  29. package/cli/mcp/memory-facade.mjs +455 -0
  30. package/cli/mcp/memory-reflect-consolidate.mjs +120 -0
  31. package/cli/mcp/notebooklm-banks.mjs +21 -0
  32. package/cli/mcp/notebooklm-hindsight-rest.mjs +45 -0
  33. package/cli/mcp/notebooklm-provider.mjs +225 -0
  34. package/cli/mcp/pmm-records.mjs +23 -0
  35. package/cli/mcp/portability.mjs +158 -0
  36. package/cli/mcp/stdio.mjs +65 -0
  37. package/cli/mcp/unified-mcp-install.mjs +65 -0
  38. package/cli/mem0-mcp.mjs +108 -0
  39. package/cli/notebooklm-mcp.mjs +236 -0
  40. package/cli/openmem-mcp.mjs +35 -0
  41. package/cli/openmem.mjs +205 -0
  42. package/cli/roots.mjs +85 -0
  43. package/cli/stack.mjs +436 -0
  44. package/cli/switch-memory-provider.mjs +358 -0
  45. package/cli/wiring-report.mjs +125 -0
  46. package/compose.bench.yaml +12 -0
  47. package/compose.yaml +164 -0
  48. package/config/coding-agent.json +13 -0
  49. package/config/hindsight.env.example +24 -0
  50. package/config/index.yml +54 -0
  51. package/config/litellm.lemond.yaml +40 -0
  52. package/config/litellm.ollama.yaml +33 -0
  53. package/config/notebooklm.yml +33 -0
  54. package/config/openmem.toml.example +62 -0
  55. package/docs/OPENMEM.md +23 -0
  56. package/docs/architecture.md +219 -0
  57. package/docs/assets/README.md +10 -0
  58. package/docs/assets/setup-demo.gif +0 -0
  59. package/docs/coding-agent-hooks.md +51 -0
  60. package/docs/coding-agent-scopes.md +22 -0
  61. package/docs/coding-agents.md +51 -0
  62. package/docs/cognee.md +37 -0
  63. package/docs/concepts.md +112 -0
  64. package/docs/hindsight.md +74 -0
  65. package/docs/index-sources-roadmap.md +38 -0
  66. package/docs/index-sources.md +39 -0
  67. package/docs/indexer-sinks.md +122 -0
  68. package/docs/indexing.md +57 -0
  69. package/docs/manual-stack-setup.md +74 -0
  70. package/docs/mcp-install-guide.md +4 -0
  71. package/docs/mcp-setup.md +101 -0
  72. package/docs/mem0.md +103 -0
  73. package/docs/memory-portability.md +73 -0
  74. package/docs/memory-providers.md +246 -0
  75. package/docs/model-configuration.md +84 -0
  76. package/docs/notebooklm.md +83 -0
  77. package/docs/npx-install.md +170 -0
  78. package/docs/openmem-config.md +194 -0
  79. package/docs/provider-wizard-hooks.md +222 -0
  80. package/docs/quickstart.md +101 -0
  81. package/docs/setup-wizard.md +116 -0
  82. package/docs/sources-feeds.md +52 -0
  83. package/docs/sources-readwise.md +23 -0
  84. package/docs/sources-zotero.md +22 -0
  85. package/glama.json +4 -0
  86. package/indexer/Dockerfile +38 -0
  87. package/indexer/code_outline.py +146 -0
  88. package/indexer/feed_sync.py +107 -0
  89. package/indexer/index_config.py +290 -0
  90. package/indexer/pytest.ini +5 -0
  91. package/indexer/sinks/__init__.py +148 -0
  92. package/indexer/sinks/_http.py +44 -0
  93. package/indexer/sinks/cognee.py +329 -0
  94. package/indexer/sinks/hindsight.py +107 -0
  95. package/indexer/sinks/mem0.py +247 -0
  96. package/indexer/watch.py +240 -0
  97. package/package.json +95 -4
  98. package/providers/_template/compose.yaml +52 -0
  99. package/providers/cognee/OPERATOR.md +82 -0
  100. package/providers/cognee/compose.yaml +89 -0
  101. package/providers/cognee/lib/clack.mjs +157 -0
  102. package/providers/cognee/lib/coding-agent.mjs +26 -0
  103. package/providers/cognee/lib/cognee-mcp-install.mjs +153 -0
  104. package/providers/cognee/lib/config.mjs +98 -0
  105. package/providers/cognee/lib/env-file.mjs +194 -0
  106. package/providers/cognee/lib/env-setup.mjs +83 -0
  107. package/providers/cognee/lib/llm-env.mjs +382 -0
  108. package/providers/cognee/lib/ports.mjs +25 -0
  109. package/providers/cognee/lib/smoke.mjs +246 -0
  110. package/providers/cognee/lib/stack-env.mjs +134 -0
  111. package/providers/cognee/lib/state.mjs +101 -0
  112. package/providers/cognee/lib/summary.mjs +56 -0
  113. package/providers/cognee/lib/validate.mjs +97 -0
  114. package/providers/cognee/manifest.json +56 -0
  115. package/providers/cognee/wizard.mjs +134 -0
  116. package/providers/graphiti/manifest.json +15 -0
  117. package/providers/hindsight/lib/clack.mjs +245 -0
  118. package/providers/hindsight/lib/coding-agent.mjs +204 -0
  119. package/providers/hindsight/lib/config.mjs +73 -0
  120. package/providers/hindsight/lib/env-setup.mjs +71 -0
  121. package/providers/hindsight/lib/litellm-routes.mjs +187 -0
  122. package/providers/hindsight/lib/model-backend.mjs +214 -0
  123. package/providers/hindsight/lib/notebooklm-mcp-install.mjs +127 -0
  124. package/providers/hindsight/lib/ollama-compose.mjs +51 -0
  125. package/providers/hindsight/lib/ports.mjs +30 -0
  126. package/providers/hindsight/lib/probe.mjs +33 -0
  127. package/providers/hindsight/lib/stack-env.mjs +132 -0
  128. package/providers/hindsight/lib/state.mjs +75 -0
  129. package/providers/hindsight/lib/summary.mjs +43 -0
  130. package/providers/hindsight/lib/validate.mjs +75 -0
  131. package/providers/hindsight/manifest.json +58 -0
  132. package/providers/hindsight/wizard.mjs +149 -0
  133. package/providers/letta/manifest.json +29 -0
  134. package/providers/mem0/OPERATOR.md +124 -0
  135. package/providers/mem0/compose.yaml +57 -0
  136. package/providers/mem0/lib/coding-agent.mjs +58 -0
  137. package/providers/mem0/lib/config.mjs +45 -0
  138. package/providers/mem0/lib/configure.mjs +135 -0
  139. package/providers/mem0/lib/dotenv.mjs +71 -0
  140. package/providers/mem0/lib/env-file.mjs +110 -0
  141. package/providers/mem0/lib/http.mjs +69 -0
  142. package/providers/mem0/lib/mcp-install.mjs +165 -0
  143. package/providers/mem0/lib/secret.mjs +30 -0
  144. package/providers/mem0/lib/smoke.mjs +179 -0
  145. package/providers/mem0/lib/stack-env.mjs +216 -0
  146. package/providers/mem0/lib/state.mjs +67 -0
  147. package/providers/mem0/lib/summary.mjs +60 -0
  148. package/providers/mem0/lib/url.mjs +106 -0
  149. package/providers/mem0/lib/validate.mjs +158 -0
  150. package/providers/mem0/manifest.json +47 -0
  151. package/providers/mem0/wizard.mjs +133 -0
  152. package/providers/zep/manifest.json +29 -0
  153. package/scripts/check-pack.mjs +218 -0
  154. package/scripts/gen-masks.mjs +83 -0
  155. package/scripts/gen-masks.sh +4 -0
  156. package/scripts/promotion-provider-smoke.mjs +34 -0
  157. package/scripts/run-setup.sh +30 -0
  158. package/scripts/setup.mjs +94 -0
  159. package/scripts/setup.sh +5 -0
  160. package/scripts/spin-up.sh +45 -0
  161. package/scripts/wizard.sh +5 -0
  162. package/server.json +34 -0
  163. package/skills/openmem/SKILL.md +29 -0
  164. package/sync/Dockerfile +27 -0
  165. package/sync/memory_backend.py +87 -0
  166. package/sync/notebooklm_sync.py +664 -0
  167. package/wizard/bin/pm.mjs +97 -0
  168. package/wizard/bin/setup.mjs +33 -0
  169. package/wizard/lib/compose-version.mjs +32 -0
  170. package/wizard/lib/compose.mjs +70 -0
  171. package/wizard/lib/curl.mjs +103 -0
  172. package/wizard/lib/docker-build.mjs +148 -0
  173. package/wizard/lib/exec.mjs +41 -0
  174. package/wizard/lib/indexer.mjs +6 -0
  175. package/wizard/lib/load-machine-env.mjs +4 -0
  176. package/wizard/lib/log.mjs +32 -0
  177. package/wizard/lib/notebooklm-agent-access.mjs +92 -0
  178. package/wizard/lib/notebooklm.mjs +214 -0
  179. package/wizard/lib/parse-args.mjs +87 -0
  180. package/wizard/lib/paths.mjs +39 -0
  181. package/wizard/lib/pm-config.mjs +163 -0
  182. package/wizard/lib/ports.mjs +94 -0
  183. package/wizard/lib/prompts.mjs +57 -0
  184. package/wizard/lib/provider-wizard/helpers.mjs +98 -0
  185. package/wizard/lib/provider-wizard/run-steps.mjs +129 -0
  186. package/wizard/lib/providers/registry.mjs +262 -0
  187. package/wizard/lib/run-clack.mjs +108 -0
  188. package/wizard/lib/run-wizard.mjs +755 -0
  189. package/wizard/lib/validate-setup.mjs +62 -0
  190. package/wizard/lib/wizard-action.mjs +34 -0
  191. package/wizard/lib/wizard-conf.mjs +71 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Cognee smoke test (replaces the manifest's JSON-only HTTP smoke).
3
+ *
4
+ * Cognee ingests asynchronously (add, then cognify), so the test cannot read back what it
5
+ * wrote through search. It proves the server is up, accepts a multipart document and lists
6
+ * it, then removes its dataset:
7
+ * 1. GET /health (wait, the stack may still be starting)
8
+ * 2. POST /api/v1/datasets create dataset `wizard_smoke`
9
+ * 3. POST /api/v1/add (multipart) upload one small markdown file
10
+ * 4. GET /api/v1/datasets/{id}/data the document must be listed
11
+ * 5. DELETE /api/v1/datasets/{id} always, even after a failure
12
+ * It does not call cognify: the LLM may not be configured yet. X-Api-Key is sent only when
13
+ * COGNEE_API_KEY is set in the environment or in the indexer env file; its value is never
14
+ * printed.
15
+ */
16
+ import { readEnvFile } from "./env-file.mjs";
17
+ import { capturedCogneeApiKey } from "./llm-env.mjs";
18
+ import { SMOKE_DATASET, ensureState } from "./state.mjs";
19
+
20
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
21
+
22
+ export const SMOKE_DOC_ID = "wizard-smoke";
23
+ export const SMOKE_FILE = "wizard-smoke.md";
24
+ export const SMOKE_TEXT = "Wizard smoke test: the project codename is heron.";
25
+
26
+ export const RECALL_SKIPPED =
27
+ "recall skipped: Cognee ingests asynchronously (add, then cognify); search results appear once the graph is built";
28
+
29
+ const DEFAULTS = {
30
+ healthTimeoutMs: 120_000,
31
+ healthIntervalMs: 3_000,
32
+ requestTimeoutMs: 30_000,
33
+ addTimeoutMs: 120_000,
34
+ };
35
+
36
+ /** A step failed; the message is safe to print (no headers, no bodies beyond a short detail). */
37
+ class SmokeError extends Error {}
38
+
39
+ /**
40
+ * Replace every occurrence of `secret` in text (mem0-style). Empty secret is a no-op.
41
+ * @param {string} text
42
+ * @param {string} secret
43
+ */
44
+ function redact(text, secret) {
45
+ let out = String(text ?? "");
46
+ if (secret) out = out.split(secret).join("***");
47
+ return out;
48
+ }
49
+
50
+ /**
51
+ * The API key for X-Api-Key: indexer env file first (durable), then the value captured from
52
+ * process.env in `init` (already scrubbed so children never inherit it). Never reads
53
+ * process.env at call time.
54
+ * @param {Record<string, any>} ctx
55
+ * @returns {string}
56
+ */
57
+ export function apiKeyFor(ctx) {
58
+ const fromFile = readEnvFile(ctx.cognee.indexerEnvFile).values.get("COGNEE_API_KEY") || "";
59
+ if (fromFile) return fromFile;
60
+ return capturedCogneeApiKey(ctx);
61
+ }
62
+
63
+ /**
64
+ * A short, secret-free reason from an error response: the `detail` string when there is one
65
+ * (secrets redacted, control characters removed, at most 80 characters).
66
+ * @param {Response} res
67
+ * @param {string} [secret]
68
+ */
69
+ async function reason(res, secret = "") {
70
+ let detail = "";
71
+ try {
72
+ const body = JSON.parse(await res.text());
73
+ if (typeof body?.detail === "string") {
74
+ // Short and printable: a server message can carry control characters, a long dump, or the key.
75
+ const cleaned = redact(body.detail, secret).replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 80);
76
+ detail = `: ${cleaned}`;
77
+ }
78
+ } catch {
79
+ // not JSON: the status line is enough
80
+ }
81
+ return `HTTP ${res.status}${detail}`;
82
+ }
83
+
84
+ /**
85
+ * @param {Record<string, any>} item one entry of the data listing
86
+ * @returns {string[]} every doc_id the entry carries in its external metadata
87
+ */
88
+ function docIds(item) {
89
+ let meta = item?.externalMetadata ?? item?.external_metadata;
90
+ if (typeof meta === "string") {
91
+ try {
92
+ meta = JSON.parse(meta);
93
+ } catch {
94
+ return [];
95
+ }
96
+ }
97
+ const entries = Array.isArray(meta) ? meta : meta ? [meta] : [];
98
+ return entries.map((e) => e?.doc_id).filter((v) => typeof v === "string");
99
+ }
100
+
101
+ /**
102
+ * Run the smoke test.
103
+ * @param {Record<string, any>} ctx
104
+ * @param {Helpers} h
105
+ * @param {{ fetch?: typeof fetch, sleep?: (ms: number) => Promise<void>,
106
+ * healthTimeoutMs?: number, healthIntervalMs?: number,
107
+ * requestTimeoutMs?: number, addTimeoutMs?: number }} [opts] test seams
108
+ */
109
+ export async function runSmoke(ctx, h, opts = {}) {
110
+ ensureState(ctx);
111
+ const o = { ...DEFAULTS, ...opts };
112
+ const doFetch = opts.fetch || globalThis.fetch;
113
+ const sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
114
+ const base = String(await h.apiBase(ctx)).replace(/\/+$/, "");
115
+ const logs = `docker ${h.composeArgs(ctx, "logs", "cognee").join(" ")}`;
116
+
117
+ if (ctx.dry) {
118
+ h.info(`[dry-run] smoke test against ${base}:`);
119
+ h.info(`[dry-run] 1. wait for GET ${base}/health (up to 120s)`);
120
+ h.info(`[dry-run] 2. POST ${base}/api/v1/datasets {"name":"${SMOKE_DATASET}"}`);
121
+ h.info(`[dry-run] 3. POST ${base}/api/v1/add (multipart: ${SMOKE_FILE} into ${SMOKE_DATASET})`);
122
+ h.info(`[dry-run] 4. GET ${base}/api/v1/datasets/<id>/data must list the document`);
123
+ h.info(`[dry-run] 5. DELETE ${base}/api/v1/datasets/<id> (always)`);
124
+ h.info(`[dry-run] ${RECALL_SKIPPED}`);
125
+ return;
126
+ }
127
+
128
+ const key = apiKeyFor(ctx);
129
+ /** @type {Record<string, string>} */
130
+ const auth = key ? { "X-Api-Key": key } : {};
131
+ /** @param {Response} res */
132
+ const why = (res) => reason(res, key);
133
+
134
+ /**
135
+ * @param {string} method
136
+ * @param {string} route
137
+ * @param {{ json?: unknown, form?: FormData, timeoutMs?: number }} [req]
138
+ */
139
+ const call = async (method, route, req = {}) => {
140
+ /** @type {Record<string, string>} */
141
+ const headers = { ...auth };
142
+ let body;
143
+ if (req.json !== undefined) {
144
+ headers["Content-Type"] = "application/json";
145
+ body = JSON.stringify(req.json);
146
+ } else if (req.form) body = req.form;
147
+ return doFetch(`${base}${route}`, {
148
+ method,
149
+ headers,
150
+ body,
151
+ signal: AbortSignal.timeout(req.timeoutMs ?? o.requestTimeoutMs),
152
+ });
153
+ };
154
+
155
+ /** Id of the dataset named wizard_smoke, from the listing; "" when absent or unlistable. */
156
+ const findDatasetId = async () => {
157
+ try {
158
+ const res = await call("GET", "/api/v1/datasets");
159
+ if (!res.ok) return "";
160
+ const list = await res.json().catch(() => null);
161
+ const found = (Array.isArray(list) ? list : []).find((d) => d?.name === SMOKE_DATASET);
162
+ return typeof found?.id === "string" ? found.id : "";
163
+ } catch {
164
+ return "";
165
+ }
166
+ };
167
+
168
+ /** @type {string} */
169
+ let datasetId = "";
170
+ /** @type {string} */
171
+ let failure = "";
172
+ try {
173
+ // 1. health
174
+ h.info(`smoke test: waiting for ${base}/health`);
175
+ const deadline = Date.now() + o.healthTimeoutMs;
176
+ let healthy = false;
177
+ for (;;) {
178
+ try {
179
+ healthy = (await call("GET", "/health", { timeoutMs: 5_000 })).ok;
180
+ } catch {
181
+ healthy = false;
182
+ }
183
+ if (healthy || Date.now() >= deadline) break;
184
+ await sleep(o.healthIntervalMs);
185
+ }
186
+ if (!healthy) throw new SmokeError(`GET /health did not answer 2xx within ${Math.round(o.healthTimeoutMs / 1000)}s`);
187
+
188
+ // 2. dataset. A create that times out or gets a 5xx may still have created it: look the
189
+ // dataset up by name so the cleanup below deletes an orphan.
190
+ /** @type {Response | null} */
191
+ let created = null;
192
+ let createError = "";
193
+ try {
194
+ created = await call("POST", "/api/v1/datasets", { json: { name: SMOKE_DATASET } });
195
+ } catch (e) {
196
+ createError = `create dataset request failed (${/** @type {Error} */ (e).name})`;
197
+ }
198
+ if (created?.ok) {
199
+ const dataset = await created.json().catch(() => null);
200
+ datasetId = typeof dataset?.id === "string" ? dataset.id : "";
201
+ }
202
+ if (!datasetId) {
203
+ if (!created || created.status >= 500 || created.ok) datasetId = await findDatasetId();
204
+ throw new SmokeError(createError || (created?.ok ? "create dataset returned no id" : `create dataset failed (${await why(/** @type {Response} */ (created))})`));
205
+ }
206
+
207
+ // 3. add (multipart)
208
+ const form = new FormData();
209
+ form.append("datasetName", SMOKE_DATASET);
210
+ form.append("data", new Blob([SMOKE_TEXT], { type: "text/markdown" }), SMOKE_FILE);
211
+ form.append("external_metadata", JSON.stringify([{ doc_id: SMOKE_DOC_ID }]));
212
+ const added = await call("POST", "/api/v1/add", { form, timeoutMs: o.addTimeoutMs });
213
+ if (!added.ok) throw new SmokeError(`add failed (${await why(added)})`);
214
+
215
+ // 4. listing
216
+ const listed = await call("GET", `/api/v1/datasets/${encodeURIComponent(datasetId)}/data`);
217
+ if (!listed.ok) throw new SmokeError(`list data failed (${await why(listed)})`);
218
+ const payload = await listed.json().catch(() => null);
219
+ const items = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : [];
220
+ if (items.length < 1) throw new SmokeError("the dataset listing is empty after add");
221
+ const ids = items.flatMap(docIds);
222
+ if (ids.length && !ids.includes(SMOKE_DOC_ID)) {
223
+ throw new SmokeError(`the listed document carries doc_id ${JSON.stringify(ids[0])}, expected ${SMOKE_DOC_ID}`);
224
+ }
225
+ } catch (e) {
226
+ failure = e instanceof SmokeError ? e.message : `request failed (${/** @type {Error} */ (e).name})`;
227
+ } finally {
228
+ // 5. cleanup, best effort, before any exit
229
+ if (datasetId) {
230
+ try {
231
+ const del = await call("DELETE", `/api/v1/datasets/${encodeURIComponent(datasetId)}`);
232
+ if (del.ok) h.ok(`deleted smoke dataset ${SMOKE_DATASET}`);
233
+ else h.warn(`could not delete smoke dataset ${SMOKE_DATASET} (${await why(del)}); delete it by hand: DELETE ${base}/api/v1/datasets/${datasetId}`);
234
+ } catch (e) {
235
+ h.warn(`could not delete smoke dataset ${SMOKE_DATASET} (${/** @type {Error} */ (e).name}); delete it by hand: DELETE ${base}/api/v1/datasets/${datasetId}`);
236
+ }
237
+ }
238
+ }
239
+
240
+ if (failure) {
241
+ h.die(`Cognee smoke test failed: ${failure}. Check: ${logs} (and LLM_API_KEY in cognee.env)`);
242
+ return;
243
+ }
244
+ h.ok(`Cognee accepted and listed a document in dataset ${SMOKE_DATASET}`);
245
+ h.info(RECALL_SKIPPED);
246
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Cognee's contribution to scripts/setup.mjs: the COGNEE_PORT entry of the stack .env, the paths of
3
+ * non-default env files (COGNEE_ENV_FILE, COGNEE_INDEXER_ENV_FILE: paths, never contents), an inert
4
+ * LITELLM_CONFIG entry, and the end-of-run notices about the Cognee env file.
5
+ * Runs as a plain Node script step (no wizard context), so it only reads process environment
6
+ * and the paths it is given. It never prints or returns secret values.
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { envValue } from "../../../wizard/lib/paths.mjs";
12
+ import { isCognifyReady, providerMismatchWarning } from "./llm-env.mjs";
13
+ import { parseEnvText } from "./env-file.mjs";
14
+
15
+ const DEFAULT_PORT = "8766";
16
+
17
+ /** Read-only code root: PM_PACKAGE_ROOT (set by scripts/setup.mjs), else this module's checkout. */
18
+ function packageRoot() {
19
+ const moduleRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
20
+ return path.resolve(process.env.PM_PACKAGE_ROOT || moduleRoot);
21
+ }
22
+
23
+ /** @param {string} home @param {NodeJS.ProcessEnv} env @param {string} [repo] */
24
+ function cogneeEnvFile(home, env, repo) {
25
+ const value = env.COGNEE_ENV_FILE || (repo && envValue(path.join(repo, ".env"), "COGNEE_ENV_FILE"));
26
+ if (!value) return path.join(home, ".config/personal-memory/cognee.env");
27
+ return value.startsWith("~/") ? path.join(home, value.slice(2)) : path.resolve(repo || "", value);
28
+ }
29
+
30
+ /**
31
+ * An env-file path from the environment as an absolute path: `~` is expanded and a relative
32
+ * path is taken relative to the stack root, which is how compose resolves env_file.
33
+ * @param {string} value @param {string} repo @param {string} home
34
+ */
35
+ function absolutePath(value, repo, home) {
36
+ if (/[\r\n\0]/.test(value)) throw new Error("an env file path must be a single line");
37
+ const expanded = value === "~" ? home : value.startsWith("~/") ? path.join(home, value.slice(2)) : value;
38
+ return path.resolve(repo, expanded);
39
+ }
40
+
41
+ /**
42
+ * @param {{ repo: string, home?: string, env: NodeJS.ProcessEnv }} opts
43
+ * @returns {{ entries: [string, string][], drop: string[], report: string[] }}
44
+ * entries: KEY=value lines to add to the stack .env; drop: keys to remove first (so a key is
45
+ * rewritten, never duplicated); report: KEY=value strings echoed in the "wrote .env" line
46
+ */
47
+ export function prepareStackEnv({ repo, home = process.env.HOME || "", env }) {
48
+ const stackEnv = path.join(repo, ".env");
49
+
50
+ const port = env.COGNEE_PORT || envValue(stackEnv, "COGNEE_PORT") || DEFAULT_PORT;
51
+ if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) {
52
+ throw new Error(`COGNEE_PORT must be a number from 1 to 65535 (got: ${port})`);
53
+ }
54
+
55
+ const profiles = ["cognee"];
56
+ const notebookYml = path.join(home, ".config/hindsight/notebooklm.yml");
57
+ if (fs.existsSync(notebookYml) && /^enabled:\s*true\b/m.test(fs.readFileSync(notebookYml, "utf8"))) {
58
+ profiles.push("notebooklm");
59
+ }
60
+
61
+ /** @type {[string, string][]} */
62
+ const entries = [
63
+ ["COGNEE_PORT", String(Number(port))],
64
+ ["MEMORY_SINK", "cognee"],
65
+ ["MEMORY_API_URL", "http://cognee:8000"],
66
+ ["COMPOSE_PROFILES", profiles.join(",")],
67
+ ];
68
+ const drop = ["COGNEE_PORT", "COMPOSE_PROFILES", "MEMORY_SINK", "MEMORY_API_URL"];
69
+ const report = [`COGNEE_PORT=${entries[0][1]}`, `COMPOSE_PROFILES=${profiles.join(",")}`];
70
+
71
+ // Env files at a non-default location: persist the PATH (not its contents) so a hand-run
72
+ // `docker compose` finds the same files. Absent from the environment, nothing is written and
73
+ // an existing line is kept.
74
+ for (const name of ["COGNEE_ENV_FILE", "COGNEE_INDEXER_ENV_FILE"]) {
75
+ if (!env[name]) continue;
76
+ const file = absolutePath(env[name], repo, home);
77
+ entries.push([name, file]);
78
+ drop.push(name);
79
+ report.push(`${name}=${file}`);
80
+ }
81
+
82
+ // Compose interpolates every service before it filters by profile, so the root compose.yaml
83
+ // requires LITELLM_CONFIG even under `--profile cognee`. The value is inert here: the litellm
84
+ // service is gated by the profile "hindsight" and never starts for Cognee. Keep any value the
85
+ // stack .env already has; otherwise honor the operator's process env; otherwise the shipped
86
+ // config. An empty LITELLM_CONFIG= line is dropped so it is never duplicated.
87
+ if (!envValue(stackEnv, "LITELLM_CONFIG")) {
88
+ let litellmConfig = env.LITELLM_CONFIG;
89
+ if (!litellmConfig) {
90
+ litellmConfig = path.join(packageRoot(), "config", "litellm.lemond.yaml");
91
+ if (!fs.existsSync(litellmConfig)) {
92
+ throw new Error(`shipped config not found: ${litellmConfig}`);
93
+ }
94
+ }
95
+ entries.push(["LITELLM_CONFIG", litellmConfig]);
96
+ drop.push("LITELLM_CONFIG");
97
+ report.push(`LITELLM_CONFIG=${litellmConfig}`);
98
+ }
99
+ return { entries, drop, report };
100
+ }
101
+
102
+ /**
103
+ * Printed after the .env is written. Names the env file, never its contents.
104
+ * @param {{ repo?: string, home: string, env: NodeJS.ProcessEnv }} opts
105
+ * @returns {string[]}
106
+ */
107
+ export function setupNotices({ repo, home, env }) {
108
+ const file = cogneeEnvFile(home, env, repo);
109
+ const notices = [];
110
+ let text = "";
111
+ try {
112
+ text = fs.readFileSync(file, "utf8");
113
+ } catch {
114
+ // missing or unreadable: same notice as an empty file
115
+ }
116
+ const values = parseEnvText(text);
117
+ if (!isCognifyReady(values)) {
118
+ const mismatch = providerMismatchWarning(values);
119
+ if (mismatch) notices.push(`NOTE: ${mismatch} in ${file}.`);
120
+ notices.push(
121
+ `NOTE: Cognee needs a cognify-ready config in ${file}: default is GRAPH_EXTRACTOR=gliner_demo ` +
122
+ "with FastEmbed embeddings, or LLM_PROVIDER=ollama, or LLM_API_KEY. " +
123
+ "Documents are stored but not searchable until cognify works.",
124
+ );
125
+ }
126
+ try {
127
+ if (fs.statSync(file).mode & 0o077) {
128
+ notices.push(`NOTE: ${file} is readable by other users; run: chmod 600 ${file}`);
129
+ }
130
+ } catch {
131
+ // no file: nothing to say about its mode
132
+ }
133
+ return notices;
134
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Cognee's slice of the wizard context: defaults, and where the operator env files live.
3
+ *
4
+ * Two optional env files hold every Cognee secret; neither is the stack .env:
5
+ * cognee.env loaded by the cognee service (LLM_API_KEY, LLM_*, EMBEDDING_*)
6
+ * cognee-indexer.env loaded by the indexer (COGNEE_API_KEY only, when auth is enabled)
7
+ * Their paths follow providers/cognee/compose.yaml: COGNEE_ENV_FILE and
8
+ * COGNEE_INDEXER_ENV_FILE, defaulting to ~/.config/personal-memory/.
9
+ */
10
+ import path from "node:path";
11
+ import { envValue } from "../../../wizard/lib/paths.mjs";
12
+
13
+ export const DEFAULT_PORT = 8766;
14
+ export const DEFAULT_ENV_FILE = "~/.config/personal-memory/cognee.env";
15
+ export const DEFAULT_INDEXER_ENV_FILE = "~/.config/personal-memory/cognee-indexer.env";
16
+
17
+ /** Dataset the smoke test creates and deletes. */
18
+ export const SMOKE_DATASET = "wizard_smoke";
19
+
20
+ /**
21
+ * @param {string} p
22
+ * @param {string} home
23
+ */
24
+ function expand(p, home) {
25
+ if (p === "~") return home;
26
+ if (p.startsWith("~/")) return path.join(home, p.slice(2));
27
+ return p;
28
+ }
29
+
30
+ /**
31
+ * Absolute path for an env-file setting: `~` is expanded and a relative path is taken
32
+ * relative to the stack root (compose resolves env_file against its project directory).
33
+ * @param {string} value
34
+ * @param {{ home?: string, repoRoot?: string }} ctx
35
+ */
36
+ export function resolveFile(value, ctx) {
37
+ return path.resolve(ctx.repoRoot || "", expand(value, ctx.home || ""));
38
+ }
39
+
40
+ /**
41
+ * One env-file setting, most specific first: the environment variable, the `[memory.cognee]`
42
+ * TOML value, the stack .env (where a previous run persisted it), then the default.
43
+ * @param {Record<string, any>} ctx
44
+ * @param {string} name COGNEE_ENV_FILE or COGNEE_INDEXER_ENV_FILE
45
+ * @param {string} fromToml
46
+ * @param {string} fallback
47
+ */
48
+ function envFileSetting(ctx, name, fromToml, fallback) {
49
+ const fromStack = ctx.repoRoot ? envValue(path.join(ctx.repoRoot, ".env"), name) : "";
50
+ return resolveFile(process.env[name] || fromToml || fromStack || fallback, ctx);
51
+ }
52
+
53
+ /**
54
+ * Fill the Cognee fields of the wizard context. Idempotent: a field that is already set
55
+ * is kept. The env file paths are recomputed on every call because the environment, the
56
+ * TOML table (applied in `init`) and the stack .env can change between phases.
57
+ * @param {Record<string, any>} ctx
58
+ */
59
+ export function ensureState(ctx) {
60
+ const cognee = (ctx.cognee ||= { port: "", envFile: "", indexerEnvFile: "" });
61
+ cognee.envFile = envFileSetting(ctx, "COGNEE_ENV_FILE", cognee.configEnvFile || "", DEFAULT_ENV_FILE);
62
+ cognee.indexerEnvFile = envFileSetting(
63
+ ctx,
64
+ "COGNEE_INDEXER_ENV_FILE",
65
+ cognee.configIndexerEnvFile || "",
66
+ DEFAULT_INDEXER_ENV_FILE,
67
+ );
68
+ if (ctx.portCognee === undefined) ctx.portCognee = "";
69
+ if (ctx.cogneeUrl === undefined) ctx.cogneeUrl = "";
70
+ return ctx;
71
+ }
72
+
73
+ /**
74
+ * Whether an env file path differs from its shipped default (and so must be persisted).
75
+ * @param {Record<string, any>} ctx
76
+ * @param {"envFile"|"indexerEnvFile"} which
77
+ */
78
+ export function isCustomPath(ctx, which) {
79
+ const fallback = which === "envFile" ? DEFAULT_ENV_FILE : DEFAULT_INDEXER_ENV_FILE;
80
+ return ctx.cognee[which] !== resolveFile(fallback, ctx);
81
+ }
82
+
83
+ /**
84
+ * The host port in force: the resolved one, else the environment, else the default.
85
+ * @param {Record<string, any>} ctx
86
+ */
87
+ export function currentPort(ctx) {
88
+ return String(ctx.portCognee || process.env.COGNEE_PORT || DEFAULT_PORT);
89
+ }
90
+
91
+ /**
92
+ * A path for display and config.toml: `~/...` when it sits under the home directory.
93
+ * @param {string} file
94
+ * @param {string} [home]
95
+ */
96
+ export function tildePath(file, home) {
97
+ if (home && (file === home || file.startsWith(`${home}${path.sep}`))) {
98
+ return `~${file.slice(home.length)}`;
99
+ }
100
+ return file;
101
+ }
@@ -0,0 +1,56 @@
1
+ /** Cognee's contribution to the wizard's closing summary. */
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { readEnvFile } from "./env-file.mjs";
5
+ import { isCognifyReady } from "./llm-env.mjs";
6
+ import { currentPort, ensureState } from "./state.mjs";
7
+
8
+ /**
9
+ * Lines the meta wizard prints in its summary, in the order it prints them.
10
+ * @param {Record<string, any>} ctx
11
+ * @returns {{ running: string[], configFiles: string[], notes: string[], logServices: string[] }}
12
+ */
13
+ export function summary(ctx) {
14
+ ensureState(ctx);
15
+ const url = ctx.cogneeUrl || `http://127.0.0.1:${currentPort(ctx)}`;
16
+ const running = [
17
+ ` Cognee API ${url} (docs: ${url}/docs)`,
18
+ " indexer sink cognee -> http://cognee:8000 (internal network)",
19
+ " ingestion is asynchronous: the indexer requests cognify once per changed dataset",
20
+ ];
21
+ const configFiles = [` ${ctx.cognee.envFile} (cognify settings, mode 600)`];
22
+ if (fs.existsSync(ctx.cognee.indexerEnvFile)) {
23
+ configFiles.push(` ${ctx.cognee.indexerEnvFile} (indexer API key, mode 600)`);
24
+ }
25
+ configFiles.push(` ${path.join(ctx.repoRoot, ".env")} (COGNEE_PORT, COMPOSE_PROFILES)`);
26
+ const env = readEnvFile(ctx.cognee.envFile);
27
+ const ready = isCognifyReady(env.values);
28
+ const provider = (env.values.get("LLM_PROVIDER") || "").toLowerCase();
29
+ const emb = (env.values.get("EMBEDDING_PROVIDER") || "").toLowerCase();
30
+ const extractor = (env.values.get("GRAPH_EXTRACTOR") || "").toLowerCase();
31
+ let readyNote;
32
+ if (!ready) {
33
+ readyNote =
34
+ `Cognee needs a cognify-ready config in ${ctx.cognee.envFile}: ` +
35
+ "GRAPH_EXTRACTOR=gliner_demo + FastEmbed (default), LLM_PROVIDER=ollama, or LLM_API_KEY; " +
36
+ "without it documents are stored but not searchable. Then restart with: ./pm up";
37
+ } else if (extractor === "gliner_demo" || emb === "fastembed") {
38
+ readyNote =
39
+ `Cognee builds its graph with local GLiNER + FastEmbed from ${ctx.cognee.envFile}; ` +
40
+ "change it there, then restart with: ./pm up";
41
+ } else if (provider === "ollama") {
42
+ readyNote =
43
+ `Cognee builds its graph with Ollama from ${ctx.cognee.envFile}; change it there, then restart with: ./pm up`;
44
+ } else {
45
+ readyNote =
46
+ `Cognee builds its graph with the LLM configured in ${ctx.cognee.envFile}; change it there, then restart with: ./pm up`;
47
+ }
48
+ const notes = [
49
+ readyNote,
50
+ `Graph status: GET ${url}/api/v1/datasets/status`,
51
+ "The first start downloads a large image and may take several minutes; GLiNER/FastEmbed add ~800 MB on first cognify.",
52
+ "Deleting a dataset removes its graph and vectors, not just the stored files.",
53
+ `Coding agents: no plugin — call the REST API at ${url} (see /docs); ingest POST /api/v1/add, then cognify, then search.`,
54
+ ];
55
+ return { running, configFiles, notes, logServices: ["cognee", "indexer"] };
56
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Cognee checks that run after the meta wizard's provider-neutral validation: the operator
3
+ * env files (mode, GLiNER/FastEmbed / Ollama / LLM-key cognify readiness), the stack .env
4
+ * (port present, no secret assignment lines), and the compose fragment. A missing cognify
5
+ * config is a warning-class check (documents are stored but not searchable); an env file
6
+ * that is unreadable or open to group/other is an error.
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { readEnvFile, tooOpen } from "./env-file.mjs";
11
+ import { isCognifyReady, providerMismatchWarning } from "./llm-env.mjs";
12
+ import { ensureState } from "./state.mjs";
13
+
14
+ /** @typedef {import("../../../wizard/lib/provider-wizard/helpers.mjs").ProviderWizardHelpers} Helpers */
15
+
16
+ // Spelled in two pieces: indexer/tests/test_cognee_compose.py asserts that no file under
17
+ // providers/cognee contains the word, so this module must not either.
18
+ const UNFINISHED = new RegExp(["place", "holder"].join(""), "i");
19
+ // Assignment lines only (not HINDSIGHT_API_LLM_API_KEY or a mention in a comment).
20
+ const SECRET_ASSIGNMENT = /^(?:export\s+)?(?:LLM|COGNEE|EMBEDDING)_API_KEY=/m;
21
+
22
+ /**
23
+ * Appends to ctx.validationErrors and ctx.validationChecks.
24
+ * @param {Record<string, any>} ctx
25
+ * @param {Helpers} h
26
+ */
27
+ export function validateCognee(ctx, h) {
28
+ ensureState(ctx);
29
+ const errors = (ctx.validationErrors ||= []);
30
+ const checks = (ctx.validationChecks ||= []);
31
+
32
+ // cognee.env: mode and cognify readiness.
33
+ const file = ctx.cognee.envFile;
34
+ const env = readEnvFile(file);
35
+ if (!env.exists) {
36
+ checks.push("cognify config missing: documents are stored but not searchable");
37
+ } else if (env.error) {
38
+ errors.push(`${file} is not readable (${env.error})`);
39
+ } else {
40
+ if (tooOpen(file)) errors.push(`${file} must not be world/group readable (want mode 600)`);
41
+ else checks.push("cognee.env mode is private");
42
+ const mismatch = providerMismatchWarning(env.values);
43
+ if (mismatch) checks.push(mismatch);
44
+ if (isCognifyReady(env.values)) {
45
+ const provider = (env.values.get("LLM_PROVIDER") || "").toLowerCase();
46
+ const emb = (env.values.get("EMBEDDING_PROVIDER") || "").toLowerCase();
47
+ const extractor = (env.values.get("GRAPH_EXTRACTOR") || "").toLowerCase();
48
+ if (extractor === "gliner_demo" || emb === "fastembed") {
49
+ checks.push("cognee.env is cognify-ready (gliner + fastembed)");
50
+ } else if (provider === "ollama") {
51
+ checks.push("cognee.env is cognify-ready (ollama + embeddings)");
52
+ } else {
53
+ checks.push("cognee.env has LLM_API_KEY");
54
+ }
55
+ } else if (env.values.get("LLM_API_KEY")) {
56
+ checks.push("LLM_API_KEY set but cognify not ready (check LLM/EMBEDDING providers)");
57
+ } else {
58
+ checks.push("cognify config missing: documents are stored but not searchable");
59
+ }
60
+ }
61
+
62
+ // cognee-indexer.env: optional, COGNEE_API_KEY only.
63
+ const indexerFile = ctx.cognee.indexerEnvFile;
64
+ const indexerEnv = readEnvFile(indexerFile);
65
+ if (indexerEnv.exists) {
66
+ if (indexerEnv.error) errors.push(`${indexerFile} is not readable (${indexerEnv.error})`);
67
+ else {
68
+ if (tooOpen(indexerFile)) {
69
+ errors.push(`${indexerFile} must not be world/group readable (want mode 600)`);
70
+ }
71
+ if (indexerEnv.values.has("LLM_API_KEY")) {
72
+ errors.push(`${indexerFile} must not hold LLM_API_KEY (only COGNEE_API_KEY belongs there)`);
73
+ }
74
+ }
75
+ }
76
+
77
+ // Stack .env: the port, and no secret assignment lines.
78
+ const stackEnv = path.join(ctx.repoRoot, ".env");
79
+ if (fs.existsSync(stackEnv)) {
80
+ if (!h.envValue(stackEnv, "COGNEE_PORT")) errors.push("repo .env missing COGNEE_PORT");
81
+ if (SECRET_ASSIGNMENT.test(fs.readFileSync(stackEnv, "utf8"))) {
82
+ errors.push("repo .env must not contain LLM_API_KEY or COGNEE_API_KEY (secrets belong in the Cognee env files)");
83
+ }
84
+ checks.push("repo .env has COGNEE_PORT and no Cognee secrets");
85
+ }
86
+
87
+ // Compose fragment(s) from the manifest.
88
+ const fragments = ctx.memoryProvider?.compose?.files || ["providers/cognee/compose.yaml"];
89
+ for (const rel of fragments) {
90
+ const abs = path.join(ctx.repoRoot, rel);
91
+ if (!fs.existsSync(abs)) {
92
+ errors.push(`missing compose fragment ${rel}`);
93
+ } else if (UNFINISHED.test(fs.readFileSync(abs, "utf8"))) {
94
+ errors.push(`${rel} still contains unfinished image text`);
95
+ } else checks.push(`compose fragment ${rel} present`);
96
+ }
97
+ }