@seanhogg/builderforce-memory-mcp 2026.7.12 → 2026.7.14

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.
@@ -1 +1 @@
1
- {"version":3,"file":"claude-hooks.d.ts","sourceRoot":"","sources":["../../src/install/claude-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA6I1D;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,MAAM,CA0BzC;AAED,qFAAqF;AACrF,MAAM,WAAW,gBAAgB;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAOpE;AASD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACtB,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAmCrE"}
1
+ {"version":3,"file":"claude-hooks.d.ts","sourceRoot":"","sources":["../../src/install/claude-hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE3C,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA8L1D;AAED,2EAA2E;AAC3E,wBAAgB,gBAAgB,IAAI,MAAM,CA0BzC;AAED,qFAAqF;AACrF,MAAM,WAAW,gBAAgB;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAOpE;AASD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACtB,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAmDrE"}
@@ -33,6 +33,8 @@ export function bfmemHookSource(memoryFile) {
33
33
  * durable correction is persisted via memory_remember.
34
34
  */
35
35
  import fs from "node:fs";
36
+ import os from "node:os";
37
+ import path from "node:path";
36
38
 
37
39
  const FILE = ${JSON.stringify(memoryFile)};
38
40
  const mode = process.argv[2] || "--session";
@@ -48,6 +50,27 @@ function tokens(s) {
48
50
  return String(s || "").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
49
51
  }
50
52
 
53
+ // ── Per-session injection ledger ─────────────────────────────────────────────
54
+ // The SessionStart digest and the per-prompt recall both push memory CONTENT
55
+ // into the context window, where it stays for the rest of the session. Without
56
+ // dedup, a long session re-injects the same memories on every turn — unbounded
57
+ // growth that is exactly what pushes a session past 150k tokens. The ledger
58
+ // records which keys have already been surfaced THIS session so we inject each
59
+ // memory at most once. Keyed by the host-provided session_id; degrades to the
60
+ // old always-inject behaviour when no id is available.
61
+ function ledgerFile(sid) {
62
+ const safe = String(sid || "").replace(/[^a-zA-Z0-9_.-]/g, "").slice(0, 80) || "nosession";
63
+ return path.join(os.tmpdir(), "bfmem-injected-" + safe + ".json");
64
+ }
65
+ function loadLedger(sid) {
66
+ try { const a = JSON.parse(fs.readFileSync(ledgerFile(sid), "utf8")); return new Set(Array.isArray(a) ? a : []); }
67
+ catch { return new Set(); }
68
+ }
69
+ function saveLedger(sid, set) {
70
+ if (!sid) return; // no id → no dedup state to persist (safe fallback)
71
+ try { fs.writeFileSync(ledgerFile(sid), JSON.stringify([...set].slice(-500))); } catch { /* best effort */ }
72
+ }
73
+
51
74
  if (mode === "--precompact") {
52
75
  process.stdout.write(
53
76
  "[builderforce-memory] Context is about to be compacted. Persist any durable learnings from this session — decisions, fixes, project facts, user preferences — by calling memory_remember with concise one-line facts. Recall them later with memory_recall instead of re-reading files.",
@@ -56,13 +79,18 @@ if (mode === "--precompact") {
56
79
  }
57
80
 
58
81
  if (mode === "--recall") {
59
- let prompt = "";
60
- try { const j = JSON.parse(readStdin() || "{}"); prompt = typeof j.prompt === "string" ? j.prompt : ""; } catch { /* no input */ }
82
+ let prompt = "", sid = "";
83
+ try {
84
+ const j = JSON.parse(readStdin() || "{}");
85
+ prompt = typeof j.prompt === "string" ? j.prompt : "";
86
+ sid = typeof j.session_id === "string" ? j.session_id : "";
87
+ } catch { /* no input */ }
61
88
  if (!prompt) process.exit(0);
62
89
  const entries = loadEntries();
63
90
  if (entries.length === 0) process.exit(0);
64
91
  const q = new Set(tokens(prompt));
65
92
  if (q.size === 0) process.exit(0);
93
+ const led = loadLedger(sid);
66
94
  const scored = entries
67
95
  .map((e) => {
68
96
  const hay = new Set(tokens([e.key, e.content, (e.tags || []).join(" ")].join(" ")));
@@ -71,10 +99,13 @@ if (mode === "--recall") {
71
99
  return { e, score };
72
100
  })
73
101
  .filter((x) => x.score >= 2)
102
+ .filter((x) => !led.has(x.e.key)) // already surfaced this session → don't re-inject
74
103
  .sort((a, b) => b.score - a.score)
75
104
  .slice(0, 3);
76
105
  if (scored.length === 0) process.exit(0);
77
- const lines = scored.map((x) => "• " + x.e.key + ": " + String(x.e.content || "").slice(0, 240)).join("\\n");
106
+ for (const x of scored) led.add(x.e.key);
107
+ saveLedger(sid, led);
108
+ const lines = scored.map((x) => "• " + x.e.key + ": " + String(x.e.content || "").slice(0, 200)).join("\\n");
78
109
  process.stdout.write(
79
110
  "[builderforce-memory] Relevant stored memories for this request — use these instead of re-deriving or re-reading:\\n" + lines,
80
111
  );
@@ -145,19 +176,37 @@ if (mode === "--capture") {
145
176
  process.exit(0);
146
177
  }
147
178
 
179
+ // SessionStart (default mode). Read the source + session_id so we can (a) seed
180
+ // the per-session ledger with the digest keys and (b) skip re-injecting on
181
+ // 'resume', where the earlier context — including this digest — is retained and
182
+ // a second copy would be pure waste.
183
+ let src = "startup", sessionId = "";
184
+ try {
185
+ const j = JSON.parse(readStdin() || "{}");
186
+ if (typeof j.source === "string") src = j.source;
187
+ if (typeof j.session_id === "string") sessionId = j.session_id;
188
+ } catch { /* no input */ }
189
+
148
190
  const entries = loadEntries();
149
191
  if (entries.length === 0) {
150
- process.stdout.write(
192
+ if (src !== "resume") process.stdout.write(
151
193
  "[builderforce-memory] Active (memory empty). Store durable facts with memory_remember and retrieve them with memory_recall instead of re-reading files or chat history — that is how token usage stays low.",
152
194
  );
153
195
  process.exit(0);
154
196
  }
155
- const top = entries
197
+ const ranked = entries
156
198
  .map((e) => ({ key: e.key, content: String(e.content ?? ""), importance: typeof e.importance === "number" ? e.importance : 0.5 }))
157
199
  .sort((a, b) => b.importance - a.importance)
158
- .slice(0, 8)
159
- .map((e) => "• " + e.key + ": " + e.content.slice(0, 200))
160
- .join("\\n");
200
+ .slice(0, 6);
201
+
202
+ // Record the digest keys so per-prompt recall won't re-surface what's already
203
+ // on screen. Fresh window (startup/clear) → new ledger; otherwise merge.
204
+ const led = (src === "startup" || src === "clear") ? new Set() : loadLedger(sessionId);
205
+ for (const e of ranked) led.add(e.key);
206
+ saveLedger(sessionId, led);
207
+
208
+ if (src === "resume") process.exit(0); // context retained across resume — nothing to re-inject
209
+ const top = ranked.map((e) => "• " + e.key + ": " + e.content.slice(0, 180)).join("\\n");
161
210
  process.stdout.write(
162
211
  "[builderforce-memory] Active — token-saving memory loaded. Before re-reading files or history, call memory_recall. Store durable facts with memory_remember.\\nTop memories:\\n" + top,
163
212
  );
@@ -235,15 +284,29 @@ export function installClaudeCombo(opts) {
235
284
  fs.copyFileSync(paths.settingsFile, `${paths.settingsFile}.bak`);
236
285
  }
237
286
  const hooks = (settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {});
287
+ // Compare commands by a normalized form (path separators unified, whitespace
288
+ // collapsed, case-folded) so a re-install can't register a second copy of the
289
+ // same hook just because an earlier run wrote "C:/…" and this one writes
290
+ // "C:\…". Without this, Windows accumulates duplicate SessionStart/PreCompact
291
+ // hooks that each fire — doubling the memory the combo injects every session.
292
+ const normCmd = (c) => String(c ?? "").replace(/\\/g, "/").replace(/\s+/g, " ").trim().toLowerCase();
293
+ const hookRef = normCmd(paths.hookScript);
238
294
  const added = [];
239
295
  for (const { event, mode } of HOOK_EVENTS) {
240
296
  const command = `node "${paths.hookScript}" ${mode}`;
241
- const arr = (Array.isArray(hooks[event]) ? hooks[event] : (hooks[event] = []));
242
- const exists = arr.some((g) => (g.hooks || []).some((h) => h.command === command));
243
- if (!exists) {
244
- arr.push({ hooks: [{ type: "command", command }] });
297
+ const arr = (Array.isArray(hooks[event]) ? hooks[event] : []);
298
+ // Drop any existing group that points at OUR hook script for THIS mode
299
+ // (any path-separator variant), then re-add exactly one canonical entry.
300
+ // This both dedupes accidental duplicates and refreshes the command.
301
+ const kept = arr.filter((g) => !(g.hooks || []).some((h) => {
302
+ const n = normCmd(h.command ?? "");
303
+ return n.includes(hookRef) && n.endsWith(mode);
304
+ }));
305
+ const wasPresent = kept.length !== arr.length;
306
+ kept.push({ hooks: [{ type: "command", command }] });
307
+ hooks[event] = kept;
308
+ if (!wasPresent)
245
309
  added.push(event);
246
- }
247
310
  }
248
311
  settings.hooks = hooks;
249
312
  const settingsDir = nodePath.dirname(paths.settingsFile);
@@ -1 +1 @@
1
- {"version":3,"file":"claude-hooks.js","sourceRoot":"","sources":["../../src/install/claude-hooks.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"claude-hooks.js","sourceRoot":"","sources":["../../src/install/claude-hooks.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanhogg/builderforce-memory-mcp",
3
- "version": "2026.7.12",
3
+ "version": "2026.7.14",
4
4
  "description": "BuilderForce Agent Memory — MCP transport. Exposes @seanhogg/builderforce-memory to any MCP client (Claude Agent SDK in-process, stdio, or HTTP) over a pluggable MemoryBackend, with token-saving fetch-on-demand recall.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "peerDependencies": {
52
52
  "@anthropic-ai/claude-agent-sdk": ">=0.1.0",
53
- "@seanhogg/builderforce-memory": "^2026.7.12",
53
+ "@seanhogg/builderforce-memory": "^2026.7.13",
54
54
  "fake-indexeddb": "^6.0.0"
55
55
  },
56
56
  "peerDependenciesMeta": {
@@ -73,7 +73,7 @@
73
73
  "fake-indexeddb": "^6.0.0",
74
74
  "typescript": "^6.0.3",
75
75
  "@typescript/native-preview": "7.0.0-dev.20260627.2",
76
- "@seanhogg/builderforce-memory": "2026.7.12"
76
+ "@seanhogg/builderforce-memory": "2026.7.14"
77
77
  },
78
78
  "engines": {
79
79
  "node": ">=18.0.0"
@@ -36,6 +36,8 @@ export function bfmemHookSource(memoryFile: string): string {
36
36
  * durable correction is persisted via memory_remember.
37
37
  */
38
38
  import fs from "node:fs";
39
+ import os from "node:os";
40
+ import path from "node:path";
39
41
 
40
42
  const FILE = ${JSON.stringify(memoryFile)};
41
43
  const mode = process.argv[2] || "--session";
@@ -51,6 +53,27 @@ function tokens(s) {
51
53
  return String(s || "").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
52
54
  }
53
55
 
56
+ // ── Per-session injection ledger ─────────────────────────────────────────────
57
+ // The SessionStart digest and the per-prompt recall both push memory CONTENT
58
+ // into the context window, where it stays for the rest of the session. Without
59
+ // dedup, a long session re-injects the same memories on every turn — unbounded
60
+ // growth that is exactly what pushes a session past 150k tokens. The ledger
61
+ // records which keys have already been surfaced THIS session so we inject each
62
+ // memory at most once. Keyed by the host-provided session_id; degrades to the
63
+ // old always-inject behaviour when no id is available.
64
+ function ledgerFile(sid) {
65
+ const safe = String(sid || "").replace(/[^a-zA-Z0-9_.-]/g, "").slice(0, 80) || "nosession";
66
+ return path.join(os.tmpdir(), "bfmem-injected-" + safe + ".json");
67
+ }
68
+ function loadLedger(sid) {
69
+ try { const a = JSON.parse(fs.readFileSync(ledgerFile(sid), "utf8")); return new Set(Array.isArray(a) ? a : []); }
70
+ catch { return new Set(); }
71
+ }
72
+ function saveLedger(sid, set) {
73
+ if (!sid) return; // no id → no dedup state to persist (safe fallback)
74
+ try { fs.writeFileSync(ledgerFile(sid), JSON.stringify([...set].slice(-500))); } catch { /* best effort */ }
75
+ }
76
+
54
77
  if (mode === "--precompact") {
55
78
  process.stdout.write(
56
79
  "[builderforce-memory] Context is about to be compacted. Persist any durable learnings from this session — decisions, fixes, project facts, user preferences — by calling memory_remember with concise one-line facts. Recall them later with memory_recall instead of re-reading files.",
@@ -59,13 +82,18 @@ if (mode === "--precompact") {
59
82
  }
60
83
 
61
84
  if (mode === "--recall") {
62
- let prompt = "";
63
- try { const j = JSON.parse(readStdin() || "{}"); prompt = typeof j.prompt === "string" ? j.prompt : ""; } catch { /* no input */ }
85
+ let prompt = "", sid = "";
86
+ try {
87
+ const j = JSON.parse(readStdin() || "{}");
88
+ prompt = typeof j.prompt === "string" ? j.prompt : "";
89
+ sid = typeof j.session_id === "string" ? j.session_id : "";
90
+ } catch { /* no input */ }
64
91
  if (!prompt) process.exit(0);
65
92
  const entries = loadEntries();
66
93
  if (entries.length === 0) process.exit(0);
67
94
  const q = new Set(tokens(prompt));
68
95
  if (q.size === 0) process.exit(0);
96
+ const led = loadLedger(sid);
69
97
  const scored = entries
70
98
  .map((e) => {
71
99
  const hay = new Set(tokens([e.key, e.content, (e.tags || []).join(" ")].join(" ")));
@@ -74,10 +102,13 @@ if (mode === "--recall") {
74
102
  return { e, score };
75
103
  })
76
104
  .filter((x) => x.score >= 2)
105
+ .filter((x) => !led.has(x.e.key)) // already surfaced this session → don't re-inject
77
106
  .sort((a, b) => b.score - a.score)
78
107
  .slice(0, 3);
79
108
  if (scored.length === 0) process.exit(0);
80
- const lines = scored.map((x) => "• " + x.e.key + ": " + String(x.e.content || "").slice(0, 240)).join("\\n");
109
+ for (const x of scored) led.add(x.e.key);
110
+ saveLedger(sid, led);
111
+ const lines = scored.map((x) => "• " + x.e.key + ": " + String(x.e.content || "").slice(0, 200)).join("\\n");
81
112
  process.stdout.write(
82
113
  "[builderforce-memory] Relevant stored memories for this request — use these instead of re-deriving or re-reading:\\n" + lines,
83
114
  );
@@ -148,19 +179,37 @@ if (mode === "--capture") {
148
179
  process.exit(0);
149
180
  }
150
181
 
182
+ // SessionStart (default mode). Read the source + session_id so we can (a) seed
183
+ // the per-session ledger with the digest keys and (b) skip re-injecting on
184
+ // 'resume', where the earlier context — including this digest — is retained and
185
+ // a second copy would be pure waste.
186
+ let src = "startup", sessionId = "";
187
+ try {
188
+ const j = JSON.parse(readStdin() || "{}");
189
+ if (typeof j.source === "string") src = j.source;
190
+ if (typeof j.session_id === "string") sessionId = j.session_id;
191
+ } catch { /* no input */ }
192
+
151
193
  const entries = loadEntries();
152
194
  if (entries.length === 0) {
153
- process.stdout.write(
195
+ if (src !== "resume") process.stdout.write(
154
196
  "[builderforce-memory] Active (memory empty). Store durable facts with memory_remember and retrieve them with memory_recall instead of re-reading files or chat history — that is how token usage stays low.",
155
197
  );
156
198
  process.exit(0);
157
199
  }
158
- const top = entries
200
+ const ranked = entries
159
201
  .map((e) => ({ key: e.key, content: String(e.content ?? ""), importance: typeof e.importance === "number" ? e.importance : 0.5 }))
160
202
  .sort((a, b) => b.importance - a.importance)
161
- .slice(0, 8)
162
- .map((e) => "• " + e.key + ": " + e.content.slice(0, 200))
163
- .join("\\n");
203
+ .slice(0, 6);
204
+
205
+ // Record the digest keys so per-prompt recall won't re-surface what's already
206
+ // on screen. Fresh window (startup/clear) → new ledger; otherwise merge.
207
+ const led = (src === "startup" || src === "clear") ? new Set() : loadLedger(sessionId);
208
+ for (const e of ranked) led.add(e.key);
209
+ saveLedger(sessionId, led);
210
+
211
+ if (src === "resume") process.exit(0); // context retained across resume — nothing to re-inject
212
+ const top = ranked.map((e) => "• " + e.key + ": " + e.content.slice(0, 180)).join("\\n");
164
213
  process.stdout.write(
165
214
  "[builderforce-memory] Active — token-saving memory loaded. Before re-reading files or history, call memory_recall. Store durable facts with memory_remember.\\nTop memories:\\n" + top,
166
215
  );
@@ -251,12 +300,28 @@ export function installClaudeCombo(opts: {
251
300
  fs.copyFileSync(paths.settingsFile, `${paths.settingsFile}.bak`);
252
301
  }
253
302
  const hooks = (settings.hooks && typeof settings.hooks === "object" ? settings.hooks : {}) as Record<string, unknown>;
303
+ // Compare commands by a normalized form (path separators unified, whitespace
304
+ // collapsed, case-folded) so a re-install can't register a second copy of the
305
+ // same hook just because an earlier run wrote "C:/…" and this one writes
306
+ // "C:\…". Without this, Windows accumulates duplicate SessionStart/PreCompact
307
+ // hooks that each fire — doubling the memory the combo injects every session.
308
+ const normCmd = (c: string) => String(c ?? "").replace(/\\/g, "/").replace(/\s+/g, " ").trim().toLowerCase();
309
+ const hookRef = normCmd(paths.hookScript);
254
310
  const added: string[] = [];
255
311
  for (const { event, mode } of HOOK_EVENTS) {
256
312
  const command = `node "${paths.hookScript}" ${mode}`;
257
- const arr = (Array.isArray(hooks[event]) ? hooks[event] : (hooks[event] = [])) as Array<{ hooks?: Array<{ type?: string; command?: string }> }>;
258
- const exists = arr.some((g) => (g.hooks || []).some((h) => h.command === command));
259
- if (!exists) { arr.push({ hooks: [{ type: "command", command }] }); added.push(event); }
313
+ const arr = (Array.isArray(hooks[event]) ? hooks[event] : []) as Array<{ hooks?: Array<{ type?: string; command?: string }> }>;
314
+ // Drop any existing group that points at OUR hook script for THIS mode
315
+ // (any path-separator variant), then re-add exactly one canonical entry.
316
+ // This both dedupes accidental duplicates and refreshes the command.
317
+ const kept = arr.filter((g) => !(g.hooks || []).some((h) => {
318
+ const n = normCmd(h.command ?? "");
319
+ return n.includes(hookRef) && n.endsWith(mode);
320
+ }));
321
+ const wasPresent = kept.length !== arr.length;
322
+ kept.push({ hooks: [{ type: "command", command }] });
323
+ hooks[event] = kept;
324
+ if (!wasPresent) added.push(event);
260
325
  }
261
326
  settings.hooks = hooks;
262
327
  const settingsDir = nodePath.dirname(paths.settingsFile);