myagentmemory 0.5.4 → 0.5.5
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/dist/hooks.js +70 -38
- package/package.json +1 -1
- package/src/hooks.ts +71 -38
package/dist/hooks.js
CHANGED
|
@@ -125,7 +125,13 @@ function hasClaudeHookGroup(homeDir, eventKey, command) {
|
|
|
125
125
|
if (!hook || typeof hook !== "object")
|
|
126
126
|
continue;
|
|
127
127
|
const h = hook;
|
|
128
|
-
|
|
128
|
+
// Match by command alone (marker is optional) — legacy entries that
|
|
129
|
+
// predate HOOK_MARKER_JSON already run this exact command, and the
|
|
130
|
+
// command string is specific enough to agent-memory that it's a safe
|
|
131
|
+
// signal on its own. Otherwise a pre-marker install is invisible here,
|
|
132
|
+
// doctor/isHookInstalled falsely report "not installed" for hooks that
|
|
133
|
+
// are actually live, and upsertClaudeHookGroup can't find them to dedupe.
|
|
134
|
+
if (h.command === command)
|
|
129
135
|
return true;
|
|
130
136
|
}
|
|
131
137
|
}
|
|
@@ -330,9 +336,10 @@ function uninstallPiMemoryDelegate(homeDir) {
|
|
|
330
336
|
*/
|
|
331
337
|
function upsertClaudeHookGroup(hooks, eventKey, command) {
|
|
332
338
|
const groups = Array.isArray(hooks[eventKey]) ? [...hooks[eventKey]] : [];
|
|
333
|
-
|
|
334
|
-
for
|
|
335
|
-
|
|
339
|
+
// Locate every agent-memory-owned hook entry — matched by marker, or by
|
|
340
|
+
// exact command string for legacy pre-marker installs — wherever it lives.
|
|
341
|
+
const owned = [];
|
|
342
|
+
for (const group of groups) {
|
|
336
343
|
if (!group || typeof group !== "object")
|
|
337
344
|
continue;
|
|
338
345
|
const g = group;
|
|
@@ -341,44 +348,68 @@ function upsertClaudeHookGroup(hooks, eventKey, command) {
|
|
|
341
348
|
if (!hook || typeof hook !== "object")
|
|
342
349
|
continue;
|
|
343
350
|
const h = hook;
|
|
344
|
-
if (h[HOOK_MARKER_JSON] === true)
|
|
345
|
-
|
|
346
|
-
break;
|
|
347
|
-
}
|
|
351
|
+
if (h[HOOK_MARKER_JSON] === true || h.command === command)
|
|
352
|
+
owned.push({ group: g, hook: h });
|
|
348
353
|
}
|
|
349
354
|
}
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
for (const idx of dupes.reverse())
|
|
354
|
-
groups.splice(idx, 1);
|
|
355
|
-
const group = groups[keep];
|
|
356
|
-
let changed = dupes.length > 0;
|
|
357
|
-
if ("matcher" in group) {
|
|
358
|
-
delete group.matcher;
|
|
359
|
-
changed = true;
|
|
360
|
-
}
|
|
361
|
-
const list = group.hooks;
|
|
362
|
-
for (const h of list) {
|
|
363
|
-
if (h[HOOK_MARKER_JSON] === true && h.command !== command) {
|
|
364
|
-
h.command = command;
|
|
365
|
-
changed = true;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
355
|
+
if (owned.length === 0) {
|
|
356
|
+
// No existing managed hook anywhere — add a fresh group without a matcher so it fires on all harnesses.
|
|
357
|
+
groups.push({ hooks: [{ type: "command", command, [HOOK_MARKER_JSON]: true }] });
|
|
368
358
|
hooks[eventKey] = groups;
|
|
369
|
-
return { changed, hadManaged:
|
|
370
|
-
}
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
hooks
|
|
374
|
-
|
|
359
|
+
return { changed: true, hadManaged: false };
|
|
360
|
+
}
|
|
361
|
+
// Keep the first owned entry in place — fixed up in place, preserving its
|
|
362
|
+
// identity so a no-op re-install reports unchanged — and remove every
|
|
363
|
+
// other owned entry from its own group's hooks array, whether it's a
|
|
364
|
+
// duplicate in another group or piled up alongside the keeper in the same
|
|
365
|
+
// group. Never delete a whole group, since it could carry unrelated
|
|
366
|
+
// hand-added hooks.
|
|
367
|
+
const keeper = owned[0];
|
|
368
|
+
let changed = false;
|
|
369
|
+
if (keeper.hook.command !== command) {
|
|
370
|
+
keeper.hook.command = command;
|
|
371
|
+
changed = true;
|
|
372
|
+
}
|
|
373
|
+
if (keeper.hook[HOOK_MARKER_JSON] !== true) {
|
|
374
|
+
keeper.hook[HOOK_MARKER_JSON] = true;
|
|
375
|
+
changed = true;
|
|
376
|
+
}
|
|
377
|
+
for (const dupe of owned.slice(1)) {
|
|
378
|
+
const list = dupe.group.hooks;
|
|
379
|
+
const idx = list.indexOf(dupe.hook);
|
|
380
|
+
if (idx !== -1)
|
|
381
|
+
list.splice(idx, 1);
|
|
382
|
+
changed = true;
|
|
383
|
+
}
|
|
384
|
+
// Only clear the keeper's group matcher when that group is exclusively
|
|
385
|
+
// ours (no unrelated hooks left in it after dedup) — never when it also
|
|
386
|
+
// carries an unrelated hand-added hook.
|
|
387
|
+
const keeperList = keeper.group.hooks;
|
|
388
|
+
if (keeperList.length === 1 && "matcher" in keeper.group) {
|
|
389
|
+
delete keeper.group.matcher;
|
|
390
|
+
changed = true;
|
|
391
|
+
}
|
|
392
|
+
// Drop any group left with zero hooks; never drop one that still has
|
|
393
|
+
// unrelated hooks in it.
|
|
394
|
+
const filtered = groups.filter((group) => {
|
|
395
|
+
if (!group || typeof group !== "object")
|
|
396
|
+
return true;
|
|
397
|
+
const g = group;
|
|
398
|
+
return !Array.isArray(g.hooks) || g.hooks.length > 0;
|
|
399
|
+
});
|
|
400
|
+
hooks[eventKey] = filtered;
|
|
401
|
+
return { changed, hadManaged: true };
|
|
375
402
|
}
|
|
376
403
|
/**
|
|
377
404
|
* Remove all agent-memory-managed hook entries for `eventKey`. Used when
|
|
378
405
|
* downgrading from per-turn back to stable (drops UserPromptSubmit).
|
|
406
|
+
* `command`, when given, also matches legacy entries that predate
|
|
407
|
+
* HOOK_MARKER_JSON — the same broadened detection upsertClaudeHookGroup and
|
|
408
|
+
* hasClaudeHookGroup use — so a pre-marker install isn't left behind after a
|
|
409
|
+
* downgrade/uninstall that reports success.
|
|
379
410
|
* Returns true if anything was removed.
|
|
380
411
|
*/
|
|
381
|
-
function removeClaudeHookGroup(hooks, eventKey) {
|
|
412
|
+
function removeClaudeHookGroup(hooks, eventKey, command) {
|
|
382
413
|
const groups = Array.isArray(hooks[eventKey]) ? hooks[eventKey] : [];
|
|
383
414
|
if (groups.length === 0)
|
|
384
415
|
return false;
|
|
@@ -390,7 +421,8 @@ function removeClaudeHookGroup(hooks, eventKey) {
|
|
|
390
421
|
const g = { ...group };
|
|
391
422
|
const list = Array.isArray(g.hooks) ? g.hooks : [];
|
|
392
423
|
const kept = list.filter((h) => {
|
|
393
|
-
const
|
|
424
|
+
const hook = h && typeof h === "object" ? h : null;
|
|
425
|
+
const isOurs = !!hook && (hook[HOOK_MARKER_JSON] === true || (!!command && hook.command === command));
|
|
394
426
|
if (isOurs)
|
|
395
427
|
removed++;
|
|
396
428
|
return !isOurs;
|
|
@@ -426,7 +458,7 @@ function installClaudeCodeHook(homeDir, mode = "per-turn") {
|
|
|
426
458
|
promptHadManaged = prompt.hadManaged;
|
|
427
459
|
}
|
|
428
460
|
else {
|
|
429
|
-
promptChanged = removeClaudeHookGroup(hooks, "UserPromptSubmit");
|
|
461
|
+
promptChanged = removeClaudeHookGroup(hooks, "UserPromptSubmit", userPromptSubmitHookCommand("claude"));
|
|
430
462
|
}
|
|
431
463
|
// Stop backs the write side of memory with a periodic nudge. It is orthogonal
|
|
432
464
|
// to stable/per-turn context injection, so it is installed unconditionally.
|
|
@@ -779,9 +811,9 @@ function uninstallClaudeCodeHook(homeDir) {
|
|
|
779
811
|
}
|
|
780
812
|
const settings = readJsonConfig(settingsPath);
|
|
781
813
|
const hooks = settings.hooks ?? {};
|
|
782
|
-
const sessionRemoved = removeClaudeHookGroup(hooks, "SessionStart");
|
|
783
|
-
const promptRemoved = removeClaudeHookGroup(hooks, "UserPromptSubmit");
|
|
784
|
-
const stopRemoved = removeClaudeHookGroup(hooks, "Stop");
|
|
814
|
+
const sessionRemoved = removeClaudeHookGroup(hooks, "SessionStart", sessionStartHookCommand("claude"));
|
|
815
|
+
const promptRemoved = removeClaudeHookGroup(hooks, "UserPromptSubmit", userPromptSubmitHookCommand("claude"));
|
|
816
|
+
const stopRemoved = removeClaudeHookGroup(hooks, "Stop", stopHookCommand("claude"));
|
|
785
817
|
const legacyPreCompactRemoved = removeClaudeHookGroup(hooks, "PreCompact");
|
|
786
818
|
if (!sessionRemoved && !promptRemoved && !stopRemoved && !legacyPreCompactRemoved) {
|
|
787
819
|
return { key: "claude", label: "Claude Code", installed: false, reason: "not installed" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myagentmemory",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "agentmemory (agent-memory) is persistent memory for coding agents (Claude Code, OpenAI Codex, Cursor, Agent) with qmd-powered semantic search across daily logs, long-term memory, and scratchpad",
|
|
5
5
|
"main": "./dist/core.js",
|
|
6
6
|
"types": "./dist/core.d.ts",
|
package/src/hooks.ts
CHANGED
|
@@ -156,7 +156,13 @@ function hasClaudeHookGroup(homeDir: string, eventKey: string, command: string):
|
|
|
156
156
|
for (const hook of list) {
|
|
157
157
|
if (!hook || typeof hook !== "object") continue;
|
|
158
158
|
const h = hook as Record<string, unknown>;
|
|
159
|
-
|
|
159
|
+
// Match by command alone (marker is optional) — legacy entries that
|
|
160
|
+
// predate HOOK_MARKER_JSON already run this exact command, and the
|
|
161
|
+
// command string is specific enough to agent-memory that it's a safe
|
|
162
|
+
// signal on its own. Otherwise a pre-marker install is invisible here,
|
|
163
|
+
// doctor/isHookInstalled falsely report "not installed" for hooks that
|
|
164
|
+
// are actually live, and upsertClaudeHookGroup can't find them to dedupe.
|
|
165
|
+
if (h.command === command) return true;
|
|
160
166
|
}
|
|
161
167
|
}
|
|
162
168
|
return false;
|
|
@@ -388,55 +394,81 @@ function upsertClaudeHookGroup(
|
|
|
388
394
|
command: string,
|
|
389
395
|
): { changed: boolean; hadManaged: boolean } {
|
|
390
396
|
const groups = Array.isArray(hooks[eventKey]) ? [...(hooks[eventKey] as unknown[])] : [];
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
397
|
+
|
|
398
|
+
// Locate every agent-memory-owned hook entry — matched by marker, or by
|
|
399
|
+
// exact command string for legacy pre-marker installs — wherever it lives.
|
|
400
|
+
const owned: Array<{ group: Record<string, unknown>; hook: Record<string, unknown> }> = [];
|
|
401
|
+
for (const group of groups) {
|
|
394
402
|
if (!group || typeof group !== "object") continue;
|
|
395
403
|
const g = group as Record<string, unknown>;
|
|
396
404
|
const list = Array.isArray(g.hooks) ? (g.hooks as unknown[]) : [];
|
|
397
405
|
for (const hook of list) {
|
|
398
406
|
if (!hook || typeof hook !== "object") continue;
|
|
399
407
|
const h = hook as Record<string, unknown>;
|
|
400
|
-
if (h[HOOK_MARKER_JSON] === true) {
|
|
401
|
-
managedIndexes.push(i);
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
408
|
+
if (h[HOOK_MARKER_JSON] === true || h.command === command) owned.push({ group: g, hook: h });
|
|
404
409
|
}
|
|
405
410
|
}
|
|
406
411
|
|
|
407
|
-
if (
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
for (const idx of dupes.reverse()) groups.splice(idx, 1);
|
|
411
|
-
const group = groups[keep] as Record<string, unknown>;
|
|
412
|
-
let changed = dupes.length > 0;
|
|
413
|
-
if ("matcher" in group) {
|
|
414
|
-
delete group.matcher;
|
|
415
|
-
changed = true;
|
|
416
|
-
}
|
|
417
|
-
const list = group.hooks as Record<string, unknown>[];
|
|
418
|
-
for (const h of list) {
|
|
419
|
-
if (h[HOOK_MARKER_JSON] === true && h.command !== command) {
|
|
420
|
-
h.command = command;
|
|
421
|
-
changed = true;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
412
|
+
if (owned.length === 0) {
|
|
413
|
+
// No existing managed hook anywhere — add a fresh group without a matcher so it fires on all harnesses.
|
|
414
|
+
groups.push({ hooks: [{ type: "command", command, [HOOK_MARKER_JSON]: true }] });
|
|
424
415
|
hooks[eventKey] = groups;
|
|
425
|
-
return { changed, hadManaged:
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
hooks
|
|
431
|
-
|
|
416
|
+
return { changed: true, hadManaged: false };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Keep the first owned entry in place — fixed up in place, preserving its
|
|
420
|
+
// identity so a no-op re-install reports unchanged — and remove every
|
|
421
|
+
// other owned entry from its own group's hooks array, whether it's a
|
|
422
|
+
// duplicate in another group or piled up alongside the keeper in the same
|
|
423
|
+
// group. Never delete a whole group, since it could carry unrelated
|
|
424
|
+
// hand-added hooks.
|
|
425
|
+
const keeper = owned[0];
|
|
426
|
+
let changed = false;
|
|
427
|
+
if (keeper.hook.command !== command) {
|
|
428
|
+
keeper.hook.command = command;
|
|
429
|
+
changed = true;
|
|
430
|
+
}
|
|
431
|
+
if (keeper.hook[HOOK_MARKER_JSON] !== true) {
|
|
432
|
+
keeper.hook[HOOK_MARKER_JSON] = true;
|
|
433
|
+
changed = true;
|
|
434
|
+
}
|
|
435
|
+
for (const dupe of owned.slice(1)) {
|
|
436
|
+
const list = dupe.group.hooks as unknown[];
|
|
437
|
+
const idx = list.indexOf(dupe.hook);
|
|
438
|
+
if (idx !== -1) list.splice(idx, 1);
|
|
439
|
+
changed = true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Only clear the keeper's group matcher when that group is exclusively
|
|
443
|
+
// ours (no unrelated hooks left in it after dedup) — never when it also
|
|
444
|
+
// carries an unrelated hand-added hook.
|
|
445
|
+
const keeperList = keeper.group.hooks as unknown[];
|
|
446
|
+
if (keeperList.length === 1 && "matcher" in keeper.group) {
|
|
447
|
+
delete keeper.group.matcher;
|
|
448
|
+
changed = true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Drop any group left with zero hooks; never drop one that still has
|
|
452
|
+
// unrelated hooks in it.
|
|
453
|
+
const filtered = groups.filter((group) => {
|
|
454
|
+
if (!group || typeof group !== "object") return true;
|
|
455
|
+
const g = group as Record<string, unknown>;
|
|
456
|
+
return !Array.isArray(g.hooks) || (g.hooks as unknown[]).length > 0;
|
|
457
|
+
});
|
|
458
|
+
hooks[eventKey] = filtered;
|
|
459
|
+
return { changed, hadManaged: true };
|
|
432
460
|
}
|
|
433
461
|
|
|
434
462
|
/**
|
|
435
463
|
* Remove all agent-memory-managed hook entries for `eventKey`. Used when
|
|
436
464
|
* downgrading from per-turn back to stable (drops UserPromptSubmit).
|
|
465
|
+
* `command`, when given, also matches legacy entries that predate
|
|
466
|
+
* HOOK_MARKER_JSON — the same broadened detection upsertClaudeHookGroup and
|
|
467
|
+
* hasClaudeHookGroup use — so a pre-marker install isn't left behind after a
|
|
468
|
+
* downgrade/uninstall that reports success.
|
|
437
469
|
* Returns true if anything was removed.
|
|
438
470
|
*/
|
|
439
|
-
function removeClaudeHookGroup(hooks: Record<string, unknown>, eventKey: string): boolean {
|
|
471
|
+
function removeClaudeHookGroup(hooks: Record<string, unknown>, eventKey: string, command?: string): boolean {
|
|
440
472
|
const groups = Array.isArray(hooks[eventKey]) ? (hooks[eventKey] as unknown[]) : [];
|
|
441
473
|
if (groups.length === 0) return false;
|
|
442
474
|
let removed = 0;
|
|
@@ -446,7 +478,8 @@ function removeClaudeHookGroup(hooks: Record<string, unknown>, eventKey: string)
|
|
|
446
478
|
const g = { ...(group as Record<string, unknown>) };
|
|
447
479
|
const list = Array.isArray(g.hooks) ? (g.hooks as unknown[]) : [];
|
|
448
480
|
const kept = list.filter((h) => {
|
|
449
|
-
const
|
|
481
|
+
const hook = h && typeof h === "object" ? (h as Record<string, unknown>) : null;
|
|
482
|
+
const isOurs = !!hook && (hook[HOOK_MARKER_JSON] === true || (!!command && hook.command === command));
|
|
450
483
|
if (isOurs) removed++;
|
|
451
484
|
return !isOurs;
|
|
452
485
|
});
|
|
@@ -478,7 +511,7 @@ function installClaudeCodeHook(homeDir: string, mode: HookMode = "per-turn"): Ho
|
|
|
478
511
|
promptChanged = prompt.changed;
|
|
479
512
|
promptHadManaged = prompt.hadManaged;
|
|
480
513
|
} else {
|
|
481
|
-
promptChanged = removeClaudeHookGroup(hooks, "UserPromptSubmit");
|
|
514
|
+
promptChanged = removeClaudeHookGroup(hooks, "UserPromptSubmit", userPromptSubmitHookCommand("claude"));
|
|
482
515
|
}
|
|
483
516
|
// Stop backs the write side of memory with a periodic nudge. It is orthogonal
|
|
484
517
|
// to stable/per-turn context injection, so it is installed unconditionally.
|
|
@@ -848,9 +881,9 @@ function uninstallClaudeCodeHook(homeDir: string): HookInstallResult {
|
|
|
848
881
|
}
|
|
849
882
|
const settings = readJsonConfig(settingsPath);
|
|
850
883
|
const hooks = (settings.hooks as Record<string, unknown>) ?? {};
|
|
851
|
-
const sessionRemoved = removeClaudeHookGroup(hooks, "SessionStart");
|
|
852
|
-
const promptRemoved = removeClaudeHookGroup(hooks, "UserPromptSubmit");
|
|
853
|
-
const stopRemoved = removeClaudeHookGroup(hooks, "Stop");
|
|
884
|
+
const sessionRemoved = removeClaudeHookGroup(hooks, "SessionStart", sessionStartHookCommand("claude"));
|
|
885
|
+
const promptRemoved = removeClaudeHookGroup(hooks, "UserPromptSubmit", userPromptSubmitHookCommand("claude"));
|
|
886
|
+
const stopRemoved = removeClaudeHookGroup(hooks, "Stop", stopHookCommand("claude"));
|
|
854
887
|
const legacyPreCompactRemoved = removeClaudeHookGroup(hooks, "PreCompact");
|
|
855
888
|
if (!sessionRemoved && !promptRemoved && !stopRemoved && !legacyPreCompactRemoved) {
|
|
856
889
|
return { key: "claude", label: "Claude Code", installed: false, reason: "not installed" };
|