@bcmyguest/openmem 0.6.3 → 0.9.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 (104) hide show
  1. package/{skills → .agents/skills}/openmem/SKILL.md +5 -0
  2. package/README.md +81 -76
  3. package/cli/coding-agent/capabilities.mjs +14 -3
  4. package/cli/coding-agent/context-store.mjs +81 -0
  5. package/cli/coding-agent/context-trim.mjs +899 -0
  6. package/cli/coding-agent/file-io.mjs +72 -0
  7. package/cli/coding-agent/hook-harnesses.mjs +2 -1
  8. package/cli/coding-agent/hook-runner.mjs +28 -4
  9. package/cli/coding-agent/install-access.mjs +43 -17
  10. package/cli/coding-agent/install-context-proxy.mjs +333 -0
  11. package/cli/coding-agent/install-rtk.mjs +60 -0
  12. package/cli/coding-agent/install-shim.mjs +119 -15
  13. package/cli/coding-agent/opencode-config.mjs +21 -5
  14. package/cli/coding-agent/opencode-plugin.mjs +39 -3
  15. package/cli/coding-agent/opencode2-plugin/index.js +299 -0
  16. package/cli/coding-agent/recall-frame.mjs +29 -0
  17. package/cli/coding-agent/remove-upstream-access.mjs +122 -0
  18. package/cli/coding-agent/runtime.mjs +2 -1
  19. package/cli/coding-agent/session-db.mjs +626 -0
  20. package/cli/coding-agent/session-facade.mjs +30 -6
  21. package/cli/coding-agent/toml-edit.mjs +161 -0
  22. package/cli/context-management.mjs +254 -0
  23. package/cli/feed-sync.mjs +18 -2
  24. package/cli/install-coding-agent.mjs +173 -40
  25. package/cli/mcp/context-sql-runner.mjs +58 -0
  26. package/cli/mcp/context-store.mjs +670 -0
  27. package/cli/mcp/knowledge.mjs +333 -0
  28. package/cli/mcp/memory-facade.mjs +99 -33
  29. package/cli/openmem-mcp.mjs +3 -0
  30. package/cli/openmem.mjs +18 -1
  31. package/cli/schedule.mjs +176 -0
  32. package/cli/stack.mjs +2 -0
  33. package/cli/switch-memory-provider.mjs +1 -1
  34. package/compose.yaml +67 -4
  35. package/config/hindsight.env.example +2 -0
  36. package/config/openmem.toml.example +64 -2
  37. package/docs/architecture.md +35 -9
  38. package/docs/coding-agent-hooks.md +26 -2
  39. package/docs/coding-agents.md +27 -7
  40. package/docs/compatibility.md +80 -0
  41. package/docs/concepts.md +30 -7
  42. package/docs/context-management.md +230 -0
  43. package/docs/context-proxy.md +115 -0
  44. package/docs/context-research.md +226 -0
  45. package/docs/context-store.md +277 -0
  46. package/docs/evidence-plan.md +216 -0
  47. package/docs/hindsight.md +5 -2
  48. package/docs/mcp-setup.md +5 -2
  49. package/docs/mem0.md +39 -1
  50. package/docs/memory-providers.md +2 -2
  51. package/docs/meridian-litellm.md +198 -0
  52. package/docs/model-configuration.md +22 -0
  53. package/docs/npx-install.md +6 -4
  54. package/docs/openmem-config.md +2 -2
  55. package/docs/quickstart.md +12 -6
  56. package/docs/setup-wizard.md +5 -4
  57. package/docs/sources-feeds.md +9 -22
  58. package/package.json +5 -4
  59. package/providers/cognee/OPERATOR.md +2 -2
  60. package/providers/hindsight/lib/clack.mjs +122 -20
  61. package/providers/hindsight/lib/coding-agent.mjs +1 -1
  62. package/providers/hindsight/lib/config.mjs +6 -0
  63. package/providers/hindsight/lib/env-setup.mjs +2 -1
  64. package/providers/hindsight/lib/litellm-routes.mjs +21 -12
  65. package/providers/hindsight/lib/meridian.mjs +307 -0
  66. package/providers/hindsight/lib/model-backend.mjs +38 -9
  67. package/providers/hindsight/lib/probe.mjs +21 -0
  68. package/providers/hindsight/lib/stack-env.mjs +5 -12
  69. package/providers/hindsight/lib/state.mjs +6 -1
  70. package/providers/hindsight/lib/summary.mjs +16 -2
  71. package/providers/hindsight/lib/validate.mjs +6 -0
  72. package/providers/hindsight/wizard.mjs +3 -2
  73. package/providers/mem0/OPERATOR.md +21 -2
  74. package/providers/mem0/compose.yaml +24 -2
  75. package/providers/mem0/lib/coding-agent.mjs +1 -1
  76. package/providers/mem0/lib/configure.mjs +66 -0
  77. package/providers/mem0/lib/litellm.mjs +457 -0
  78. package/providers/mem0/lib/stack-env.mjs +40 -1
  79. package/providers/mem0/lib/summary.mjs +30 -2
  80. package/providers/mem0/manifest.json +1 -1
  81. package/providers/mem0/wizard.mjs +15 -2
  82. package/proxy/config.yaml +33 -0
  83. package/proxy/openmem_context/__init__.py +6 -0
  84. package/proxy/openmem_context/adapters.py +504 -0
  85. package/proxy/openmem_context/compress.py +53 -0
  86. package/proxy/openmem_context/core.py +608 -0
  87. package/proxy/openmem_context/embed.py +212 -0
  88. package/proxy/openmem_context/recall.py +49 -0
  89. package/proxy/openmem_context/session.py +70 -0
  90. package/proxy/openmem_context/sessiondb.py +276 -0
  91. package/proxy/openmem_context/settings.py +134 -0
  92. package/proxy/openmem_context/store.py +129 -0
  93. package/proxy/openmem_context/tokens.py +123 -0
  94. package/proxy/openmem_context/ttl.py +78 -0
  95. package/proxy/openmem_context_hook.py +34 -0
  96. package/scripts/check-pack.mjs +2 -1
  97. package/scripts/run-setup.sh +14 -5
  98. package/scripts/setup.mjs +14 -1
  99. package/server.json +2 -2
  100. package/wizard/lib/context-step.mjs +140 -0
  101. package/wizard/lib/litellm-env.mjs +55 -0
  102. package/wizard/lib/pm-config.mjs +212 -4
  103. package/wizard/lib/prompts.mjs +28 -0
  104. package/wizard/lib/run-wizard.mjs +26 -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
+ }
@@ -1,6 +1,8 @@
1
1
  import { search as cogneeSearch } from "../cognee-mcp.mjs";
2
2
  import { callMemoryConsolidate, callMemoryReflect } from "./memory-reflect-consolidate.mjs";
3
3
  import { memoryExport, memoryImport } from "./portability.mjs";
4
+ import { KNOWLEDGE_MCP_TOOLS, KNOWLEDGE_TOOL_NAMES, callKnowledgeTool } from "./knowledge.mjs";
5
+ import { CONTEXT_STORE_MCP_TOOLS, CONTEXT_STORE_TOOL_NAMES, callContextStoreTool } from "./context-store.mjs";
4
6
  import fs from "node:fs";
5
7
  import os from "node:os";
6
8
  import path from "node:path";
@@ -14,28 +16,42 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../
14
16
  export const UNIFIED_MCP_TOOLS = [
15
17
  {
16
18
  name: "memory_project_scope",
17
- description: "Read the local project bank mapping and session access policy for a workspace. Makes no memory-provider calls.",
19
+ title: "Resolve project bank",
20
+ description:
21
+ "Resolve the project bank (user_id / dataset) and access policy for a workspace path. " +
22
+ "Call it instead of guessing a bank name. Local mapping only.",
23
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
18
24
  inputSchema: {
19
25
  type: "object",
20
- properties: { cwd: { type: "string", description: "Absolute workspace path (defaults to the MCP process working directory)" } },
26
+ properties: {
27
+ cwd: {
28
+ type: "string",
29
+ description: "Absolute workspace path (defaults to the MCP process working directory)",
30
+ },
31
+ },
21
32
  additionalProperties: false,
22
33
  },
23
34
  },
24
35
  {
25
36
  name: "memory_add",
37
+ title: "Store a memory",
26
38
  description:
27
- "Store text in the active memory provider (Hindsight bank, mem0 user scope, or Cognee dataset ingest when supported). " +
28
- "On Hindsight, defaults to async ingest so the MCP returns before LLM extraction finishes (avoids client timeouts).",
39
+ "Store durable facts, decisions or session notes in the active provider. " +
40
+ "Pass bank from memory_project_scope for project work. Hindsight ingests async by default.",
41
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
29
42
  inputSchema: {
30
43
  type: "object",
31
44
  properties: {
32
- content: { type: "string", description: "Text to remember" },
33
- bank: { type: "string", description: "Hindsight bank or mem0 user_id scope (default: openmem-mcp)" },
34
- metadata: { type: "object", description: "Optional provider-specific metadata" },
45
+ content: { type: "string", description: "Plain text to store as a durable memory" },
46
+ bank: {
47
+ type: "string",
48
+ description: "Hindsight bank or mem0 user_id / Cognee dataset scope (default: openmem-mcp)",
49
+ },
50
+ metadata: { type: "object", description: "Optional provider-specific metadata object" },
35
51
  async: {
36
52
  type: "boolean",
37
53
  description:
38
- "Hindsight only: queue background ingest (default true). Set false to wait for extraction (slow; may hit MCP timeouts).",
54
+ "Hindsight only: true (default) returns after enqueueing ingest; false waits for extraction (may hit MCP client timeouts).",
39
55
  },
40
56
  },
41
57
  required: ["content"],
@@ -44,13 +60,16 @@ export const UNIFIED_MCP_TOOLS = [
44
60
  },
45
61
  {
46
62
  name: "memory_search",
47
- description: "Semantic or graph search over memories in the active provider.",
63
+ title: "Search memories",
64
+ description:
65
+ "Return ranked raw hits from stored memories. memory_recall suits questions; memory_reflect synthesizes an answer.",
66
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
48
67
  inputSchema: {
49
68
  type: "object",
50
69
  properties: {
51
- query: { type: "string" },
52
- bank: { type: "string", description: "Optional bank / user_id scope" },
53
- limit: { type: "number", description: "Max results where supported" },
70
+ query: { type: "string", description: "Search query text" },
71
+ bank: { type: "string", description: "Optional bank / user_id / dataset scope" },
72
+ limit: { type: "number", description: "Max hits where the provider supports it" },
54
73
  },
55
74
  required: ["query"],
56
75
  additionalProperties: false,
@@ -58,14 +77,16 @@ export const UNIFIED_MCP_TOOLS = [
58
77
  },
59
78
  {
60
79
  name: "memory_recall",
80
+ title: "Recall context for a question",
61
81
  description:
62
- "Retrieve context for a question (Hindsight recall API; mem0/Cognee use search under the hood).",
82
+ "Retrieve stored context for a natural-language question (Hindsight recall; search on mem0 and Cognee).",
83
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
63
84
  inputSchema: {
64
85
  type: "object",
65
86
  properties: {
66
- query: { type: "string" },
67
- bank: { type: "string" },
68
- limit: { type: "number" },
87
+ query: { type: "string", description: "Question or topic to recall context for" },
88
+ bank: { type: "string", description: "Optional bank / user_id / dataset scope" },
89
+ limit: { type: "number", description: "Max items where supported" },
69
90
  },
70
91
  required: ["query"],
71
92
  additionalProperties: false,
@@ -73,27 +94,37 @@ export const UNIFIED_MCP_TOOLS = [
73
94
  },
74
95
  {
75
96
  name: "memory_forget",
76
- description: "Delete a memory by id (mem0) or drop an entire Hindsight bank when bank is set without memory_id.",
97
+ title: "Delete memories",
98
+ description:
99
+ "Delete memories: mem0 needs memory_id; on Hindsight a bank without memory_id drops the whole bank. " +
100
+ "Confirm with the user first. Not available on Cognee.",
101
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
77
102
  inputSchema: {
78
103
  type: "object",
79
104
  properties: {
80
- memory_id: { type: "string", description: "Provider memory id (mem0)" },
81
- bank: { type: "string", description: "Hindsight bank to delete, or mem0 user_id scope for bulk delete" },
105
+ memory_id: { type: "string", description: "Provider memory id (required for mem0 deletes)" },
106
+ bank: {
107
+ type: "string",
108
+ description: "Hindsight bank to drop (when memory_id omitted), or mem0 user_id scope",
109
+ },
82
110
  },
83
111
  additionalProperties: false,
84
112
  },
85
113
  },
86
114
  {
87
115
  name: "memory_export",
116
+ title: "Export memories (PMM)",
88
117
  description:
89
- "Export memories as PMM v1 (json/jsonl). Hindsight lists via /memories/list; mem0 GET /memories; Cognee dataset /data.",
118
+ "Export memories as PMM v1 JSON or JSONL for backup or migration. Pass bank for large stores.",
119
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
90
120
  inputSchema: {
91
121
  type: "object",
92
122
  properties: {
93
- format: { type: "string", enum: ["json", "jsonl"], description: "Serialization (default json)" },
123
+ format: { type: "string", enum: ["json", "jsonl"], description: "Serialization format (default json)" },
94
124
  bank: {
95
125
  type: "string",
96
- description: "Hindsight bank, mem0 user_id, or indexer bank for Cognee dataset mapping. Omit on Hindsight to export all banks.",
126
+ description:
127
+ "Hindsight bank, mem0 user_id, or indexer bank for Cognee. Omit on Hindsight to export all banks.",
97
128
  },
98
129
  },
99
130
  additionalProperties: false,
@@ -101,12 +132,19 @@ export const UNIFIED_MCP_TOOLS = [
101
132
  },
102
133
  {
103
134
  name: "memory_import",
104
- description: "Import PMM v1 payload into the active provider (best-effort cross-provider).",
135
+ title: "Import memories (PMM)",
136
+ description:
137
+ "Import a PMM v1 payload from memory_export. Content and scope carry over; embeddings and graphs do not.",
138
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
105
139
  inputSchema: {
106
140
  type: "object",
107
141
  properties: {
108
- data: { type: "string", description: "Serialized export payload" },
109
- merge_strategy: { type: "string", enum: ["skip_duplicates", "overwrite"] },
142
+ data: { type: "string", description: "Serialized PMM export (json or jsonl text)" },
143
+ merge_strategy: {
144
+ type: "string",
145
+ enum: ["skip_duplicates", "overwrite"],
146
+ description: "How to treat records that already exist (default skip_duplicates)",
147
+ },
110
148
  },
111
149
  required: ["data"],
112
150
  additionalProperties: false,
@@ -114,19 +152,24 @@ export const UNIFIED_MCP_TOOLS = [
114
152
  },
115
153
  {
116
154
  name: "memory_provider_status",
117
- description: "Report active MEMORY_PROVIDER and resolved backend URL (no secrets).",
155
+ title: "Provider status",
156
+ description:
157
+ "Report the active provider id and backend URL.",
158
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
118
159
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
119
160
  },
120
161
  {
121
162
  name: "memory_reflect",
163
+ title: "Reflect / synthesize an answer",
122
164
  description:
123
- "Synthesize session context for a question. Hindsight uses recall; mem0/Cognee search then optional LiteLLM synthesis (PM_LLM_URL / stack LiteLLM).",
165
+ "Synthesize a reasoned answer from stored memories, with evidence. The answer is not stored.",
166
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
124
167
  inputSchema: {
125
168
  type: "object",
126
169
  properties: {
127
- query: { type: "string" },
128
- bank: { type: "string" },
129
- limit: { type: "number" },
170
+ query: { type: "string", description: "Question to answer from memory" },
171
+ bank: { type: "string", description: "Optional bank / user_id / dataset scope" },
172
+ limit: { type: "number", description: "Max evidence items where supported" },
130
173
  },
131
174
  required: ["query"],
132
175
  additionalProperties: false,
@@ -134,13 +177,20 @@ export const UNIFIED_MCP_TOOLS = [
134
177
  },
135
178
  {
136
179
  name: "memory_consolidate",
137
- description: "Provider maintenance: Cognee cognify for a bank/dataset; Hindsight notes LiteLLM route; mem0 skips.",
180
+ title: "Consolidate / cognify",
181
+ description:
182
+ "Rebuild the provider's graph or index after bulk ingest (Cognee cognify; no-op on mem0).",
183
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
138
184
  inputSchema: {
139
185
  type: "object",
140
- properties: { bank: { type: "string" } },
186
+ properties: {
187
+ bank: { type: "string", description: "Bank / dataset to consolidate (required for Cognee cognify)" },
188
+ },
141
189
  additionalProperties: false,
142
190
  },
143
191
  },
192
+ ...KNOWLEDGE_MCP_TOOLS,
193
+ ...CONTEXT_STORE_MCP_TOOLS,
144
194
  ];
145
195
 
146
196
  export function textResult(value, isError = false) {
@@ -254,6 +304,22 @@ export async function callUnifiedTool(ctx, name, args = {}, fetchFn = fetch) {
254
304
  }
255
305
  }
256
306
 
307
+ if (CONTEXT_STORE_TOOL_NAMES.has(name)) {
308
+ try {
309
+ return textResult(await callContextStoreTool(name, args));
310
+ } catch (err) {
311
+ return textResult(err instanceof Error ? err.message : String(err), true);
312
+ }
313
+ }
314
+
315
+ if (KNOWLEDGE_TOOL_NAMES.has(name)) {
316
+ try {
317
+ return textResult(await callKnowledgeTool(ctx, name, args, fetchFn));
318
+ } catch (err) {
319
+ return textResult(err instanceof Error ? err.message : String(err), true);
320
+ }
321
+ }
322
+
257
323
  if (ctx.provider === "hindsight") {
258
324
  return callHindsight(ctx, name, args, fetchFn);
259
325
  }
@@ -431,7 +497,7 @@ export async function handleUnifiedMessage(msg, ctx, fetchFn = fetch) {
431
497
  result: {
432
498
  protocolVersion,
433
499
  capabilities: { tools: {} },
434
- serverInfo: { name: "openmem", version: "0.1.0" },
500
+ serverInfo: { name: "openmem", version: "0.6.4" },
435
501
  },
436
502
  };
437
503
  }
@@ -13,6 +13,7 @@ import path from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { isMainModule } from "./is-main.mjs";
15
15
  import { resolveMcpContext } from "./mcp/context.mjs";
16
+ import { startContextStore } from "./mcp/context-store.mjs";
16
17
  import { handleUnifiedMessage } from "./mcp/memory-facade.mjs";
17
18
  import { serveStdio } from "./mcp/stdio.mjs";
18
19
  import { resolveStackRoot } from "./stack.mjs";
@@ -23,6 +24,8 @@ async function main() {
23
24
  const home = process.env.HOME || os.homedir();
24
25
  const stack = resolveStackRoot({ pkgRoot: PKG_ROOT, env: process.env, home });
25
26
  const ctx = resolveMcpContext({ home, stackRoot: stack.root, repoRoot: PKG_ROOT, env: process.env });
27
+ // Session-file TTL cleanup (docs/context-store.md, "Retention"), at most once a day; never throws.
28
+ await startContextStore(process.env, { home, stackRoot: stack.root });
26
29
  process.stdin.resume();
27
30
  await serveStdio(process.stdin, (msg) => handleUnifiedMessage(msg, ctx));
28
31
  }
package/cli/openmem.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * openmem: npx entry point for the memory-stack setup wizard.
4
4
  *
5
- * openmem [setup|configure|up|down|doctor|providers|init|--help|--version]
5
+ * openmem [setup|configure|up|down|doctor|providers|init|context|--help|--version]
6
6
  *
7
7
  * The wizard code runs from this package (read-only, may be an npx cache). Everything it
8
8
  * writes, and the compose files it drives, live in a stable stack root; see cli/stack.mjs.
@@ -30,8 +30,11 @@ Usage (prefer npx once published: npx ${BIN} <command>):
30
30
  ${BIN} doctor [flags] validate the configuration without the smoke test
31
31
  ${BIN} wiring [--json] report provider, API URLs, and MCP alignment
32
32
  ${BIN} feed-sync fetch RSS/OpenAlex into feeds.out_dir (then indexer watches it)
33
+ ${BIN} schedule <install|remove> [--dry-run] manage hourly feed sync with the OS scheduler
33
34
  ${BIN} install-mcp [flags] merge memory MCP servers into ~/.cursor/mcp.json
34
35
  ${BIN} install-coding-agent [flags] Hindsight plugin or MCP session path (Claude/Codex/OpenCode/Cursor)
36
+ ${BIN} context <on|off|status> [--dry-run]
37
+ context management (smart compaction): config, agent wiring, proxy service
35
38
  ${BIN} switch <provider> [flags]
36
39
  change MEMORY_PROVIDER (stack + MCP; no data migration)
37
40
  ${BIN} providers [--json] list memory providers and their status
@@ -179,6 +182,20 @@ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
179
182
  env: process.env,
180
183
  });
181
184
  process.exit(r.status ?? 1);
185
+ } else if (cmd === "schedule") {
186
+ const r = spawnSync(process.execPath, [path.join(PKG_ROOT, "cli/schedule.mjs"), ...argv.slice(1)], {
187
+ stdio: "inherit",
188
+ env: process.env,
189
+ });
190
+ process.exit(r.status ?? 1);
191
+ } else if (cmd === "context") {
192
+ // on/off run compose from the stack root, so materialize it first.
193
+ const { sr } = ensureStack({ quiet: true });
194
+ const r = spawnSync(process.execPath, [path.join(PKG_ROOT, "cli/context-management.mjs"), ...argv.slice(1)], {
195
+ stdio: "inherit",
196
+ env: { ...process.env, PM_STACK_ROOT: sr.root },
197
+ });
198
+ process.exit(r.status ?? 1);
182
199
  } else if (cmd === "wiring") {
183
200
  const r = spawnSync(process.execPath, [path.join(PKG_ROOT, "cli/wiring-report.mjs"), ...argv.slice(1)], {
184
201
  stdio: "inherit",