@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,86 @@
1
+ import { callUnifiedTool } from "../mcp/memory-facade.mjs";
2
+ import { recallBlock } from "./recall-frame.mjs";
3
+
4
+ export const HOOK_CONTEXT_MAX_CHARS = 8000;
5
+
6
+ function toolText(result) {
7
+ const t = result?.content?.[0]?.text;
8
+ if (!t) return "";
9
+ try {
10
+ const j = JSON.parse(t);
11
+ if (j?.result?.results) return JSON.stringify(j.result.results, null, 2);
12
+ if (j?.result) return typeof j.result === "string" ? j.result : JSON.stringify(j.result, null, 2);
13
+ return typeof j === "string" ? j : JSON.stringify(j, null, 2);
14
+ } catch {
15
+ return String(t);
16
+ }
17
+ }
18
+
19
+ function recallText(result, limit, maxChars = HOOK_CONTEXT_MAX_CHARS) {
20
+ try {
21
+ const value = JSON.parse(result?.content?.[0]?.text || "");
22
+ const hits = value?.result?.results;
23
+ if (Array.isArray(hits)) {
24
+ return hits.slice(0, limit)
25
+ .map((hit) => hit?.text ?? hit?.memory ?? hit?.content ?? "")
26
+ .filter((item) => typeof item === "string" && item.trim())
27
+ .join("\n\n")
28
+ .slice(0, maxChars);
29
+ }
30
+ } catch { /* use generic result text */ }
31
+ return toolText(result).slice(0, maxChars);
32
+ }
33
+
34
+ /**
35
+ * Hook or plugin recall text framed as untrusted data (recall-frame.mjs), at most HOOK_CONTEXT_MAX_CHARS
36
+ * characters frame included; "" for empty text.
37
+ */
38
+ export function hookRecallBlock(text) {
39
+ return recallBlock(String(text ?? "").trim(), HOOK_CONTEXT_MAX_CHARS);
40
+ }
41
+
42
+ /** @param {ReturnType<import("../mcp/context.mjs").resolveMcpContext>} ctx */
43
+ export async function sessionRecall(ctx, { query, bank, limit = 10, budget, maxTokens, maxChars }, fetchFn = fetch) {
44
+ const out = await callUnifiedTool(ctx, "memory_recall", { query, bank, limit, budget, maxTokens }, fetchFn);
45
+ if (out.isError) throw new Error(toolText(out));
46
+ return recallText(out, limit, maxChars);
47
+ }
48
+
49
+ /** @param {ReturnType<import("../mcp/context.mjs").resolveMcpContext>} ctx */
50
+ export async function sessionRetain(ctx, { content, bank }, fetchFn = fetch) {
51
+ const text = String(content || "").trim();
52
+ if (!text) return;
53
+ const out = await callUnifiedTool(ctx, "memory_add", { content: text, bank, async: true }, fetchFn);
54
+ if (out.isError) throw new Error(toolText(out));
55
+ }
56
+
57
+ /** SessionStart context via unified memory_reflect (all providers). */
58
+ export async function sessionReflectOrSearch(ctx, { query, bank, limit = 12 }, fetchFn = fetch) {
59
+ const out = await callUnifiedTool(ctx, "memory_reflect", { query, bank, limit }, fetchFn);
60
+ if (out.isError) throw new Error(toolText(out));
61
+ const body = toolText(out);
62
+ try {
63
+ const j = JSON.parse(body);
64
+ if (j.answer) return String(j.answer);
65
+ if (j.result) return typeof j.result === "string" ? j.result : JSON.stringify(j.result, null, 2);
66
+ if (j.hits) return String(j.hits);
67
+ } catch {
68
+ /* plain text */
69
+ }
70
+ return body;
71
+ }
72
+
73
+ /**
74
+ * True when the bank has pending or processing ingest (`memory_status` active > 0).
75
+ * Synchronous providers (mem0) and providers without ingest status (Cognee) report false.
76
+ */
77
+ export async function sessionIngestActive(ctx, { bank }, fetchFn = fetch) {
78
+ const out = await callUnifiedTool(ctx, "memory_status", { bank }, fetchFn);
79
+ if (out.isError) throw new Error(toolText(out));
80
+ try {
81
+ const status = JSON.parse(out?.content?.[0]?.text || "");
82
+ return status?.supported !== false && Number(status?.active) > 0;
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Text-preserving TOML edits: locate statements (table headers and key/value pairs) with a
3
+ * small lexer that understands strings, multi-line strings, comments and multi-line arrays,
4
+ * so an edit removes exactly one entry's text and leaves every other byte alone.
5
+ */
6
+
7
+ const BARE_KEY = /[A-Za-z0-9_-]/;
8
+
9
+ /** Offset just past the line break at or after `i` (or the end of the text). */
10
+ function lineEnd(text, i) {
11
+ const nl = text.indexOf("\n", i);
12
+ return nl < 0 ? text.length : nl + 1;
13
+ }
14
+
15
+ /** Offset after the string starting at `i` (any of the four TOML string forms). */
16
+ function skipString(text, i) {
17
+ const quote = text[i];
18
+ const triple = text.startsWith(quote.repeat(3), i);
19
+ if (triple) {
20
+ let j = i + 3;
21
+ while (j < text.length) {
22
+ if (quote === '"' && text[j] === "\\") { j += 2; continue; }
23
+ if (text.startsWith(quote.repeat(3), j)) {
24
+ j += 3;
25
+ // Up to two quotes may directly precede the closing delimiter.
26
+ for (let k = 0; k < 2 && text[j] === quote; k++) j++;
27
+ return j;
28
+ }
29
+ j++;
30
+ }
31
+ throw new SyntaxError("unterminated multi-line string");
32
+ }
33
+ let j = i + 1;
34
+ while (j < text.length && text[j] !== "\n") {
35
+ if (quote === '"' && text[j] === "\\") { j += 2; continue; }
36
+ if (text[j] === quote) return j + 1;
37
+ j++;
38
+ }
39
+ throw new SyntaxError("unterminated string");
40
+ }
41
+
42
+ /** Parse a dotted key at `i`; returns the segments and the offset after the key. */
43
+ function parseKey(text, i) {
44
+ const parts = [];
45
+ for (;;) {
46
+ while (text[i] === " " || text[i] === "\t") i++;
47
+ if (text[i] === '"' || text[i] === "'") {
48
+ const end = skipString(text, i);
49
+ const raw = text.slice(i, end);
50
+ if (raw[0] === "'") parts.push(raw.slice(1, -1));
51
+ else {
52
+ try { parts.push(JSON.parse(raw)); } catch { parts.push(raw.slice(1, -1)); }
53
+ }
54
+ i = end;
55
+ } else {
56
+ const start = i;
57
+ while (i < text.length && BARE_KEY.test(text[i])) i++;
58
+ if (i === start) throw new SyntaxError(`bad key at offset ${start}`);
59
+ parts.push(text.slice(start, i));
60
+ }
61
+ while (text[i] === " " || text[i] === "\t") i++;
62
+ if (text[i] !== ".") return { parts, end: i };
63
+ i++;
64
+ }
65
+ }
66
+
67
+ /** Offset after a value starting at `i`, through the end of its last line (comment included). */
68
+ function skipValue(text, i) {
69
+ let depth = 0;
70
+ while (i < text.length) {
71
+ const c = text[i];
72
+ if (c === '"' || c === "'") { i = skipString(text, i); continue; }
73
+ if (c === "#") { i = text.indexOf("\n", i); if (i < 0) return text.length; continue; }
74
+ if (c === "[" || c === "{") depth++;
75
+ else if (c === "]" || c === "}") depth--;
76
+ else if (c === "\n" && depth <= 0) return i + 1;
77
+ i++;
78
+ }
79
+ return text.length;
80
+ }
81
+
82
+ /**
83
+ * Statements of a TOML document in order: `{ kind: "table" | "array-table" | "kv", path, start, end }`.
84
+ * `path` is the full key path (a key/value pair includes its table's path); `start` is the
85
+ * offset of the statement's line and `end` the offset after its last line.
86
+ * @param {string} text
87
+ */
88
+ export function tomlStatements(text) {
89
+ const out = [];
90
+ let table = [];
91
+ let i = 0;
92
+ while (i < text.length) {
93
+ const start = i;
94
+ while (text[i] === " " || text[i] === "\t" || text[i] === "\r") i++;
95
+ if (i >= text.length) break;
96
+ if (text[i] === "\n") { i++; continue; }
97
+ if (text[i] === "#") { i = lineEnd(text, i); continue; }
98
+ if (text[i] === "[") {
99
+ const array = text[i + 1] === "[";
100
+ const key = parseKey(text, i + (array ? 2 : 1));
101
+ table = key.parts;
102
+ out.push({ kind: array ? "array-table" : "table", path: table, start, end: lineEnd(text, key.end) });
103
+ i = lineEnd(text, key.end);
104
+ continue;
105
+ }
106
+ const key = parseKey(text, i);
107
+ if (text[key.end] !== "=") throw new SyntaxError(`expected "=" at offset ${key.end}`);
108
+ const end = skipValue(text, key.end + 1);
109
+ out.push({ kind: "kv", path: [...table, ...key.parts], start, end });
110
+ i = end;
111
+ }
112
+ return out;
113
+ }
114
+
115
+ const startsWith = (pathParts, prefix) => prefix.every((p, k) => pathParts[k] === p);
116
+
117
+ /**
118
+ * Text spans to delete so no statement at or under `prefix` remains: whole tables (header through
119
+ * the table's last key/value pair, so comments that follow it stay) and single key/value pairs.
120
+ * @param {string} text
121
+ * @param {string[]} prefix
122
+ * @returns {{ start: number, end: number }[]}
123
+ */
124
+ export function tomlSpansUnder(text, prefix) {
125
+ const statements = tomlStatements(text);
126
+ const spans = [];
127
+ for (let s = 0; s < statements.length; s++) {
128
+ const st = statements[s];
129
+ if (st.kind === "kv") {
130
+ if (startsWith(st.path, prefix)) spans.push({ start: st.start, end: st.end });
131
+ continue;
132
+ }
133
+ if (!startsWith(st.path, prefix)) continue;
134
+ let end = st.end;
135
+ let k = s + 1;
136
+ for (; k < statements.length && statements[k].kind === "kv"; k++) end = statements[k].end;
137
+ spans.push({ start: st.start, end });
138
+ s = k - 1;
139
+ }
140
+ return spans;
141
+ }
142
+
143
+ /** Remove `spans` from `text`, also dropping blank lines a removal would leave doubled. */
144
+ export function removeSpans(text, spans) {
145
+ let out = text;
146
+ for (const { start, end } of [...spans].sort((a, b) => b.start - a.start)) {
147
+ let stop = end;
148
+ const before = out.slice(0, start);
149
+ if (start === 0 || /\n[ \t\r]*\n$/.test(before)) {
150
+ const m = /^(?:[ \t\r]*\n)+/.exec(out.slice(stop));
151
+ if (m) stop += m[0].length;
152
+ }
153
+ out = out.slice(0, start) + out.slice(stop);
154
+ }
155
+ return out;
156
+ }
157
+
158
+ /** Remove every statement at or under `prefix` (e.g. `["mcp_servers", "hindsight"]`). */
159
+ export function removeTomlPath(text, prefix) {
160
+ return removeSpans(text, tomlSpansUnder(text, prefix));
161
+ }
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Opt-in stdio MCP: read-only Cognee health + search against the local REST API.
4
+ * Base URL: COGNEE_API_URL / COGNEE_URL, else http://127.0.0.1:${COGNEE_PORT|8766}.
5
+ * Optional COGNEE_API_KEY → X-Api-Key (never written to mcp.json). Zero npm deps.
6
+ */
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { isMainModule } from "./is-main.mjs";
10
+ import { DEFAULT_PORT } from "../providers/cognee/lib/state.mjs";
11
+
12
+ export { DEFAULT_PORT };
13
+
14
+ /**
15
+ * @param {NodeJS.ProcessEnv} [env]
16
+ * @returns {string}
17
+ */
18
+ export function resolveBaseUrl(env = process.env) {
19
+ const fromUrl = String(env.COGNEE_API_URL || env.COGNEE_URL || "").trim();
20
+ if (fromUrl) return fromUrl.replace(/\/+$/, "");
21
+ const port = String(env.COGNEE_PORT || DEFAULT_PORT).trim() || String(DEFAULT_PORT);
22
+ return `http://127.0.0.1:${port}`;
23
+ }
24
+
25
+ /**
26
+ * @param {NodeJS.ProcessEnv} [env]
27
+ * @returns {Record<string, string>}
28
+ */
29
+ export function authHeaders(env = process.env) {
30
+ const key = String(env.COGNEE_API_KEY || "").trim();
31
+ return key ? { "X-Api-Key": key } : {};
32
+ }
33
+
34
+ /**
35
+ * @param {string} baseUrl
36
+ * @param {typeof fetch} [fetchFn]
37
+ * @param {NodeJS.ProcessEnv} [env]
38
+ */
39
+ export async function healthCheck(baseUrl, fetchFn = fetch, env = process.env) {
40
+ const res = await fetchFn(`${baseUrl.replace(/\/+$/, "")}/health`, {
41
+ headers: { ...authHeaders(env) },
42
+ });
43
+ const text = await res.text();
44
+ if (!res.ok) throw new Error(`GET /health → ${res.status}: ${text.slice(0, 400)}`);
45
+ try {
46
+ return JSON.parse(text);
47
+ } catch {
48
+ return text;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * @param {string} baseUrl
54
+ * @param {{ query: string, searchType?: string, datasets?: string[] }} body
55
+ * @param {typeof fetch} [fetchFn]
56
+ * @param {NodeJS.ProcessEnv} [env]
57
+ */
58
+ export async function search(baseUrl, body, fetchFn = fetch, env = process.env) {
59
+ /** @type {Record<string, unknown>} */
60
+ const payload = {
61
+ searchType: body.searchType || "GRAPH_COMPLETION",
62
+ query: body.query,
63
+ };
64
+ if (Array.isArray(body.datasets) && body.datasets.length) {
65
+ payload.datasets = body.datasets.map(String);
66
+ }
67
+ const res = await fetchFn(`${baseUrl.replace(/\/+$/, "")}/api/v1/search`, {
68
+ method: "POST",
69
+ headers: { "content-type": "application/json", ...authHeaders(env) },
70
+ body: JSON.stringify(payload),
71
+ });
72
+ const text = await res.text();
73
+ if (!res.ok) throw new Error(`POST /api/v1/search → ${res.status}: ${text.slice(0, 400)}`);
74
+ try {
75
+ return JSON.parse(text);
76
+ } catch {
77
+ return text;
78
+ }
79
+ }
80
+
81
+ const TOOLS = [
82
+ {
83
+ name: "cognee_health",
84
+ description: "GET /health on the local Cognee REST API. Confirms the stack is up.",
85
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
86
+ },
87
+ {
88
+ name: "cognee_search",
89
+ description:
90
+ "POST /api/v1/search (read-only). Args: query (required), optional searchType (default GRAPH_COMPLETION), optional datasets[]. Does not add or cognify.",
91
+ inputSchema: {
92
+ type: "object",
93
+ properties: {
94
+ query: { type: "string", description: "Search / completion query" },
95
+ searchType: {
96
+ type: "string",
97
+ description: "Cognee searchType (default GRAPH_COMPLETION)",
98
+ },
99
+ datasets: {
100
+ type: "array",
101
+ items: { type: "string" },
102
+ description: "Optional dataset name filter",
103
+ },
104
+ },
105
+ required: ["query"],
106
+ additionalProperties: false,
107
+ },
108
+ },
109
+ ];
110
+
111
+ function textResult(value, isError = false) {
112
+ const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
113
+ return { content: [{ type: "text", text }], isError };
114
+ }
115
+
116
+ /**
117
+ * @param {{ baseUrl: string, env?: NodeJS.ProcessEnv }} access
118
+ * @param {string} name
119
+ * @param {Record<string, unknown>} [args]
120
+ * @param {typeof fetch} [fetchFn]
121
+ */
122
+ export async function callTool(access, name, args = {}, fetchFn = fetch) {
123
+ const env = access.env || process.env;
124
+ if (name === "cognee_health") {
125
+ return textResult(await healthCheck(access.baseUrl, fetchFn, env));
126
+ }
127
+ if (name === "cognee_search") {
128
+ const query = String(args.query || "").trim();
129
+ if (!query) return textResult("query is required", true);
130
+ /** @type {{ query: string, searchType?: string, datasets?: string[] }} */
131
+ const body = { query };
132
+ if (args.searchType != null && String(args.searchType).trim()) {
133
+ body.searchType = String(args.searchType).trim();
134
+ }
135
+ if (Array.isArray(args.datasets)) {
136
+ body.datasets = args.datasets.map(String);
137
+ }
138
+ return textResult({
139
+ query,
140
+ searchType: body.searchType || "GRAPH_COMPLETION",
141
+ result: await search(access.baseUrl, body, fetchFn, env),
142
+ });
143
+ }
144
+ return textResult(`unknown tool: ${name}`, true);
145
+ }
146
+
147
+ /**
148
+ * @param {Record<string, any>} msg
149
+ * @param {{ baseUrl: string, env?: NodeJS.ProcessEnv }} access
150
+ * @param {typeof fetch} [fetchFn]
151
+ */
152
+ export async function handleMessage(msg, access, fetchFn = fetch) {
153
+ const { id, method, params } = msg;
154
+ if (method === "initialize") {
155
+ const protocolVersion =
156
+ typeof params?.protocolVersion === "string" && params.protocolVersion
157
+ ? params.protocolVersion
158
+ : "2024-11-05";
159
+ return {
160
+ jsonrpc: "2.0",
161
+ id,
162
+ result: {
163
+ protocolVersion,
164
+ capabilities: { tools: {} },
165
+ serverInfo: { name: "cognee", version: "0.1.0" },
166
+ },
167
+ };
168
+ }
169
+ if (method === "notifications/initialized" || method === "initialized") return null;
170
+ if (method === "tools/list") return { jsonrpc: "2.0", id, result: { tools: TOOLS } };
171
+ if (method === "tools/call") {
172
+ try {
173
+ return { jsonrpc: "2.0", id, result: await callTool(access, params?.name, params?.arguments || {}, fetchFn) };
174
+ } catch (err) {
175
+ return { jsonrpc: "2.0", id, result: textResult(err instanceof Error ? err.message : String(err), true) };
176
+ }
177
+ }
178
+ if (method === "ping") return { jsonrpc: "2.0", id, result: {} };
179
+ if (id === undefined) return null;
180
+ return { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${method}` } };
181
+ }
182
+
183
+ /** @param {"ndjson" | "content-length"} framing */
184
+ function writeMessage(obj, framing = "ndjson") {
185
+ if (obj == null) return;
186
+ const body = JSON.stringify(obj);
187
+ // Cursor Agent CLI speaks NDJSON on stdio. Content-Length kept for LSP-style clients.
188
+ if (framing === "content-length") {
189
+ process.stdout.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`);
190
+ return;
191
+ }
192
+ process.stdout.write(`${body}\n`);
193
+ }
194
+
195
+ /** Content-Length frames or NDJSON; reply using the same framing the request used. */
196
+ export async function serveStdio(input, onMsg) {
197
+ let buf = Buffer.alloc(0);
198
+ for await (const chunk of input) {
199
+ buf = Buffer.concat([buf, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
200
+ while (buf.length) {
201
+ const asStr = buf.toString("utf8");
202
+ const crlf = asStr.indexOf("\r\n\r\n");
203
+ const lf = asStr.indexOf("\n\n");
204
+ const headerEnd = crlf >= 0 ? crlf : lf;
205
+ const sepLen = crlf >= 0 ? 4 : lf >= 0 ? 2 : -1;
206
+ if (sepLen > 0 && /content-length\s*:/i.test(asStr.slice(0, headerEnd))) {
207
+ const m = /Content-Length:\s*(\d+)/i.exec(asStr.slice(0, headerEnd));
208
+ if (!m) {
209
+ buf = buf.subarray(Buffer.byteLength(asStr.slice(0, headerEnd + sepLen), "utf8"));
210
+ continue;
211
+ }
212
+ const len = Number(m[1]);
213
+ const headerBytes = Buffer.byteLength(asStr.slice(0, headerEnd + sepLen), "utf8");
214
+ if (buf.length < headerBytes + len) break;
215
+ const body = buf.subarray(headerBytes, headerBytes + len).toString("utf8");
216
+ buf = buf.subarray(headerBytes + len);
217
+ let msg;
218
+ try {
219
+ msg = JSON.parse(body);
220
+ } catch (err) {
221
+ writeMessage(
222
+ {
223
+ jsonrpc: "2.0",
224
+ id: null,
225
+ error: { code: -32700, message: `Parse error: ${err instanceof Error ? err.message : err}` },
226
+ },
227
+ "content-length",
228
+ );
229
+ continue;
230
+ }
231
+ writeMessage(await onMsg(msg), "content-length");
232
+ continue;
233
+ }
234
+ const nl = asStr.indexOf("\n");
235
+ if (nl < 0) break;
236
+ const line = asStr.slice(0, nl).trim();
237
+ buf = buf.subarray(Buffer.byteLength(asStr.slice(0, nl + 1), "utf8"));
238
+ if (!line.startsWith("{")) continue;
239
+ try {
240
+ writeMessage(await onMsg(JSON.parse(line)), "ndjson");
241
+ } catch {
242
+ /* skip bad line */
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ async function main() {
249
+ const baseUrl = resolveBaseUrl(process.env);
250
+ const access = { baseUrl, env: process.env };
251
+ process.stdin.resume();
252
+ await serveStdio(process.stdin, (msg) => handleMessage(msg, access));
253
+ }
254
+
255
+ if (isMainModule(import.meta.url)) {
256
+ main().catch((err) => {
257
+ console.error(`cognee-mcp: ${err instanceof Error ? err.message : err}`);
258
+ process.exit(1);
259
+ });
260
+ }