@bcmyguest/openmem 0.6.3 → 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.
- package/{skills → .agents/skills}/openmem/SKILL.md +5 -0
- package/README.md +80 -76
- package/cli/coding-agent/capabilities.mjs +14 -3
- package/cli/coding-agent/context-store.mjs +81 -0
- package/cli/coding-agent/context-trim.mjs +897 -0
- package/cli/coding-agent/file-io.mjs +72 -0
- package/cli/coding-agent/hook-harnesses.mjs +2 -1
- package/cli/coding-agent/hook-runner.mjs +28 -4
- package/cli/coding-agent/install-access.mjs +43 -17
- package/cli/coding-agent/install-context-proxy.mjs +333 -0
- package/cli/coding-agent/install-rtk.mjs +60 -0
- package/cli/coding-agent/install-shim.mjs +119 -15
- package/cli/coding-agent/opencode-config.mjs +21 -5
- package/cli/coding-agent/opencode-plugin.mjs +39 -3
- package/cli/coding-agent/opencode2-plugin/index.js +299 -0
- package/cli/coding-agent/recall-frame.mjs +29 -0
- package/cli/coding-agent/remove-upstream-access.mjs +122 -0
- package/cli/coding-agent/runtime.mjs +2 -1
- package/cli/coding-agent/session-db.mjs +626 -0
- package/cli/coding-agent/session-facade.mjs +30 -6
- package/cli/coding-agent/toml-edit.mjs +161 -0
- package/cli/context-management.mjs +254 -0
- package/cli/feed-sync.mjs +18 -2
- package/cli/install-coding-agent.mjs +173 -40
- package/cli/mcp/context-sql-runner.mjs +58 -0
- package/cli/mcp/context-store.mjs +670 -0
- package/cli/mcp/knowledge.mjs +333 -0
- package/cli/mcp/memory-facade.mjs +99 -33
- package/cli/openmem-mcp.mjs +3 -0
- package/cli/openmem.mjs +18 -1
- package/cli/schedule.mjs +176 -0
- package/cli/stack.mjs +2 -0
- package/cli/switch-memory-provider.mjs +1 -1
- package/compose.yaml +67 -4
- package/config/hindsight.env.example +2 -0
- package/config/openmem.toml.example +64 -2
- package/docs/architecture.md +35 -9
- package/docs/coding-agent-hooks.md +26 -2
- package/docs/coding-agents.md +27 -7
- package/docs/compatibility.md +80 -0
- package/docs/concepts.md +30 -7
- package/docs/context-management.md +255 -0
- package/docs/context-proxy.md +115 -0
- package/docs/context-research.md +215 -0
- package/docs/context-store.md +277 -0
- package/docs/hindsight.md +5 -2
- package/docs/mcp-setup.md +5 -2
- package/docs/mem0.md +39 -1
- package/docs/memory-providers.md +2 -2
- package/docs/meridian-litellm.md +198 -0
- package/docs/model-configuration.md +22 -0
- package/docs/npx-install.md +6 -4
- package/docs/openmem-config.md +2 -2
- package/docs/quickstart.md +12 -6
- package/docs/setup-wizard.md +5 -4
- package/docs/sources-feeds.md +9 -22
- package/package.json +5 -4
- package/providers/cognee/OPERATOR.md +2 -2
- package/providers/hindsight/lib/clack.mjs +122 -20
- package/providers/hindsight/lib/coding-agent.mjs +1 -1
- package/providers/hindsight/lib/config.mjs +6 -0
- package/providers/hindsight/lib/env-setup.mjs +2 -1
- package/providers/hindsight/lib/litellm-routes.mjs +21 -12
- package/providers/hindsight/lib/meridian.mjs +307 -0
- package/providers/hindsight/lib/model-backend.mjs +38 -9
- package/providers/hindsight/lib/probe.mjs +21 -0
- package/providers/hindsight/lib/stack-env.mjs +5 -12
- package/providers/hindsight/lib/state.mjs +6 -1
- package/providers/hindsight/lib/summary.mjs +16 -2
- package/providers/hindsight/lib/validate.mjs +6 -0
- package/providers/hindsight/wizard.mjs +3 -2
- package/providers/mem0/OPERATOR.md +21 -2
- package/providers/mem0/compose.yaml +24 -2
- package/providers/mem0/lib/coding-agent.mjs +1 -1
- package/providers/mem0/lib/configure.mjs +66 -0
- package/providers/mem0/lib/litellm.mjs +457 -0
- package/providers/mem0/lib/stack-env.mjs +40 -1
- package/providers/mem0/lib/summary.mjs +30 -2
- package/providers/mem0/manifest.json +1 -1
- package/providers/mem0/wizard.mjs +15 -2
- package/proxy/config.yaml +33 -0
- package/proxy/openmem_context/__init__.py +6 -0
- package/proxy/openmem_context/adapters.py +504 -0
- package/proxy/openmem_context/compress.py +53 -0
- package/proxy/openmem_context/core.py +606 -0
- package/proxy/openmem_context/embed.py +212 -0
- package/proxy/openmem_context/recall.py +49 -0
- package/proxy/openmem_context/session.py +70 -0
- package/proxy/openmem_context/sessiondb.py +276 -0
- package/proxy/openmem_context/settings.py +134 -0
- package/proxy/openmem_context/store.py +129 -0
- package/proxy/openmem_context/tokens.py +123 -0
- package/proxy/openmem_context/ttl.py +78 -0
- package/proxy/openmem_context_hook.py +34 -0
- package/scripts/check-pack.mjs +2 -1
- package/scripts/setup.mjs +14 -1
- package/server.json +2 -2
- package/wizard/lib/context-step.mjs +140 -0
- package/wizard/lib/litellm-env.mjs +55 -0
- package/wizard/lib/pm-config.mjs +212 -4
- package/wizard/lib/prompts.mjs +28 -0
- 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
|
-
|
|
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: {
|
|
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
|
|
28
|
-
"
|
|
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: "
|
|
33
|
-
bank: {
|
|
34
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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: {
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
109
|
-
merge_strategy: {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: {
|
|
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.
|
|
500
|
+
serverInfo: { name: "openmem", version: "0.6.4" },
|
|
435
501
|
},
|
|
436
502
|
};
|
|
437
503
|
}
|
package/cli/openmem-mcp.mjs
CHANGED
|
@@ -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",
|