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.
Files changed (3) hide show
  1. package/dist/hooks.js +70 -38
  2. package/package.json +1 -1
  3. 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
- if (h[HOOK_MARKER_JSON] === true && h.command === command)
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
- const managedIndexes = [];
334
- for (let i = 0; i < groups.length; i++) {
335
- const group = groups[i];
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
- managedIndexes.push(i);
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 (managedIndexes.length > 0) {
351
- const keep = managedIndexes[0];
352
- const dupes = managedIndexes.slice(1);
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: true };
370
- }
371
- // No existing managed group add a fresh one without a matcher so it fires on all harnesses.
372
- groups.push({ hooks: [{ type: "command", command, [HOOK_MARKER_JSON]: true }] });
373
- hooks[eventKey] = groups;
374
- return { changed: true, hadManaged: false };
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 isOurs = h && typeof h === "object" && h[HOOK_MARKER_JSON] === true;
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.4",
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
- if (h[HOOK_MARKER_JSON] === true && h.command === command) return true;
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
- const managedIndexes: number[] = [];
392
- for (let i = 0; i < groups.length; i++) {
393
- const group = groups[i];
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 (managedIndexes.length > 0) {
408
- const keep = managedIndexes[0];
409
- const dupes = managedIndexes.slice(1);
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: true };
426
- }
427
-
428
- // No existing managed group add a fresh one without a matcher so it fires on all harnesses.
429
- groups.push({ hooks: [{ type: "command", command, [HOOK_MARKER_JSON]: true }] });
430
- hooks[eventKey] = groups;
431
- return { changed: true, hadManaged: false };
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 isOurs = h && typeof h === "object" && (h as Record<string, unknown>)[HOOK_MARKER_JSON] === true;
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" };