memtrace 0.3.51 → 0.3.53
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/lib/claude-integration.js +57 -27
- package/package.json +4 -4
|
@@ -213,9 +213,32 @@ function applySettingsHooks(settings, hooksDir) {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
/**
|
|
216
|
-
* Merge our hook into an existing hooks array
|
|
217
|
-
*
|
|
218
|
-
*
|
|
216
|
+
* Merge our hook into an existing hooks array — strictly idempotent.
|
|
217
|
+
*
|
|
218
|
+
* Earlier versions only deduped by the `_managed_by` / `_hook_kind`
|
|
219
|
+
* marker fields and only inspected the first block that matched. Two
|
|
220
|
+
* gaps fell out of that:
|
|
221
|
+
*
|
|
222
|
+
* 1. Some Claude Code workflows (and external settings.json linters)
|
|
223
|
+
* round-trip the file and strip underscore-prefixed keys they
|
|
224
|
+
* don't recognise. With the markers gone, our dedup matched
|
|
225
|
+
* nothing and every `memtrace install` appended a fresh block —
|
|
226
|
+
* we've seen real users end up with 9× duplicates and a
|
|
227
|
+
* noticeable per-prompt slowdown.
|
|
228
|
+
*
|
|
229
|
+
* 2. Even when markers survived, the function only ever reached
|
|
230
|
+
* into the *first* matching block. Pre-existing duplicates in
|
|
231
|
+
* other blocks were never cleaned up, just left to compound.
|
|
232
|
+
*
|
|
233
|
+
* The new contract: regardless of how settings.json got into its
|
|
234
|
+
* current shape, this function leaves it with exactly one of our
|
|
235
|
+
* hooks installed under exactly one block. Running it 100 times
|
|
236
|
+
* produces the same result as running it once — and self-heals
|
|
237
|
+
* existing duplication on the way through.
|
|
238
|
+
*
|
|
239
|
+
* Match strategy: a hook is "ours" if it has the managed markers
|
|
240
|
+
* intact OR its command path matches one we generate. The
|
|
241
|
+
* command-path match is what catches stripped-marker pollution.
|
|
219
242
|
*
|
|
220
243
|
* @param {Array} existing
|
|
221
244
|
* @param {object} ourEntry shape `{ matcher?, hooks: [...] }`
|
|
@@ -223,33 +246,40 @@ function applySettingsHooks(settings, hooksDir) {
|
|
|
223
246
|
* @returns {Array}
|
|
224
247
|
*/
|
|
225
248
|
function mergeHookList(existing, ourEntry, ourHook) {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (!block.hooks) continue;
|
|
231
|
-
const hasOurs = block.hooks.some(
|
|
232
|
-
(h) => h && h._managed_by === HOOK_MANAGED_TAG && h._hook_kind === ourHook._hook_kind,
|
|
249
|
+
const isOurs = (h) =>
|
|
250
|
+
!!h && (
|
|
251
|
+
(h._managed_by === HOOK_MANAGED_TAG && h._hook_kind === ourHook._hook_kind) ||
|
|
252
|
+
(typeof h.command === "string" && h.command === ourHook.command)
|
|
233
253
|
);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
254
|
+
|
|
255
|
+
// Phase 1: walk every block, strip every "ours" hook, drop blocks
|
|
256
|
+
// left empty by the strip. This sweep is what self-heals an
|
|
257
|
+
// already-polluted settings.json instead of leaving prior copies
|
|
258
|
+
// alongside the fresh one.
|
|
259
|
+
const cleaned = [];
|
|
260
|
+
for (const block of existing) {
|
|
261
|
+
if (!block || !Array.isArray(block.hooks)) {
|
|
262
|
+
cleaned.push(block);
|
|
263
|
+
continue;
|
|
237
264
|
}
|
|
265
|
+
const remainingHooks = block.hooks.filter((h) => !isOurs(h));
|
|
266
|
+
if (remainingHooks.length === 0) {
|
|
267
|
+
// Block existed only to host our hook — drop it entirely so
|
|
268
|
+
// we don't leave behind an empty `{ matcher: '…', hooks: [] }`
|
|
269
|
+
// skeleton for Claude Code to ignore on every read.
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
cleaned.push({ ...block, hooks: remainingHooks });
|
|
238
273
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
(h) => !(h && h._managed_by === HOOK_MANAGED_TAG && h._hook_kind === ourHook._hook_kind),
|
|
249
|
-
);
|
|
250
|
-
block.hooks.push(ourHook);
|
|
251
|
-
out[foundManagedBlock] = block;
|
|
252
|
-
return out;
|
|
274
|
+
|
|
275
|
+
// Phase 2: re-insert exactly once as a fresh block. We deliberately
|
|
276
|
+
// do NOT merge into an existing block (even one with the same matcher)
|
|
277
|
+
// because Claude Code semantics treat each block independently anyway,
|
|
278
|
+
// and keeping our hook in its own block makes the install / uninstall
|
|
279
|
+
// boundary explicit. Phase 1 has already removed every prior copy of
|
|
280
|
+
// ours, so this single append yields exactly one of us in the output.
|
|
281
|
+
cleaned.push(ourEntry);
|
|
282
|
+
return cleaned;
|
|
253
283
|
}
|
|
254
284
|
|
|
255
285
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memtrace",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.53",
|
|
4
4
|
"description": "Code intelligence graph — MCP server + AI agent skills + visualization UI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"fs-extra": "^11.0.0"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@memtrace/darwin-arm64": "0.3.
|
|
43
|
-
"@memtrace/linux-x64": "0.3.
|
|
44
|
-
"@memtrace/win32-x64": "0.3.
|
|
42
|
+
"@memtrace/darwin-arm64": "0.3.53",
|
|
43
|
+
"@memtrace/linux-x64": "0.3.53",
|
|
44
|
+
"@memtrace/win32-x64": "0.3.53"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=18"
|