@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,97 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { SHIM_AGENT_IDS, SHIM_HARNESSES, SHIM_MARKER } from "./hook-harnesses.mjs";
4
+ import { mergeOpencodeConfig, opencodeConfigPath } from "./opencode-config.mjs";
5
+
6
+ function hookCommand(root, harnessId, phase) {
7
+ return `node ${JSON.stringify(path.join(root, "cli", "coding-agent", "hook-runner.mjs"))} ${harnessId} ${phase}`;
8
+ }
9
+
10
+ function nestedHookBlock(root, harnessId, phase, timeout) {
11
+ return {
12
+ hooks: [{ type: "command", command: hookCommand(root, harnessId, phase), timeout }],
13
+ };
14
+ }
15
+
16
+ function mergeNestedHooks(existing, root, harnessId, spec) {
17
+ const hooks = existing && typeof existing === "object" ? { ...existing } : {};
18
+ for (const [phase, meta] of Object.entries(spec.events)) {
19
+ const list = Array.isArray(hooks[meta.hook]) ? hooks[meta.hook].filter((e) => !JSON.stringify(e).includes(SHIM_MARKER)) : [];
20
+ hooks[meta.hook] = [...list, nestedHookBlock(root, harnessId, phase, meta.timeout)];
21
+ }
22
+ return hooks;
23
+ }
24
+
25
+ function mergeFlatHooks(existing, root, harnessId, spec) {
26
+ const hooks = existing && typeof existing === "object" ? { ...existing } : {};
27
+ for (const [phase, meta] of Object.entries(spec.events)) {
28
+ const list = Array.isArray(hooks[meta.hook]) ? hooks[meta.hook].filter((e) => !String(e?.command).includes(SHIM_MARKER)) : [];
29
+ hooks[meta.hook] = [
30
+ ...list,
31
+ { command: hookCommand(root, harnessId, phase), timeout: meta.timeout },
32
+ ];
33
+ }
34
+ return hooks;
35
+ }
36
+
37
+ function readJson(pathname) {
38
+ if (!fs.existsSync(pathname)) return {};
39
+ return JSON.parse(fs.readFileSync(pathname, "utf8"));
40
+ }
41
+
42
+ function writeJson(pathname, data) {
43
+ fs.mkdirSync(path.dirname(pathname), { recursive: true });
44
+ fs.writeFileSync(pathname, `${JSON.stringify(data, null, 2)}\n`);
45
+ }
46
+
47
+ /**
48
+ * @param {{ home: string, repoRoot: string, agents: string[], dry: boolean, h?: { info: Function, ok: Function, warn: Function } }} opts
49
+ */
50
+ export function installSessionShim(opts) {
51
+ const home = opts.home;
52
+ const h = opts.h || { info: () => {}, ok: () => {}, warn: () => {} };
53
+ const agents = opts.agents.filter((id) => SHIM_AGENT_IDS.includes(id) || id === "opencode");
54
+ const opencodePlugin = path.join(opts.repoRoot, "cli", "coding-agent", "opencode-plugin.mjs");
55
+ if (!agents.length) {
56
+ h.warn("no agents selected for session shim (claude-code, codex, cursor-cli, opencode)");
57
+ return { installed: [] };
58
+ }
59
+
60
+ const installed = [];
61
+ for (const agentId of agents) {
62
+ if (agentId === "opencode") {
63
+ const cfgPath = opencodeConfigPath(home);
64
+ if (opts.dry) {
65
+ h.info(`[dry-run] register opencode plugin ${opencodePlugin} in ${cfgPath}`);
66
+ } else {
67
+ mergeOpencodeConfig(home, "plugin", (existing) => {
68
+ const plugins = Array.isArray(existing)
69
+ ? existing.filter((p) => !/openmem|personal-memory/.test(String(p)))
70
+ : [];
71
+ return [...plugins, opencodePlugin];
72
+ });
73
+ h.ok(`opencode: registered openmem plugin in ${cfgPath}`);
74
+ }
75
+ installed.push("opencode");
76
+ continue;
77
+ }
78
+ const spec = SHIM_HARNESSES[agentId];
79
+ if (!spec) continue;
80
+ const cfgPath = spec.settingsPath(home);
81
+ if (opts.dry) {
82
+ h.info(`[dry-run] merge ${agentId} hooks in ${cfgPath}`);
83
+ installed.push(agentId);
84
+ continue;
85
+ }
86
+ const prev = readJson(cfgPath);
87
+ if (spec.configStyle === "nested") {
88
+ prev[spec.rootKey] = mergeNestedHooks(prev[spec.rootKey], opts.repoRoot, agentId, spec);
89
+ } else {
90
+ Object.assign(prev, mergeFlatHooks(prev, opts.repoRoot, agentId, spec));
91
+ }
92
+ writeJson(cfgPath, prev);
93
+ h.ok(`${agentId}: session hooks → ${cfgPath}`);
94
+ installed.push(agentId);
95
+ }
96
+ return { installed };
97
+ }
@@ -0,0 +1,91 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function opencodeConfigPath(home) {
5
+ const dir = path.join(home, ".config", "opencode");
6
+ for (const name of ["opencode.jsonc", "opencode.json"]) {
7
+ const file = path.join(dir, name);
8
+ if (fs.existsSync(file)) return file;
9
+ }
10
+ return path.join(dir, "opencode.json");
11
+ }
12
+
13
+ function tokens(source) {
14
+ const out = [];
15
+ for (let i = 0; i < source.length;) {
16
+ if (/\s/.test(source[i])) { i++; continue; }
17
+ if (source.startsWith("//", i)) { i = source.indexOf("\n", i); if (i < 0) break; continue; }
18
+ if (source.startsWith("/*", i)) {
19
+ const end = source.indexOf("*/", i + 2);
20
+ if (end < 0) throw new SyntaxError("Unclosed JSONC comment");
21
+ i = end + 2;
22
+ continue;
23
+ }
24
+ const start = i;
25
+ if (source[i] === '"') {
26
+ i++;
27
+ while (i < source.length) {
28
+ if (source[i++] === '"') break;
29
+ if (source[i - 1] === "\\") i++;
30
+ }
31
+ } else if ("{}[]:,".includes(source[i])) i++;
32
+ else while (i < source.length && !/[\s{}\[\]:,]/.test(source[i]) && !source.startsWith("//", i) && !source.startsWith("/*", i)) i++;
33
+ out.push({ value: source.slice(start, i), start, end: i });
34
+ }
35
+ return out;
36
+ }
37
+
38
+ function parseJsonc(source) {
39
+ const list = tokens(source);
40
+ const compact = list.filter((token, i) => token.value !== "," || !["}", "]"].includes(list[i + 1]?.value));
41
+ return JSON.parse(compact.map((token) => token.value).join(""));
42
+ }
43
+
44
+ function fieldSpan(list, name) {
45
+ let depth = 0;
46
+ let found = null;
47
+ for (let i = 0; i < list.length; i++) {
48
+ const token = list[i];
49
+ if (depth === 1 && token.value.startsWith('"') && list[i + 1]?.value === ":" && JSON.parse(token.value) === name) {
50
+ const start = list[i + 2]?.start;
51
+ if (start === undefined) throw new SyntaxError(`Missing JSONC value for ${name}`);
52
+ let nested = 0;
53
+ let end = start;
54
+ for (let j = i + 2; j < list.length; j++) {
55
+ const value = list[j].value;
56
+ if (nested === 0 && (value === "," || value === "}")) break;
57
+ if (value === "{" || value === "[") nested++;
58
+ if (value === "}" || value === "]") nested--;
59
+ end = list[j].end;
60
+ }
61
+ found = { start, end };
62
+ }
63
+ if (token.value === "{" || token.value === "[") depth++;
64
+ if (token.value === "}" || token.value === "]") depth--;
65
+ }
66
+ return found;
67
+ }
68
+
69
+ /** Update one top-level JSONC field while retaining text and comments outside that field. */
70
+ export function mergeOpencodeConfig(home, field, update) {
71
+ const file = opencodeConfigPath(home);
72
+ const source = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "{}\n";
73
+ const parsed = parseJsonc(source);
74
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new SyntaxError("OpenCode config must be an object");
75
+ const value = update(parsed[field]);
76
+ const list = tokens(source);
77
+ const span = fieldSpan(list, field);
78
+ let output;
79
+ if (span) output = source.slice(0, span.start) + JSON.stringify(value, null, 2) + source.slice(span.end);
80
+ else {
81
+ const close = list.findLast((token) => token.value === "}");
82
+ if (!close) throw new SyntaxError("OpenCode config has no closing object brace");
83
+ const hasFields = Object.keys(parsed).length > 0;
84
+ const last = list[list.indexOf(close) - 1];
85
+ const separator = hasFields && last?.value !== "," ? "," : "";
86
+ output = source.slice(0, close.start) + `${separator}\n ${JSON.stringify(field)}: ${JSON.stringify(value, null, 2)}\n` + source.slice(close.start);
87
+ }
88
+ fs.mkdirSync(path.dirname(file), { recursive: true });
89
+ fs.writeFileSync(file, output);
90
+ return file;
91
+ }
@@ -0,0 +1,96 @@
1
+ import os from "node:os";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { bankFromCwd } from "./bank-from-cwd.mjs";
6
+ import { accessAllowed, readAccessConfig } from "./access-policy.mjs";
7
+ import { sessionRecall, sessionReflectOrSearch, sessionRetain } from "./session-facade.mjs";
8
+ import { resolveMcpContext } from "../mcp/context.mjs";
9
+ import { resolveStackRoot } from "../stack.mjs";
10
+
11
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
12
+ const MAX_SESSIONS = 128;
13
+ const MAX_RETAIN_CHARS = 12000;
14
+ function messageText(item) {
15
+ return (item.parts || []).filter((part) => part.type === "text" && part.text).map((part) => part.text).join("\n");
16
+ }
17
+
18
+ /** OpenCode v1 local plugin: prompt recall, system context, and per-turn retention. */
19
+ export const PersonalMemoryPlugin = async ({ client, directory, worktree }) => {
20
+ const home = process.env.HOME || os.homedir();
21
+ let stackRoot;
22
+ try {
23
+ stackRoot = JSON.parse(fs.readFileSync(path.join(ROOT, "agent-runtime.json"), "utf8")).stackRoot;
24
+ } catch {
25
+ // A checkout uses its ordinary stack resolution.
26
+ }
27
+ const env = { ...process.env, PM_STACK_ROOT: process.env.PM_STACK_ROOT || stackRoot };
28
+ const stack = resolveStackRoot({ pkgRoot: ROOT, home, env });
29
+ const ctx = resolveMcpContext({ home, stackRoot: stack.root, repoRoot: ROOT, env });
30
+ const projectDir = worktree && path.parse(worktree).root !== worktree ? worktree : directory;
31
+ const accessConfig = readAccessConfig(home);
32
+ const allowed = accessAllowed(projectDir, accessConfig);
33
+ if (!allowed) {
34
+ return {
35
+ "chat.message": async () => {},
36
+ "experimental.chat.system.transform": async () => {},
37
+ event: async () => {},
38
+ };
39
+ }
40
+ const bank = bankFromCwd(projectDir, { home, mapPathToBank: accessConfig.mapPathToBank });
41
+ const injections = new Map();
42
+ const seen = new Map();
43
+
44
+ function remember(sessionID, messageID) {
45
+ seen.delete(sessionID);
46
+ seen.set(sessionID, messageID);
47
+ while (seen.size > MAX_SESSIONS) seen.delete(seen.keys().next().value);
48
+ }
49
+
50
+ return {
51
+ "chat.message": async (input, output) => {
52
+ const prompt = messageText(output).trim();
53
+ if (!prompt) return;
54
+ injections.delete(input.sessionID);
55
+ injections.set(input.sessionID, "");
56
+ while (injections.size > MAX_SESSIONS) injections.delete(injections.keys().next().value);
57
+ try {
58
+ const query = prompt.slice(0, 2000);
59
+ const context = seen.has(input.sessionID)
60
+ ? await sessionRecall(ctx, { query, bank, limit: 8 })
61
+ : await sessionReflectOrSearch(ctx, { query, bank, limit: 12 });
62
+ injections.set(input.sessionID, context);
63
+ } catch { /* Memory availability must not interrupt the prompt. */ }
64
+ },
65
+ "experimental.chat.system.transform": async (input, output) => {
66
+ const context = injections.get(input.sessionID);
67
+ if (context) output.system.push(`Personal memory for ${bank}:\n${context}`);
68
+ },
69
+ event: async ({ event }) => {
70
+ if (event.type !== "session.idle") return;
71
+ const sessionID = event.properties?.sessionID;
72
+ if (!sessionID || !injections.has(sessionID)) return;
73
+ try {
74
+ const response = await client.session.messages({ path: { id: sessionID } });
75
+ const messages = response.data || [];
76
+ const previous = seen.get(sessionID);
77
+ const lastIndex = previous ? messages.findIndex((item) => item.info?.id === previous) : -1;
78
+ // OpenCode may replace old message IDs when a session is compacted.
79
+ // In that case retain a bounded tail instead of silently losing the
80
+ // whole next turn. A small amount of overlap is preferable to a gap.
81
+ const fresh = previous && lastIndex >= 0 ? messages.slice(lastIndex + 1) : messages;
82
+ const content = fresh.map((item) => {
83
+ const text = messageText(item).trim();
84
+ return text ? `${item.info?.role || "message"}: ${text}` : "";
85
+ }).filter(Boolean).join("\n\n").slice(-MAX_RETAIN_CHARS);
86
+ if (content) await sessionRetain(ctx, { bank, content: `OpenCode session ${sessionID}:\n${content}` });
87
+ const last = messages.at(-1)?.info?.id;
88
+ if (last) remember(sessionID, last);
89
+ } catch {
90
+ // Memory availability must not interrupt OpenCode's session.
91
+ } finally {
92
+ injections.delete(sessionID);
93
+ }
94
+ },
95
+ };
96
+ };
@@ -0,0 +1,39 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+
5
+ const RUNTIME_ITEMS = ["cli", "providers", "wizard", "config", "skills", "package.json"];
6
+ const RUNTIME_DEPS = ["@iarna/toml", "@clack/prompts", "yaml"];
7
+
8
+ function packageRoot(requireFromSource, name) {
9
+ let dir = path.dirname(requireFromSource.resolve(name));
10
+ while (dir !== path.dirname(dir)) {
11
+ const manifest = path.join(dir, "package.json");
12
+ if (fs.existsSync(manifest) && JSON.parse(fs.readFileSync(manifest, "utf8")).name === name) return dir;
13
+ dir = path.dirname(dir);
14
+ }
15
+ throw new Error(`cannot locate ${name} for openmem runtime`);
16
+ }
17
+
18
+ /** Copy the agent runtime out of an ephemeral npx cache into a stable user path. */
19
+ export function prepareAgentRuntime({ home, repoRoot, stackRoot, dry = false, h }) {
20
+ const dest = path.join(home, ".local", "share", "personal-memory", "agent-runtime");
21
+ if (dry) {
22
+ h?.info(`[dry-run] stage openmem agent runtime in ${dest}`);
23
+ return dest;
24
+ }
25
+ const requireFromSource = createRequire(path.join(repoRoot, "package.json"));
26
+ fs.mkdirSync(dest, { recursive: true });
27
+ for (const item of RUNTIME_ITEMS) {
28
+ fs.cpSync(path.join(repoRoot, item), path.join(dest, item), { recursive: true, force: true });
29
+ }
30
+ fs.rmSync(path.join(dest, "cli", "coding-agent", "opencode-plugin"), { recursive: true, force: true });
31
+ for (const name of RUNTIME_DEPS) {
32
+ const target = path.join(dest, "node_modules", name);
33
+ fs.mkdirSync(path.dirname(target), { recursive: true });
34
+ fs.cpSync(packageRoot(requireFromSource, name), target, { recursive: true, force: true });
35
+ }
36
+ fs.writeFileSync(path.join(dest, "agent-runtime.json"), `${JSON.stringify({ stackRoot }, null, 2)}\n`);
37
+ h?.ok(`openmem agent runtime → ${dest}`);
38
+ return dest;
39
+ }
@@ -0,0 +1,62 @@
1
+ import { callUnifiedTool } from "../mcp/memory-facade.mjs";
2
+
3
+ const HOOK_CONTEXT_MAX_CHARS = 8000;
4
+
5
+ function toolText(result) {
6
+ const t = result?.content?.[0]?.text;
7
+ if (!t) return "";
8
+ try {
9
+ const j = JSON.parse(t);
10
+ if (j?.result?.results) return JSON.stringify(j.result.results, null, 2);
11
+ if (j?.result) return typeof j.result === "string" ? j.result : JSON.stringify(j.result, null, 2);
12
+ return typeof j === "string" ? j : JSON.stringify(j, null, 2);
13
+ } catch {
14
+ return String(t);
15
+ }
16
+ }
17
+
18
+ function recallText(result, limit) {
19
+ try {
20
+ const value = JSON.parse(result?.content?.[0]?.text || "");
21
+ const hits = value?.result?.results;
22
+ if (Array.isArray(hits)) {
23
+ return hits.slice(0, limit)
24
+ .map((hit) => hit?.text ?? hit?.memory ?? hit?.content ?? "")
25
+ .filter((item) => typeof item === "string" && item.trim())
26
+ .join("\n\n")
27
+ .slice(0, HOOK_CONTEXT_MAX_CHARS);
28
+ }
29
+ } catch { /* use generic result text */ }
30
+ return toolText(result).slice(0, HOOK_CONTEXT_MAX_CHARS);
31
+ }
32
+
33
+ /** @param {ReturnType<import("../mcp/context.mjs").resolveMcpContext>} ctx */
34
+ export async function sessionRecall(ctx, { query, bank, limit = 10, budget, maxTokens }, fetchFn = fetch) {
35
+ const out = await callUnifiedTool(ctx, "memory_recall", { query, bank, limit, budget, maxTokens }, fetchFn);
36
+ if (out.isError) throw new Error(toolText(out));
37
+ return recallText(out, limit);
38
+ }
39
+
40
+ /** @param {ReturnType<import("../mcp/context.mjs").resolveMcpContext>} ctx */
41
+ export async function sessionRetain(ctx, { content, bank }, fetchFn = fetch) {
42
+ const text = String(content || "").trim();
43
+ if (!text) return;
44
+ const out = await callUnifiedTool(ctx, "memory_add", { content: text, bank, async: true }, fetchFn);
45
+ if (out.isError) throw new Error(toolText(out));
46
+ }
47
+
48
+ /** SessionStart context via unified memory_reflect (all providers). */
49
+ export async function sessionReflectOrSearch(ctx, { query, bank, limit = 12 }, fetchFn = fetch) {
50
+ const out = await callUnifiedTool(ctx, "memory_reflect", { query, bank, limit }, fetchFn);
51
+ if (out.isError) throw new Error(toolText(out));
52
+ const body = toolText(out);
53
+ try {
54
+ const j = JSON.parse(body);
55
+ if (j.answer) return String(j.answer);
56
+ if (j.result) return typeof j.result === "string" ? j.result : JSON.stringify(j.result, null, 2);
57
+ if (j.hits) return String(j.hits);
58
+ } catch {
59
+ /* plain text */
60
+ }
61
+ return body;
62
+ }
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /** Run indexer/feed_sync.py (RSS + OpenAlex → markdown cache). */
3
+ import { spawnSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const script = path.join(PKG_ROOT, "indexer", "feed_sync.py");
9
+
10
+ const r = spawnSync("python3", [script, ...process.argv.slice(2)], { stdio: "inherit", cwd: PKG_ROOT });
11
+ process.exit(r.status ?? 1);