@seanhogg/builderforce-memory-mcp 2026.7.11 → 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,
|
|
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 {
|
|
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
|
|
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
|
|
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,
|
|
159
|
-
|
|
160
|
-
|
|
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] :
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,QAAQ,MAAM,WAAW,CAAC;AAGjC,kFAAkF;AAClF,MAAM,UAAU,eAAe,CAAC,UAAkB;IAC9C,OAAO
|
|
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.
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
|
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,
|
|
162
|
-
|
|
163
|
-
|
|
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] :
|
|
258
|
-
|
|
259
|
-
|
|
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);
|