@seanhogg/builderforce-memory-mcp 2026.7.12 → 2026.9.27
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/README.md +47 -5
- package/dist/backend.d.ts +32 -0
- package/dist/backend.d.ts.map +1 -1
- package/dist/backends/memory-store.d.ts +32 -9
- package/dist/backends/memory-store.d.ts.map +1 -1
- package/dist/backends/memory-store.js +155 -33
- package/dist/backends/memory-store.js.map +1 -1
- package/dist/bin/http.d.ts +5 -0
- package/dist/bin/http.d.ts.map +1 -1
- package/dist/bin/http.js +11 -0
- package/dist/bin/http.js.map +1 -1
- package/dist/bin/install.js +3 -2
- package/dist/bin/install.js.map +1 -1
- package/dist/bin/stdio.d.ts +17 -7
- package/dist/bin/stdio.d.ts.map +1 -1
- package/dist/bin/stdio.js +24 -8
- package/dist/bin/stdio.js.map +1 -1
- package/dist/compaction.d.ts +76 -0
- package/dist/compaction.d.ts.map +1 -0
- package/dist/compaction.js +77 -0
- package/dist/compaction.js.map +1 -0
- package/dist/embedding/evermind-embedder.d.ts +119 -0
- package/dist/embedding/evermind-embedder.d.ts.map +1 -0
- package/dist/embedding/evermind-embedder.js +201 -0
- package/dist/embedding/evermind-embedder.js.map +1 -0
- package/dist/embedding/index.d.ts +38 -0
- package/dist/embedding/index.d.ts.map +1 -0
- package/dist/embedding/index.js +44 -0
- package/dist/embedding/index.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/install/claude-hooks.d.ts +42 -2
- package/dist/install/claude-hooks.d.ts.map +1 -1
- package/dist/install/claude-hooks.js +111 -16
- package/dist/install/claude-hooks.js.map +1 -1
- package/dist/install/server-spec.d.ts +12 -0
- package/dist/install/server-spec.d.ts.map +1 -1
- package/dist/install/server-spec.js +18 -0
- package/dist/install/server-spec.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +117 -6
- package/dist/tools.js.map +1 -1
- package/package.json +15 -4
- package/server.json +47 -0
- package/src/backend.ts +36 -0
- package/src/backends/memory-store.ts +184 -35
- package/src/bin/http.ts +12 -0
- package/src/bin/install.ts +3 -2
- package/src/bin/stdio.ts +25 -8
- package/src/compaction.ts +126 -0
- package/src/embedding/evermind-embedder.ts +276 -0
- package/src/embedding/index.ts +54 -0
- package/src/index.ts +18 -4
- package/src/install/claude-hooks.ts +116 -15
- package/src/install/server-spec.ts +25 -0
- package/src/tools.ts +133 -12
package/src/tools.ts
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { z } from "zod";
|
|
18
|
-
import type { MemoryBackend, RecallHit } from "./backend.js";
|
|
18
|
+
import type { MemoryBackend, RankedRecall, RecallHit, RecallMethod, RememberInput } from "./backend.js";
|
|
19
|
+
import { DEFAULT_STUB_CHARS, planCompaction, type CompactionSkip } from "./compaction.js";
|
|
19
20
|
|
|
20
21
|
/** The MCP CallToolResult shape both server frameworks expect. */
|
|
21
22
|
export interface ToolResult {
|
|
@@ -63,15 +64,58 @@ function fail(text: string): ToolResult {
|
|
|
63
64
|
return { content: [{ type: "text", text }], isError: true };
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
/**
|
|
68
|
+
* How the ordering was produced, in one line the model can read.
|
|
69
|
+
*
|
|
70
|
+
* A semantic ranking that silently degraded to word overlap is indistinguishable
|
|
71
|
+
* from one that did not - which is exactly how "recall got worse" stays invisible.
|
|
72
|
+
* Naming the ranker (the same `embedding` / `lexical` vocabulary the gateway's
|
|
73
|
+
* Evermind recall result reports) makes the degrade legible instead.
|
|
74
|
+
*/
|
|
75
|
+
const RANKER_LINE: Record<RecallMethod, string> = {
|
|
76
|
+
embedding: "Ranked by: semantic recall (SSM embedding fused with lexical).",
|
|
77
|
+
lexical: "Ranked by: lexical recall (fallback - no embedding model loaded).",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function renderHits(hits: RecallHit[], maxChars: number, method?: RecallMethod): string {
|
|
81
|
+
const ranker = method ? `\n${RANKER_LINE[method]}` : "";
|
|
82
|
+
if (hits.length === 0) return `No matching memories.${ranker}`;
|
|
83
|
+
return (
|
|
84
|
+
hits
|
|
85
|
+
.map((h) => {
|
|
86
|
+
const tags = h.tags?.length ? ` tags=[${h.tags.join(", ")}]` : "";
|
|
87
|
+
const score = h.score != null ? ` score=${h.score.toFixed(3)}` : "";
|
|
88
|
+
return `• ${h.key}${score}${tags}\n ${clip(h.content, maxChars)}`;
|
|
89
|
+
})
|
|
90
|
+
.join("\n") + ranker
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Recall through the backend's BEST available path: `recallRanked` when the backend
|
|
96
|
+
* can name its ranker, plain `recall` otherwise. One helper, so every recall-shaped
|
|
97
|
+
* tool reports the method identically - or stays silent identically.
|
|
98
|
+
*/
|
|
99
|
+
async function rankedRecall(
|
|
100
|
+
backend: MemoryBackend,
|
|
101
|
+
query: string,
|
|
102
|
+
topK: number,
|
|
103
|
+
): Promise<Partial<RankedRecall> & { hits: RecallHit[] }> {
|
|
104
|
+
if (backend.recallRanked) return backend.recallRanked(query, topK);
|
|
105
|
+
return { hits: await backend.recall(query, topK) };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const SKIP_REASONS: Record<CompactionSkip["reason"], string> = {
|
|
109
|
+
not_found: "not found",
|
|
110
|
+
already_compacted: "already compacted",
|
|
111
|
+
not_smaller: "already shorter than its stub",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** One-line compaction receipt: what shrank, what it saved, and what was left alone. */
|
|
115
|
+
function renderCompaction(compacted: number, bytesSaved: number, skipped: CompactionSkip[]): string {
|
|
116
|
+
const lines = [`Compacted ${compacted} memory(ies), ${bytesSaved} character(s) reclaimed.`];
|
|
117
|
+
for (const s of skipped) lines.push(`• skipped ${s.key} — ${SKIP_REASONS[s.reason]}`);
|
|
118
|
+
return lines.join("\n");
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
/**
|
|
@@ -107,8 +151,8 @@ export function buildMemoryTools(backend: MemoryBackend, opts: MemoryToolsOption
|
|
|
107
151
|
const query = String(args["query"] ?? "");
|
|
108
152
|
if (!query.trim()) return fail("query is required.");
|
|
109
153
|
const k = Math.min(maxResults, Number(args["topK"] ?? maxResults));
|
|
110
|
-
const
|
|
111
|
-
return ok(renderHits(hits.slice(0, maxResults), maxContent));
|
|
154
|
+
const ranked = await rankedRecall(backend, query, k);
|
|
155
|
+
return ok(renderHits(ranked.hits.slice(0, maxResults), maxContent, ranked.method));
|
|
112
156
|
} catch (err) {
|
|
113
157
|
return fail(`recall failed: ${String(err)}`);
|
|
114
158
|
}
|
|
@@ -197,6 +241,83 @@ export function buildMemoryTools(backend: MemoryBackend, opts: MemoryToolsOption
|
|
|
197
241
|
});
|
|
198
242
|
}
|
|
199
243
|
|
|
244
|
+
// memory_compact — the FIRST-CLASS compaction path. It writes through the live
|
|
245
|
+
// store (get → plan → remember), so a durable backend's in-memory state and its
|
|
246
|
+
// on-disk snapshot agree afterwards. Compacting the snapshot file behind the
|
|
247
|
+
// server's back instead is what races: the server still holds the full bodies and
|
|
248
|
+
// re-snapshots over them on its next write. Requires read + write, so it is
|
|
249
|
+
// registered alongside the other write tools.
|
|
250
|
+
if (writable && backend.remember) {
|
|
251
|
+
tools.push({
|
|
252
|
+
name: "memory_compact",
|
|
253
|
+
description:
|
|
254
|
+
"Shrink memories that have already been absorbed elsewhere (folded into a model, " +
|
|
255
|
+
"summarised into a longer-lived note) down to a one-line pointer stub, reclaiming the " +
|
|
256
|
+
"context they were eating. Call this AFTER something durable has learned the facts — " +
|
|
257
|
+
"never as a way to tidy up, because the full body is gone afterwards. Already-compacted " +
|
|
258
|
+
"keys are skipped, so repeating the call is safe.",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
keys: z.array(z.string()).min(1).describe("Exact keys of the memories to compact."),
|
|
261
|
+
stub: z
|
|
262
|
+
.string()
|
|
263
|
+
.optional()
|
|
264
|
+
.describe(
|
|
265
|
+
"Explicit replacement body applied to every listed key. Omit to generate " +
|
|
266
|
+
"'[absorbed→Evermind vN] <first line>' from each entry's own first line.",
|
|
267
|
+
),
|
|
268
|
+
version: z
|
|
269
|
+
.number()
|
|
270
|
+
.int()
|
|
271
|
+
.min(0)
|
|
272
|
+
.optional()
|
|
273
|
+
.describe("Version of the model that absorbed these facts; appears in the generated stub."),
|
|
274
|
+
maxChars: z
|
|
275
|
+
.number()
|
|
276
|
+
.int()
|
|
277
|
+
.min(20)
|
|
278
|
+
.max(500)
|
|
279
|
+
.optional()
|
|
280
|
+
.describe(`Max characters of the generated pointer line (default ${DEFAULT_STUB_CHARS}).`),
|
|
281
|
+
},
|
|
282
|
+
handler: async (args) => {
|
|
283
|
+
try {
|
|
284
|
+
const raw = args["keys"];
|
|
285
|
+
const keys = Array.isArray(raw) ? raw.map((k) => String(k)).filter(Boolean) : [];
|
|
286
|
+
if (keys.length === 0) return fail("keys is required.");
|
|
287
|
+
|
|
288
|
+
// ONE read pass: the hit carries both the body the plan reasons about
|
|
289
|
+
// and the tags/importance the rewrite must preserve.
|
|
290
|
+
const hits = new Map<string, RecallHit>();
|
|
291
|
+
for (const key of keys) {
|
|
292
|
+
const hit = await backend.get(key);
|
|
293
|
+
if (hit) hits.set(key, hit);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const plan = planCompaction(keys.map((key) => ({ key, content: hits.get(key)?.content })), {
|
|
297
|
+
stub: typeof args["stub"] === "string" ? (args["stub"] as string) : undefined,
|
|
298
|
+
version: args["version"] == null ? undefined : Number(args["version"]),
|
|
299
|
+
maxChars: args["maxChars"] == null ? undefined : Number(args["maxChars"]),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Preserve tags/importance — compaction shortens a body, it does not
|
|
303
|
+
// reclassify the fact.
|
|
304
|
+
const writes: RememberInput[] = plan.writes.map((w) => {
|
|
305
|
+
const prev = hits.get(w.key);
|
|
306
|
+
return { key: w.key, content: w.content, tags: prev?.tags, importance: prev?.importance };
|
|
307
|
+
});
|
|
308
|
+
if (writes.length > 0) {
|
|
309
|
+
if (backend.rememberMany) await backend.rememberMany(writes);
|
|
310
|
+
else for (const w of writes) await backend.remember!(w);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return ok(renderCompaction(plan.writes.length, plan.bytesSaved, plan.skipped));
|
|
314
|
+
} catch (err) {
|
|
315
|
+
return fail(`compact failed: ${String(err)}`);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
200
321
|
if (writable && backend.forget) {
|
|
201
322
|
tools.push({
|
|
202
323
|
name: "memory_forget",
|