claude-mem-lite 4.0.3 → 5.0.0

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 (49) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +0 -49
  4. package/README.zh-CN.md +0 -48
  5. package/adopt-content.mjs +0 -1
  6. package/cli/common.mjs +0 -17
  7. package/cli.mjs +19 -3
  8. package/commands/update.md +4 -11
  9. package/format-utils.mjs +0 -21
  10. package/hook-context.mjs +6 -1
  11. package/hook-handoff.mjs +9 -0
  12. package/hook-update.mjs +37 -12
  13. package/hook.mjs +5 -29
  14. package/hooks/hooks.json +0 -10
  15. package/install.mjs +24 -666
  16. package/lib/doctor-drift.mjs +0 -1
  17. package/lib/fast-summary.mjs +11 -0
  18. package/lib/frontmatter.mjs +1 -2
  19. package/lib/hook-prune.mjs +133 -0
  20. package/lib/hook-stdin.mjs +1 -1
  21. package/lib/hook-telemetry.mjs +1 -1
  22. package/lib/metrics.mjs +2 -2
  23. package/lib/shard-gc.mjs +11 -7
  24. package/mem-cli.mjs +2 -423
  25. package/nlp.mjs +1 -1
  26. package/npm-shrinkwrap.json +2 -2
  27. package/package.json +3 -15
  28. package/schema.mjs +0 -1
  29. package/scripts/hook-launcher.mjs +2 -2
  30. package/scripts/prompt-search-utils.mjs +0 -30
  31. package/scripts/user-prompt-search.js +3 -125
  32. package/server.mjs +11 -449
  33. package/source-files.mjs +11 -28
  34. package/synonyms.mjs +2 -1
  35. package/tool-schemas.mjs +0 -80
  36. package/utils.mjs +5 -13
  37. package/commands/tools.md +0 -67
  38. package/install-metadata.mjs +0 -2193
  39. package/lib/registry-core.mjs +0 -264
  40. package/registry/preinstalled.json +0 -2419
  41. package/registry-enricher.mjs +0 -124
  42. package/registry-github.mjs +0 -86
  43. package/registry-importer.mjs +0 -569
  44. package/registry-recommend.mjs +0 -503
  45. package/registry-retriever.mjs +0 -611
  46. package/registry-scanner.mjs +0 -261
  47. package/registry.mjs +0 -665
  48. package/resource-discovery.mjs +0 -199
  49. package/scripts/pre-skill-bridge.js +0 -146
@@ -103,7 +103,6 @@ export const HOOK_SCRIPT_ENTRY_POINTS = new Set([
103
103
  'user-prompt-search.js',
104
104
  'pre-tool-recall.js',
105
105
  'post-tool-recall.js',
106
- 'pre-skill-bridge.js',
107
106
  'pre-agent-inject.sh',
108
107
  'hook-launcher.mjs',
109
108
  ]);
@@ -41,6 +41,17 @@ export const FAST_SUMMARY_LIMITS = {
41
41
  /**
42
42
  * The two reads every fast summary is built from: the session's opening prompt, and the
43
43
  * titles of its most recent observations.
44
+ *
45
+ * The observation filter is `compressed_into` ONLY, deliberately — it is NOT a half-written
46
+ * `liveObsFilterSql` waiting to be completed. `completed` is the session's own history, and a
47
+ * lesson a later save overturned still happened; dropping it here would misreport the session
48
+ * that did the work. This is the same ruling audit 2026-08-14 F4 made for the sibling field
49
+ * `session_handoffs.completed`, where the scope guard lives in
50
+ * `tests/audit-silent-20260814.test.mjs`. Audit R8 §11.3 proposed adding `superseded_at IS
51
+ * NULL` to both; both were rejected on this reasoning. The filter that DOES belong on a
52
+ * superseded row is the one guarding standing policy re-presented to a later session
53
+ * (`key_decisions`), not history.
54
+ *
44
55
  * @returns {{request: string, completed: string}} raw (unscrubbed, untruncated) values
45
56
  */
46
57
  export function readFastSummarySource(db, sessionId) {
@@ -2,7 +2,6 @@
2
2
  //
3
3
  // Audit 2026-09-02 P1-16. There were three, and they were not all the same:
4
4
  //
5
- // registry-importer.mjs the shipped one, full
6
5
  // scripts/index-managed.mjs byte-identical to it apart from the `export` keyword —
7
6
  // 30 lines, the largest duplicate block in the tree
8
7
  // scripts/convert-commands.mjs a SIMPLIFIED cut with no `|` / `>` block support and no
@@ -13,7 +12,7 @@
13
12
  // scripts reading the same files with different parsers produce different registry rows
14
13
  // depending on which one last ran.
15
14
  //
16
- // Zero dependencies on purpose — it is imported by a shipped module (registry-importer)
15
+ // Zero dependencies on purpose — it is imported by a shipped module
17
16
  // and by two dev scripts, so it must not drag anything into either.
18
17
 
19
18
  /**
@@ -0,0 +1,133 @@
1
+ // lib/hook-prune.mjs — settings.json hook-entry classification and reconciliation.
2
+ //
3
+ // Extracted here because TWO faces need it and a direct import would close a cycle:
4
+ // `install.mjs` owns hook registration, and `hook-update.mjs` (which install.mjs
5
+ // already imports) has to reconcile after a swap. Per CLAUDE.md's rule — logic two
6
+ // faces share moves to `lib/`, the big file keeps only wiring — rather than
7
+ // documenting `install.mjs -> hook-update.mjs -> install.mjs` as an allowed cycle.
8
+ //
9
+ // Zero local imports on purpose: `hook-update.mjs` reaches this from the auto-update
10
+ // path, which must stay cheap and must not drag install.mjs's dependency graph in.
11
+
12
+ import { existsSync } from 'node:fs';
13
+ import { join, dirname } from 'node:path';
14
+
15
+ /**
16
+ * Identify a settings hook as one of OURS (to replace on install / strip on uninstall).
17
+ *
18
+ * Must be tight: the old `hook.mjs` + event-word test matched a user's OWN generic hook
19
+ * (`node ~/.config/hook.mjs session-start`) and install/uninstall silently deleted it.
20
+ * The launcher marker (`hook-launcher.mjs`, which every Node hook routes through since
21
+ * v2.84) replaces that clause; the product-name substring (our install-dir / legacy-direct
22
+ * hooks) and the bash prefilters round out the real markers.
23
+ */
24
+ export function isMemHook(cfg) {
25
+ // `cfg?.hooks`, not `cfg.hooks`: a null entry in a hand-edited settings.json threw a
26
+ // TypeError here, and hook-update.mjs swallows it — so ONE malformed entry silently
27
+ // cancelled the whole reconcile and the dangling hook survived. Degraded, never
28
+ // destructive, but the reconcile is the point. (Found by adversarial review; the same
29
+ // exposure predates this module in `configureHooks`, which now shares this function.)
30
+ if (!cfg?.hooks) return false;
31
+ return cfg.hooks.some((h) => {
32
+ const cmd = h.command || '';
33
+ return (
34
+ cmd.includes('claude-mem-lite') ||
35
+ cmd.includes('hook-launcher.mjs') ||
36
+ cmd.includes('scripts/post-tool-use.sh') ||
37
+ // Same reason post-tool-use.sh is named here: a bash prefilter routes through
38
+ // NO launcher, and the product-name clause only fires when the install dir
39
+ // happens to contain it — which CLAUDE_MEM_DIR can relocate. Without this line
40
+ // an Agent|Task hook in a relocated install survives uninstall and duplicates
41
+ // on reinstall (audit 2026-08-22 P2-5 added the second prefilter).
42
+ cmd.includes('scripts/pre-agent-inject.sh')
43
+ );
44
+ });
45
+ }
46
+
47
+ /**
48
+ * The launcher's ENTRY argument, resolved — or null when this command is not a
49
+ * launcher invocation we own.
50
+ *
51
+ * `nodeHook` writes `node "<INSTALL_DIR>/scripts/hook-launcher.mjs" scripts/<entry>.js`,
52
+ * and the launcher resolves that bare relative token against its own INSTALL_DIR
53
+ * (`hook-launcher.mjs:125`). Both halves matter for orphan detection: the quoted
54
+ * launcher path exists on a healthy install, so a scanner that stops at the first
55
+ * quoted token declares the entry healthy without ever looking at the file that
56
+ * actually runs. That is precisely how the `Skill` hook removal went unseen.
57
+ *
58
+ * Plugin-channel commands are excluded: `${CLAUDE_PLUGIN_ROOT}` is expanded by Claude
59
+ * Code, not by us, so nothing here can resolve them — and hooks/hooks.json owns them.
60
+ */
61
+ export function launcherEntryPath(cmd, installDir) {
62
+ if (typeof cmd !== 'string' || cmd.includes('${CLAUDE_PLUGIN_ROOT}')) return null;
63
+ if (!cmd.includes('hook-launcher.mjs')) return null;
64
+ // Everything after the quoted launcher path; the entry is its first bare token.
65
+ const tail = cmd.split(/hook-launcher\.mjs"?\s*/)[1];
66
+ if (!tail) return null;
67
+ const entry = tail.split(/\s+/).find((t) => t && !t.startsWith('-'));
68
+ if (!entry) return null;
69
+ if (entry.startsWith('/')) return entry;
70
+ // Resolve against the base the LAUNCHER NAMED IN THIS COMMAND would use
71
+ // (`hook-launcher.mjs:36` — `join(dirname(launcherFile), '..')`), not against the
72
+ // caller's idea of the install dir. The two coincide in production today because both
73
+ // INSTALL_DIR constants are homedir-hardcoded — which is exactly why the coupling is
74
+ // worth removing rather than documenting: the day a caller passes a different
75
+ // targetDir, resolving against it deletes hooks that run fine. `installDir` stays the
76
+ // fallback for a command whose launcher path we could not parse.
77
+ const quoted = cmd.match(/"([^"]*hook-launcher\.mjs)"/);
78
+ const base = quoted ? dirname(dirname(quoted[1])) : installDir;
79
+ return join(base, entry);
80
+ }
81
+
82
+ /**
83
+ * Drop settings.json hook entries whose launcher target no longer exists.
84
+ *
85
+ * Auto-update only ever handled hook ADDITIONS. `configureHooks()` strips stale mem
86
+ * entries and rewrites every event, but its only caller is `install()`;
87
+ * `hook-update.mjs` swaps `scripts/` wholesale, so an upgrade that REMOVES a hook
88
+ * script leaves settings.json pointing at a file it just deleted. The launcher then
89
+ * treats each fire as a broken install: two stderr lines and the self-heal marker,
90
+ * for a file that is never coming back. First hit by the `PreToolUse:Skill`
91
+ * removal (R9 review P1-1); plugin-channel installs are unaffected because
92
+ * hooks/hooks.json is replaced wholesale.
93
+ *
94
+ * Narrow by construction — an entry is dropped only when it is ours (`isMemHook`),
95
+ * routes through our launcher, and its resolved target is absent. An entry that
96
+ * cannot be parsed, or whose target exists, is left exactly as found; a foreign
97
+ * tool's dead hook is not ours to remove.
98
+ *
99
+ * Pure: returns a new settings object plus the entry tokens removed, so the caller
100
+ * decides whether to write and can log what changed.
101
+ *
102
+ * @returns {{settings: object, removed: string[]}}
103
+ */
104
+ export function pruneDanglingMemHooks(settings, installDir) {
105
+ if (!settings?.hooks) return { settings, removed: [] };
106
+ const removed = [];
107
+ const hooks = {};
108
+ for (const [event, configs] of Object.entries(settings.hooks)) {
109
+ if (!Array.isArray(configs)) {
110
+ hooks[event] = configs;
111
+ continue;
112
+ }
113
+ const kept = [];
114
+ for (const cfg of configs) {
115
+ if (!isMemHook(cfg)) {
116
+ kept.push(cfg);
117
+ continue;
118
+ }
119
+ const liveHooks = (cfg.hooks || []).filter((h) => {
120
+ const target = launcherEntryPath(h.command, installDir);
121
+ if (!target || existsSync(target)) return true;
122
+ // Record the token as written, not the resolved path — that is what a
123
+ // reader has to match against settings.json and the release manifest.
124
+ const tail = String(h.command).split(/hook-launcher\.mjs"?\s*/)[1] || '';
125
+ removed.push(tail.split(/\s+/).find((t) => t && !t.startsWith('-')) || target);
126
+ return false;
127
+ });
128
+ if (liveHooks.length > 0) kept.push({ ...cfg, hooks: liveHooks });
129
+ }
130
+ hooks[event] = kept;
131
+ }
132
+ return { settings: { ...settings, hooks }, removed };
133
+ }
@@ -8,7 +8,7 @@
8
8
  // (1.5 s / 262144 / never rejects).
9
9
  //
10
10
  // UNBOUNDED, three copies of `for await (const chunk of process.stdin) input += chunk`:
11
- // pre-tool-recall.js, pre-skill-bridge.js, post-tool-recall.js. No cap and no timeout.
11
+ // pre-tool-recall.js, post-tool-recall.js. No cap and no timeout.
12
12
  // That matters most on `PreToolUse:Write`, whose `tool_input.content` is the ENTIRE file
13
13
  // being written: writing a multi-megabyte file made pre-tool-recall buffer all of it and
14
14
  // `JSON.parse` all of it, to read `file_path`. The only bound was the host's own 3 s
@@ -113,7 +113,7 @@ export function recordHookError(scope, err, runtimeDir, ctx) {
113
113
  pruneOldShards(dir);
114
114
 
115
115
  // Every hook script funnels its failures through here — including the
116
- // STANDALONE ones (scripts/pre-tool-recall.js, scripts/pre-skill-bridge.js)
116
+ // STANDALONE ones (scripts/pre-tool-recall.js, scripts/post-tool-recall.js)
117
117
  // that never import hook.mjs and so never reach its dispatch catch. That gap
118
118
  // is why the 2026-08-13 outage stayed invisible: 78 of that day's 79 entries
119
119
  // were `pre-recall:db-open`, i.e. the ONE path whose errors nothing but this
package/lib/metrics.mjs CHANGED
@@ -83,8 +83,8 @@ export function timed(dbDir, event, fn, extra = {}) {
83
83
  * grows the dir unbounded. 90d keeps a full quarter for aggregate windows while
84
84
  * bounding the dir. Runs regardless of the enable flag so a user who toggles metrics
85
85
  * OFF still gets old shards cleaned. Best-effort, never throws — called from the
86
- * SessionStart GC sweep. The sweep itself is `lib/shard-gc.mjs`, shared with
87
- * registry-recommend's shadow log; this function is the metrics dir resolution.
86
+ * SessionStart GC sweep. The sweep itself is `lib/shard-gc.mjs`; this function is
87
+ * the metrics dir resolution.
88
88
  * @returns {number} shards removed
89
89
  */
90
90
  export function gcOldMetricShards(dbDir, retainDays = 90) {
package/lib/shard-gc.mjs CHANGED
@@ -1,12 +1,16 @@
1
1
  // lib/shard-gc.mjs — retention sweep for daily JSONL shard directories.
2
2
  //
3
- // Two sinks in this tree append one `YYYY-MM-DD.jsonl` per day with no GC of their
4
- // own — `lib/metrics.mjs` (CLAUDE_MEM_METRICS) and `registry-recommend.mjs` (the
5
- // shadow log) — so a long-lived install grows both dirs unbounded. The sweep was
6
- // written twice, line for line, differing only in how the directory is resolved;
7
- // audit 2026-09-05 P2-3 found it as the largest non-intentional cross-file duplicate.
8
- // That is the twin-drift class CLAUDE.md names: the retention rule (90 days, the
9
- // `YYYY-MM-DD.jsonl` name filter) had two homes and could disagree with itself.
3
+ // `lib/metrics.mjs` (CLAUDE_MEM_METRICS) appends one `YYYY-MM-DD.jsonl` per day with
4
+ // no GC of its own, so a long-lived install grows that dir unbounded.
5
+ //
6
+ // This module exists because there used to be TWO such sinks — the second was
7
+ // `registry-recommend.mjs`'s shadow log — and the sweep was written twice, line for
8
+ // line, differing only in how the directory is resolved; audit 2026-09-05 P2-3 found
9
+ // it as the largest non-intentional cross-file duplicate. That is the twin-drift class
10
+ // CLAUDE.md names: the retention rule (90 days, the `YYYY-MM-DD.jsonl` name filter) had
11
+ // two homes and could disagree with itself. The skill-registry subsystem was removed in
12
+ // 2026-09 (docs/audits/20260906-145304.md), leaving one caller; the extraction stays
13
+ // because the retention rule is worth owning in one place, not because of the caller count.
10
14
  //
11
15
  // Directory resolution stays with each caller — it is the only part that differed.
12
16