@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,61 @@
1
+ import path from "node:path";
2
+ import { envValue } from "../../wizard/lib/paths.mjs";
3
+ import { resolveMemoryProvider } from "../install-memory-mcp.mjs";
4
+ import { resolveMem0ApiUrl, resolveMem0EnvFilePath } from "../../providers/mem0/lib/mcp-install.mjs";
5
+ import { resolveCogneeApiUrl } from "../../providers/cognee/lib/cognee-mcp-install.mjs";
6
+ import { readKey } from "../../providers/mem0/lib/env-file.mjs";
7
+
8
+ const DEFAULT_HINDSIGHT_PORT = 8888;
9
+
10
+ /**
11
+ * @param {{ home: string, stackRoot: string, env?: NodeJS.ProcessEnv }} opts
12
+ */
13
+ export function resolveHindsightApiUrl({ home, stackRoot, env = process.env }) {
14
+ const candidates = [
15
+ env.HINDSIGHT_URL,
16
+ env.PM_HINDSIGHT_URL,
17
+ envValue(path.join(stackRoot, ".env"), "HINDSIGHT_URL"),
18
+ envValue(path.join(home, ".config/hindsight/wizard.conf"), "HINDSIGHT_URL"),
19
+ ];
20
+ const url = candidates.map((v) => String(v || "").trim()).find(Boolean);
21
+ if (url) return url.replace(/\/+$/, "");
22
+ const portCandidates = [
23
+ env.HINDSIGHT_PORT,
24
+ envValue(path.join(stackRoot, ".env"), "HINDSIGHT_PORT"),
25
+ envValue(path.join(home, ".config/hindsight/wizard.conf"), "HINDSIGHT_PORT"),
26
+ ];
27
+ const port = portCandidates.map((v) => String(v || "").trim()).find(Boolean) || String(DEFAULT_HINDSIGHT_PORT);
28
+ return `http://127.0.0.1:${port}`;
29
+ }
30
+
31
+ /**
32
+ * @param {{ home: string, stackRoot: string, repoRoot: string, env?: NodeJS.ProcessEnv }} opts
33
+ */
34
+ export function resolveMcpContext(opts) {
35
+ const env = opts.env || process.env;
36
+ const home = opts.home;
37
+ const stackRoot = opts.stackRoot;
38
+ const provider = resolveMemoryProvider(home, env);
39
+ /** @type {{ provider: string, hindsightUrl?: string, mem0?: { baseUrl: string, apiKey?: string }, cognee?: { baseUrl: string, apiKey?: string } }} */
40
+ const ctx = { provider };
41
+ if (provider === "hindsight") {
42
+ ctx.hindsightUrl = resolveHindsightApiUrl({ home, stackRoot, env });
43
+ } else if (provider === "mem0") {
44
+ const baseUrl = resolveMem0ApiUrl({ home, stackRoot, env });
45
+ const envFile = resolveMem0EnvFilePath({ home, stackRoot, env });
46
+ let apiKey = String(env.MEM0_API_KEY || "").trim();
47
+ if (!apiKey) {
48
+ try {
49
+ apiKey = readKey(envFile) || "";
50
+ } catch {
51
+ apiKey = "";
52
+ }
53
+ }
54
+ ctx.mem0 = { baseUrl, apiKey: apiKey || undefined };
55
+ } else if (provider === "cognee") {
56
+ const baseUrl = resolveCogneeApiUrl({ home, stackRoot, repoRoot: opts.repoRoot, env });
57
+ const apiKey = String(env.COGNEE_API_KEY || "").trim() || undefined;
58
+ ctx.cognee = { baseUrl, apiKey };
59
+ }
60
+ return ctx;
61
+ }
@@ -0,0 +1,12 @@
1
+ import crypto from "node:crypto";
2
+
3
+ const SLUG_MAX = 40;
4
+
5
+ /** Match `indexer/sinks/cognee.py` `dataset_name(bank)`. */
6
+ export function cogneeDatasetName(bank) {
7
+ const raw = String(bank ?? "");
8
+ let slug = raw.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
9
+ slug = slug.slice(0, SLUG_MAX).replace(/^_+|_+$/g, "") || "bank";
10
+ const hash = crypto.createHash("sha256").update(raw).digest("hex").slice(0, 8);
11
+ return `${slug}_${hash}`;
12
+ }
@@ -0,0 +1,119 @@
1
+ import { authHeaders } from "../../cognee-mcp.mjs";
2
+ import { cogneeDatasetName } from "./cognee-dataset-name.mjs";
3
+
4
+ const PAGE = 1000;
5
+
6
+ /**
7
+ * @param {unknown} value
8
+ * @returns {string}
9
+ */
10
+ function trimString(value) {
11
+ return typeof value === "string" ? value.trim() : "";
12
+ }
13
+
14
+ /**
15
+ * @param {unknown} raw
16
+ * @returns {string}
17
+ */
18
+ function textFromRawData(raw) {
19
+ if (typeof raw === "string") return raw.trim();
20
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
21
+ return textFromRecord(raw);
22
+ }
23
+ return "";
24
+ }
25
+
26
+ /**
27
+ * Read `text`, `content`, or nested `raw_data` / `rawData` from a plain object.
28
+ * @param {unknown} obj
29
+ * @returns {string}
30
+ */
31
+ function textFromRecord(obj) {
32
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return "";
33
+ const rec = /** @type {Record<string, unknown>} */ (obj);
34
+ for (const key of ["text", "content"]) {
35
+ const s = trimString(rec[key]);
36
+ if (s) return s;
37
+ }
38
+ const fromRaw = textFromRawData(rec.raw_data ?? rec.rawData);
39
+ if (fromRaw) return fromRaw;
40
+ return "";
41
+ }
42
+
43
+ /**
44
+ * Prefer full document bytes/text from a Cognee dataset data row over label-only fields.
45
+ * @param {Record<string, unknown>} item
46
+ * @returns {string}
47
+ */
48
+ export function cogneeDataItemContent(item) {
49
+ const fromItem = textFromRecord(item);
50
+ if (fromItem) return fromItem;
51
+ const fromDoc = textFromRecord(item.document ?? item.doc);
52
+ if (fromDoc) return fromDoc;
53
+ return trimString(item.label) || trimString(item.name) || "";
54
+ }
55
+
56
+ /**
57
+ * @param {{ cognee?: { baseUrl: string, apiKey?: string } }} ctx
58
+ * @param {{ bank?: string }} args
59
+ * @param {typeof fetch} fetchFn
60
+ * @param {NodeJS.ProcessEnv} [env]
61
+ */
62
+ export async function exportCognee(ctx, args, fetchFn, env = process.env) {
63
+ const base = ctx.cognee?.baseUrl?.replace(/\/+$/, "");
64
+ if (!base) throw new Error("Cognee API URL not configured");
65
+ const headers = { ...authHeaders(env) };
66
+ const bank = String(args.bank || "openmem-mcp").trim() || "openmem-mcp";
67
+ const datasetName = cogneeDatasetName(bank);
68
+
69
+ const listRes = await fetchFn(`${base}/api/v1/datasets`, { headers });
70
+ const listText = await listRes.text();
71
+ if (!listRes.ok) throw new Error(`GET /api/v1/datasets → ${listRes.status}: ${listText.slice(0, 400)}`);
72
+ let datasets;
73
+ try {
74
+ datasets = listText ? JSON.parse(listText) : [];
75
+ } catch {
76
+ throw new Error("GET /api/v1/datasets: invalid JSON");
77
+ }
78
+ const row = (Array.isArray(datasets) ? datasets : []).find(
79
+ (d) => d && typeof d === "object" && d.name === datasetName && d.id,
80
+ );
81
+ if (!row) {
82
+ return {
83
+ scope: { bank, dataset: datasetName },
84
+ records: [],
85
+ note: `No Cognee dataset named ${datasetName}; indexer may not have written to this bank yet.`,
86
+ };
87
+ }
88
+ const datasetId = String(row.id);
89
+ /** @type {object[]} */
90
+ const records = [];
91
+ let offset = 0;
92
+ while (true) {
93
+ const url = `${base}/api/v1/datasets/${encodeURIComponent(datasetId)}/data?limit=${PAGE}&offset=${offset}`;
94
+ const res = await fetchFn(url, { headers });
95
+ const text = await res.text();
96
+ if (!res.ok) throw new Error(`GET dataset data → ${res.status}: ${text.slice(0, 400)}`);
97
+ let page;
98
+ try {
99
+ page = text ? JSON.parse(text) : [];
100
+ } catch {
101
+ throw new Error("GET dataset data: invalid JSON");
102
+ }
103
+ if (!Array.isArray(page)) break;
104
+ for (const item of page) {
105
+ if (!item || typeof item !== "object") continue;
106
+ const meta = item.externalMetadata ?? item.external_metadata ?? {};
107
+ const body = cogneeDataItemContent(item);
108
+ records.push({
109
+ id: item.id != null ? String(item.id) : null,
110
+ content: body || `[cognee:${item.name || item.id}]`,
111
+ metadata: { cognee: { name: item.name ?? null, externalMetadata: meta } },
112
+ scope_key: bank,
113
+ });
114
+ }
115
+ if (page.length < PAGE) break;
116
+ offset += page.length;
117
+ }
118
+ return { scope: { bank, dataset: datasetName, dataset_id: datasetId }, records };
119
+ }
@@ -0,0 +1,48 @@
1
+ import { listBanks, listMemories } from "../../notebooklm-mcp.mjs";
2
+
3
+ const PAGE = 500;
4
+
5
+ /**
6
+ * @param {{ hindsightUrl?: string }} ctx
7
+ * @param {{ bank?: string }} args
8
+ * @param {typeof fetch} fetchFn
9
+ */
10
+ export async function exportHindsight(ctx, args, fetchFn) {
11
+ const base = ctx.hindsightUrl;
12
+ if (!base) throw new Error("Hindsight API URL not configured");
13
+ const bankFilter = String(args.bank || "").trim();
14
+ const bankRows = await listBanks(base, fetchFn);
15
+ const bankIds = bankRows
16
+ .map((b) => b.bank_id || b.id || b.name || "")
17
+ .filter(Boolean);
18
+ const banks = bankFilter ? bankIds.filter((id) => id === bankFilter) : bankIds;
19
+ if (bankFilter && !banks.length) {
20
+ throw new Error(`bank not found: ${bankFilter}`);
21
+ }
22
+ /** @type {object[]} */
23
+ const records = [];
24
+ for (const bank of banks) {
25
+ let offset = 0;
26
+ while (true) {
27
+ const page = await listMemories(base, bank, "", { limit: PAGE, offset }, fetchFn);
28
+ const items = Array.isArray(page?.items) ? page.items : [];
29
+ for (const m of items) {
30
+ const content = String(m.text ?? m.content ?? "").trim();
31
+ if (!content) continue;
32
+ records.push({
33
+ id: m.id ?? m.memory_id ?? null,
34
+ content,
35
+ metadata: { type: m.type ?? null, ...(m.metadata && typeof m.metadata === "object" ? m.metadata : {}) },
36
+ scope_key: bank,
37
+ });
38
+ }
39
+ const total = Number.isFinite(page?.total) ? page.total : items.length;
40
+ offset += items.length;
41
+ if (!items.length || items.length < PAGE || offset >= total) break;
42
+ }
43
+ }
44
+ return {
45
+ scope: bankFilter ? { bank: bankFilter } : { all_banks: true, bank_count: banks.length },
46
+ records,
47
+ };
48
+ }
@@ -0,0 +1,26 @@
1
+ /** Provider PMM export adapters (orchestrator: cli/mcp/portability.mjs). */
2
+
3
+ import { exportCognee } from "./cognee.mjs";
4
+ import { exportHindsight } from "./hindsight.mjs";
5
+ import { exportMem0 } from "./mem0.mjs";
6
+
7
+ export { exportHindsight } from "./hindsight.mjs";
8
+ export { exportMem0 } from "./mem0.mjs";
9
+ export { exportCognee } from "./cognee.mjs";
10
+ export { cogneeDatasetName } from "./cognee-dataset-name.mjs";
11
+
12
+ /**
13
+ * @param {string} provider
14
+ * @param {Record<string, unknown>} ctx
15
+ * @param {Record<string, unknown>} args
16
+ * @param {typeof fetch} fetchFn
17
+ */
18
+ export async function runProviderExport(provider, ctx, args, fetchFn) {
19
+ if (provider === "mem0") {
20
+ const scopeBank = String(args.bank || "openmem-mcp").trim() || "openmem-mcp";
21
+ return exportMem0(ctx, { user_id: scopeBank }, fetchFn);
22
+ }
23
+ if (provider === "hindsight") return exportHindsight(ctx, args, fetchFn);
24
+ if (provider === "cognee") return exportCognee(ctx, args, fetchFn);
25
+ throw new Error(`export not supported for provider ${provider}`);
26
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @param {{ mem0?: { baseUrl: string, apiKey?: string } }} ctx
3
+ * @param {{ user_id: string }} scope
4
+ * @param {typeof fetch} fetchFn
5
+ */
6
+ export async function exportMem0(ctx, scope, fetchFn) {
7
+ const base = ctx.mem0?.baseUrl;
8
+ const key = ctx.mem0?.apiKey;
9
+ if (!base) throw new Error("mem0 API URL not configured");
10
+ /** @type {Record<string, string>} */
11
+ const headers = {};
12
+ if (key) headers["X-API-Key"] = key;
13
+ const q = new URLSearchParams({ user_id: scope.user_id, top_k: "1000" });
14
+ const res = await fetchFn(`${base}/memories?${q}`, { headers });
15
+ const text = await res.text();
16
+ let json;
17
+ try {
18
+ json = text ? JSON.parse(text) : {};
19
+ } catch {
20
+ throw new Error(`GET /memories → ${res.status}: ${text.slice(0, 200)}`);
21
+ }
22
+ if (!res.ok) throw new Error(`GET /memories → ${res.status}: ${text.slice(0, 400)}`);
23
+ const raw = Array.isArray(json) ? json : json.results || json.memories || [];
24
+ const records = raw.map((row) => ({
25
+ id: row.id ?? row.memory_id ?? null,
26
+ content: row.memory ?? row.text ?? row.content ?? JSON.stringify(row),
27
+ metadata: row.metadata ?? {},
28
+ scope_key: scope.user_id,
29
+ }));
30
+ return { scope, records };
31
+ }
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Provider-neutral knowledge tools: pages, initiatives, ingest status.
3
+ * Hindsight is the reference implementation (knowledge-base + operations API); other providers
4
+ * answer "unsupported" from PROVIDER_CAPABILITIES so prompts keep working across `switch`.
5
+ */
6
+
7
+ import { providerSupports } from "../coding-agent/capabilities.mjs";
8
+
9
+ const DEFAULT_BANK = "openmem-mcp";
10
+ const PAGE_MAX_TOKENS = 4096;
11
+ const PAGE_SEARCH_LIMIT = 5;
12
+ const INITIATIVES_FOLDER = "Initiatives";
13
+ const INITIATIVE_TAG = "knowledge:feature-work";
14
+ const ACTIVE_STATUSES = ["pending", "processing"];
15
+ const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled", "error"]);
16
+ const CODING_AGENT_PREFIX = "coding-agent::";
17
+
18
+ const BANK_PROP = {
19
+ type: "string",
20
+ description: "Hindsight bank (use memory_project_scope for the project bank; default openmem-mcp)",
21
+ };
22
+
23
+ export const KNOWLEDGE_MCP_TOOLS = [
24
+ {
25
+ name: "memory_pages_list",
26
+ title: "List knowledge pages",
27
+ description:
28
+ "List a bank's knowledge pages (ids, names, folders; no bodies). Hindsight only.",
29
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
30
+ inputSchema: { type: "object", properties: { bank: BANK_PROP }, additionalProperties: false },
31
+ },
32
+ {
33
+ name: "memory_pages_search",
34
+ title: "Search knowledge pages",
35
+ description:
36
+ "Search knowledge pages: past decisions and conventions. Check them before fixing a bug, changing a test or building something; " +
37
+ "verify claims against the code. Hindsight only.",
38
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ query: { type: "string", description: "What you need to know" },
43
+ bank: BANK_PROP,
44
+ limit: { type: "number", description: "Max page hits (default 5)" },
45
+ },
46
+ required: ["query"],
47
+ additionalProperties: false,
48
+ },
49
+ },
50
+ {
51
+ name: "memory_pages_read",
52
+ title: "Read a knowledge page",
53
+ description:
54
+ "Read one knowledge page by id (from a page listing, search, or [[page:<id>]] link). Hindsight only.",
55
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ page_id: { type: "string", description: "Knowledge page id (kp-…)" },
60
+ bank: BANK_PROP,
61
+ },
62
+ required: ["page_id"],
63
+ additionalProperties: false,
64
+ },
65
+ },
66
+ {
67
+ name: "memory_capture_initiative",
68
+ title: "Capture an initiative",
69
+ description:
70
+ "Record an approved feature plan as a knowledge page before writing code; relates_to_page_id records a plan change. " +
71
+ "Skip bug fixes and small tweaks. Hindsight only.",
72
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ title: { type: "string", description: "Short, specific initiative name (reuse it on updates)" },
77
+ summary: { type: "string", description: "Two or three sentences: what is being built and why, current intent" },
78
+ relates_to_page_id: { type: "string", description: "Existing initiative page id when recording a change" },
79
+ bank: BANK_PROP,
80
+ },
81
+ required: ["title", "summary"],
82
+ additionalProperties: false,
83
+ },
84
+ },
85
+ {
86
+ name: "memory_status",
87
+ title: "Ingest status",
88
+ description:
89
+ "Report whether async ingest has drained (ready, active and failed counts, memory_add operation ids). " +
90
+ "Check before recalling just-stored content. Hindsight only; mem0 is always ready.",
91
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
92
+ inputSchema: {
93
+ type: "object",
94
+ properties: {
95
+ bank: BANK_PROP,
96
+ operation_ids: { type: "array", items: { type: "string" }, description: "Operation ids to check" },
97
+ },
98
+ additionalProperties: false,
99
+ },
100
+ },
101
+ ];
102
+
103
+ export const KNOWLEDGE_TOOL_NAMES = new Set(KNOWLEDGE_MCP_TOOLS.map((t) => t.name));
104
+
105
+ const TOOL_FEATURE = {
106
+ memory_pages_list: "knowledge_pages",
107
+ memory_pages_search: "knowledge_pages",
108
+ memory_pages_read: "knowledge_pages",
109
+ memory_capture_initiative: "initiatives",
110
+ memory_status: "ingest_status",
111
+ };
112
+
113
+ /** Scope clause appended to a page's source query (Hindsight coding-agent wording). */
114
+ export function pageScopeRule(project) {
115
+ return (
116
+ ` Scope this page to ${project} ITSELF: the bank also holds facts about external tools, libraries and services that ${project} ` +
117
+ `merely uses, configures, deploys or discusses, and those belong to somebody else's codebase. Include something only when its ` +
118
+ `subject is ${project}'s own code, configuration or process; when it is about a dependency, leave it out however well-evidenced it ` +
119
+ `looks — including any commit SHA or identifier that belongs to that dependency's repository rather than this one.`
120
+ );
121
+ }
122
+
123
+ /** Hourly refresh at a minute derived from bank + page name, so pages do not all refresh at once. */
124
+ export function initiativeTrigger(bank, name) {
125
+ let h = 0;
126
+ for (const ch of `${bank}\0${name}`) h = (h * 31 + ch.codePointAt(0)) >>> 0;
127
+ return { fact_types: ["world", "experience", "observation"], tags_match: "all", refresh_cron: `${h % 60} * * * *` };
128
+ }
129
+
130
+ export function slugify(s) {
131
+ return (
132
+ String(s)
133
+ .toLowerCase()
134
+ .replace(/[^a-z0-9]+/g, "-")
135
+ .replace(/^-+|-+$/g, "")
136
+ .slice(0, 60) || "initiative"
137
+ );
138
+ }
139
+
140
+ function projectOf(bank) {
141
+ return bank.startsWith(CODING_AGENT_PREFIX) ? bank.slice(CODING_AGENT_PREFIX.length) : bank;
142
+ }
143
+
144
+ function unsupported(provider, name) {
145
+ return { provider, supported: false, tool: name, reason: `${provider} has no ${TOOL_FEATURE[name]} support in OpenMem` };
146
+ }
147
+
148
+ /**
149
+ * @param {string} url
150
+ * @param {RequestInit} init
151
+ * @param {typeof fetch} fetchFn
152
+ * @param {number[]} [allow] non-2xx statuses returned as { status } instead of thrown
153
+ */
154
+ async function hsJson(url, init, fetchFn, allow = []) {
155
+ const res = await fetchFn(url, init);
156
+ const text = await res.text();
157
+ let json;
158
+ try {
159
+ json = text ? JSON.parse(text) : null;
160
+ } catch {
161
+ json = text;
162
+ }
163
+ if (!res.ok) {
164
+ if (allow.includes(res.status)) return { status: res.status, json };
165
+ throw new Error(`${init.method || "GET"} ${url} → ${res.status}: ${String(typeof json === "string" ? json : JSON.stringify(json)).slice(0, 400)}`);
166
+ }
167
+ return { status: res.status ?? 200, json };
168
+ }
169
+
170
+ function flattenPages(nodes, folder, out = []) {
171
+ for (const n of nodes || []) {
172
+ if (!n?.id || !n?.name) continue;
173
+ if (n.kind === "page") {
174
+ out.push({ id: n.id, name: n.name, ...(n.description ? { description: n.description } : {}), ...(folder ? { folder } : {}) });
175
+ }
176
+ if (n.children?.length) flattenPages(n.children, n.kind === "folder" ? n.name : folder, out);
177
+ }
178
+ return out;
179
+ }
180
+
181
+ async function ensureFolder(bankUrl, name, fetchFn) {
182
+ const tree = await hsJson(`${bankUrl}/knowledge-base/tree`, { method: "GET" }, fetchFn);
183
+ const hit = (tree.json?.roots || []).find((n) => n.kind === "folder" && String(n.name || "").toLowerCase() === name.toLowerCase());
184
+ if (hit?.id) return hit.id;
185
+ const created = await hsJson(
186
+ `${bankUrl}/knowledge-base/folders`,
187
+ { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name }) },
188
+ fetchFn,
189
+ );
190
+ return created.json?.id;
191
+ }
192
+
193
+ async function countOps(bankUrl, status, fetchFn) {
194
+ const r = await hsJson(`${bankUrl}/operations?status=${status}&limit=1`, { method: "GET" }, fetchFn);
195
+ return Number(r.json?.total ?? r.json?.operations?.length ?? 0);
196
+ }
197
+
198
+ /**
199
+ * @param {ReturnType<import("./context.mjs").resolveMcpContext>} ctx
200
+ * @param {string} name
201
+ * @param {Record<string, unknown>} args
202
+ * @param {typeof fetch} fetchFn
203
+ */
204
+ export async function callKnowledgeTool(ctx, name, args, fetchFn) {
205
+ if (ctx.provider === "mem0" && name === "memory_status") {
206
+ return { provider: "mem0", supported: true, ready: true, note: "mem0 ingest is synchronous (infer: false)" };
207
+ }
208
+ if (!providerSupports(ctx.provider, TOOL_FEATURE[name])) return unsupported(ctx.provider, name);
209
+ if (!ctx.hindsightUrl) throw new Error("Hindsight API URL not configured");
210
+
211
+ const bank = String(args.bank || DEFAULT_BANK).trim() || DEFAULT_BANK;
212
+ const bankUrl = `${ctx.hindsightUrl}/v1/default/banks/${encodeURIComponent(bank)}`;
213
+
214
+ if (name === "memory_pages_list") {
215
+ const r = await hsJson(`${bankUrl}/knowledge-base/tree`, { method: "GET" }, fetchFn, [404]);
216
+ return { provider: "hindsight", bank, pages: r.status === 404 ? [] : flattenPages(r.json?.roots) };
217
+ }
218
+ if (name === "memory_pages_search") {
219
+ const query = String(args.query || "").trim();
220
+ if (!query) throw new Error("query is required");
221
+ const limit = Number(args.limit) > 0 ? Number(args.limit) : PAGE_SEARCH_LIMIT;
222
+ const r = await hsJson(
223
+ `${bankUrl}/knowledge-base/search?q=${encodeURIComponent(query)}&limit=${limit}`,
224
+ { method: "GET" },
225
+ fetchFn,
226
+ [404],
227
+ );
228
+ const hits = r.status === 404 ? [] : (r.json?.results || []).map((x) => ({
229
+ id: x.id,
230
+ name: x.name,
231
+ snippet: x.snippet ?? "",
232
+ score: x.score ?? 0,
233
+ }));
234
+ return { provider: "hindsight", bank, query, hits };
235
+ }
236
+ if (name === "memory_pages_read") {
237
+ const pageId = String(args.page_id || "").trim();
238
+ if (!pageId) throw new Error("page_id is required");
239
+ const r = await hsJson(`${bankUrl}/knowledge-base/pages/${encodeURIComponent(pageId)}`, { method: "GET" }, fetchFn, [404]);
240
+ if (r.status === 404) throw new Error(`knowledge page not found: ${pageId}`);
241
+ const p = r.json || {};
242
+ const body = typeof p.body === "string" && p.body.trim() ? p.body : p.markdown;
243
+ return {
244
+ provider: "hindsight",
245
+ bank,
246
+ page: { id: p.id, name: p.name, ...(p.tags?.length ? { tags: p.tags } : {}), ...(p.timestamp ? { last_updated_at: p.timestamp } : {}), body },
247
+ };
248
+ }
249
+ if (name === "memory_capture_initiative") {
250
+ const title = String(args.title || "").trim();
251
+ const summary = String(args.summary || "").trim();
252
+ if (!title || !summary) throw new Error("title and summary are required");
253
+ const relatesTo = String(args.relates_to_page_id || "").trim();
254
+ let pageId = relatesTo;
255
+ if (!pageId) {
256
+ const folderId = await ensureFolder(bankUrl, INITIATIVES_FOLDER, fetchFn);
257
+ const r = await hsJson(
258
+ `${bankUrl}/knowledge-base/pages`,
259
+ {
260
+ method: "POST",
261
+ headers: { "content-type": "application/json" },
262
+ body: JSON.stringify({
263
+ name: title,
264
+ source_query:
265
+ `Summarize the "${title}" initiative: what is being built or changed and why, and its current state — drawn from the project's memory.` +
266
+ pageScopeRule(projectOf(bank)),
267
+ ...(folderId ? { parent_id: folderId } : {}),
268
+ tags: [INITIATIVE_TAG],
269
+ max_tokens: PAGE_MAX_TOKENS,
270
+ trigger: initiativeTrigger(bank, title),
271
+ }),
272
+ },
273
+ fetchFn,
274
+ );
275
+ pageId = String(r.json?.page_id || r.json?.id || "").trim();
276
+ if (!pageId) {
277
+ throw new Error(`Hindsight created no page id for initiative "${title}"; no marker retained. Check memory_pages_list before retrying.`);
278
+ }
279
+ }
280
+ const verb = relatesTo ? "Update to an existing initiative" : "New initiative";
281
+ const retained = await hsJson(
282
+ `${bankUrl}/memories`,
283
+ {
284
+ method: "POST",
285
+ headers: { "content-type": "application/json" },
286
+ body: JSON.stringify({
287
+ async: true,
288
+ items: [
289
+ {
290
+ content: `${verb}: ${title}. ${summary}`,
291
+ context: `initiative marker for [[page:${pageId}]]`,
292
+ document_id: `initiative-marker-${slugify(pageId)}${relatesTo ? "-update" : ""}`,
293
+ tags: [INITIATIVE_TAG],
294
+ metadata: { relatedPageId: pageId },
295
+ },
296
+ ],
297
+ }),
298
+ },
299
+ fetchFn,
300
+ );
301
+ return { provider: "hindsight", bank, page_id: pageId, updated: Boolean(relatesTo), operation_id: retained.json?.operation_id ?? null };
302
+ }
303
+ if (name === "memory_status") {
304
+ const [pending, processing, failed] = await Promise.all(
305
+ [...ACTIVE_STATUSES, "failed"].map((s) => countOps(bankUrl, s, fetchFn)),
306
+ );
307
+ const ids = Array.isArray(args.operation_ids) ? args.operation_ids.map(String).filter(Boolean) : [];
308
+ const operations = {};
309
+ for (const id of ids) {
310
+ const r = await hsJson(`${bankUrl}/operations/${encodeURIComponent(id)}`, { method: "GET" }, fetchFn, [404]);
311
+ operations[id] = r.status === 404 ? "not_found" : String(r.json?.status || "unknown").toLowerCase();
312
+ }
313
+ const statuses = Object.values(operations);
314
+ return {
315
+ provider: "hindsight",
316
+ bank,
317
+ supported: true,
318
+ active: pending + processing,
319
+ pending,
320
+ processing,
321
+ failed,
322
+ ...(ids.length
323
+ ? {
324
+ operations,
325
+ // ingested: every requested op completed; a failed or unknown op is never counted as stored.
326
+ ingested: statuses.every((s) => s === "completed"),
327
+ }
328
+ : {}),
329
+ ready: ids.length ? statuses.every((s) => TERMINAL_STATUSES.has(s)) : pending + processing === 0,
330
+ };
331
+ }
332
+ throw new Error(`unknown tool: ${name}`);
333
+ }
@@ -0,0 +1,50 @@
1
+ import path from "node:path";
2
+ import { envValue } from "../../wizard/lib/paths.mjs";
3
+
4
+ /** OpenAI-compatible chat URL for cross-provider reflect synthesis (optional). */
5
+ export function resolveLlmChatUrl({ home, stackRoot, env = process.env }) {
6
+ const direct = String(env.PM_LLM_URL || env.OPENAI_API_BASE || "").trim();
7
+ if (direct) return direct.replace(/\/+$/, "").replace(/\/v1$/, "") + (direct.includes("/v1") ? "" : "/v1");
8
+ const fromEnv = envValue(path.join(stackRoot, ".env"), "LITELLM_URL");
9
+ if (fromEnv) return fromEnv.replace(/\/+$/, "");
10
+ const port = envValue(path.join(stackRoot, ".env"), "LITELLM_PORT") || "4000";
11
+ return `http://127.0.0.1:${String(port).trim()}/v1`;
12
+ }
13
+
14
+ export function resolveLlmModel(env = process.env) {
15
+ return String(env.PM_LLM_MODEL || env.PM_REFLECT_MODEL || "memory-reflect").trim() || "memory-reflect";
16
+ }
17
+
18
+ export function resolveLlmApiKey(env = process.env) {
19
+ return String(env.PM_LLM_API_KEY || env.LITELLM_MASTER_KEY || env.OPENAI_API_KEY || "").trim();
20
+ }
21
+
22
+ /**
23
+ * @param {{ query: string, context: string, model?: string, baseUrl: string, apiKey?: string }} opts
24
+ * @param {typeof fetch} fetchFn
25
+ */
26
+ export async function synthesizeReflectAnswer(opts, fetchFn = fetch) {
27
+ const base = opts.baseUrl.replace(/\/+$/, "");
28
+ const url = `${base}/chat/completions`;
29
+ const headers = { "content-type": "application/json" };
30
+ if (opts.apiKey) headers.authorization = `Bearer ${opts.apiKey}`;
31
+ const body = {
32
+ model: opts.model || resolveLlmModel(),
33
+ messages: [
34
+ {
35
+ role: "system",
36
+ content:
37
+ "Summarize the following memory search hits as concise session context for a coding agent. " +
38
+ "Do not invent facts not present in the hits.",
39
+ },
40
+ { role: "user", content: `Question: ${opts.query}\n\nMemory hits:\n${opts.context}` },
41
+ ],
42
+ max_tokens: 1024,
43
+ temperature: 0.2,
44
+ };
45
+ const res = await fetchFn(url, { method: "POST", headers, body: JSON.stringify(body) });
46
+ const text = await res.text();
47
+ if (!res.ok) throw new Error(`LLM synthesize → ${res.status}: ${text.slice(0, 300)}`);
48
+ const json = JSON.parse(text);
49
+ return json?.choices?.[0]?.message?.content?.trim() || text;
50
+ }