claude-mem-lite 3.96.0 → 3.97.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 (143) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/adopt-cli.mjs +125 -51
  4. package/bash-utils.mjs +105 -29
  5. package/claudemd.mjs +63 -19
  6. package/cli/activity.mjs +36 -18
  7. package/cli/common.mjs +101 -18
  8. package/cli/doctor.mjs +29 -9
  9. package/cli.mjs +69 -10
  10. package/deep-search.mjs +63 -20
  11. package/format-utils.mjs +15 -7
  12. package/haiku-client.mjs +189 -111
  13. package/hash-utils.mjs +15 -6
  14. package/hook-context.mjs +135 -46
  15. package/hook-episode.mjs +76 -23
  16. package/hook-handoff.mjs +216 -84
  17. package/hook-llm.mjs +476 -216
  18. package/hook-memory.mjs +106 -43
  19. package/hook-optimize.mjs +272 -109
  20. package/hook-precompact.mjs +1 -1
  21. package/hook-semaphore.mjs +46 -12
  22. package/hook-shared.mjs +101 -131
  23. package/hook-update.mjs +286 -88
  24. package/hook.mjs +1207 -617
  25. package/install-metadata.mjs +246 -125
  26. package/install.mjs +1327 -917
  27. package/lib/activity.mjs +52 -29
  28. package/lib/atomic-write.mjs +23 -4
  29. package/lib/binding-probe.mjs +40 -38
  30. package/lib/browse-core.mjs +22 -8
  31. package/lib/citation-tracker.mjs +189 -79
  32. package/lib/cite-back-hint.mjs +37 -19
  33. package/lib/cite-recall-path.mjs +3 -1
  34. package/lib/cli-flags.mjs +4 -2
  35. package/lib/cli-project.mjs +8 -5
  36. package/lib/compress-core.mjs +34 -9
  37. package/lib/cooldown-path.mjs +3 -1
  38. package/lib/db-backup.mjs +33 -11
  39. package/lib/deferred-work.mjs +73 -32
  40. package/lib/delete-core.mjs +25 -9
  41. package/lib/doctor-benchmark.mjs +5 -1
  42. package/lib/doctor-drift.mjs +25 -8
  43. package/lib/edge-attribution.mjs +23 -9
  44. package/lib/env-number.mjs +7 -8
  45. package/lib/err-sampler.mjs +22 -11
  46. package/lib/error-recall-core.mjs +15 -17
  47. package/lib/events-injection.mjs +25 -8
  48. package/lib/export-columns.mjs +49 -10
  49. package/lib/fast-summary.mjs +22 -9
  50. package/lib/file-edge-match.mjs +4 -2
  51. package/lib/file-intel.mjs +38 -13
  52. package/lib/frontmatter.mjs +22 -5
  53. package/lib/get-core.mjs +78 -11
  54. package/lib/handoff-constants.mjs +18 -0
  55. package/lib/hook-stdin.mjs +10 -3
  56. package/lib/hook-stdout.mjs +21 -7
  57. package/lib/hook-telemetry.mjs +44 -17
  58. package/lib/id-routing.mjs +11 -6
  59. package/lib/import-jsonl.mjs +92 -41
  60. package/lib/inject-search-core.mjs +5 -2
  61. package/lib/injected-ids.mjs +19 -12
  62. package/lib/install-shape.mjs +47 -23
  63. package/lib/keyctx-marker.mjs +3 -1
  64. package/lib/lesson-bridge.mjs +8 -5
  65. package/lib/llm-call.mjs +76 -0
  66. package/lib/llm-provider-probe.mjs +24 -12
  67. package/lib/low-signal-patterns.mjs +25 -22
  68. package/lib/maintain-core.mjs +221 -92
  69. package/lib/mem-override.mjs +4 -2
  70. package/lib/metrics.mjs +30 -25
  71. package/lib/native-binding-hint.mjs +29 -8
  72. package/lib/observation-write.mjs +74 -19
  73. package/lib/patha-exclude-meter.mjs +10 -6
  74. package/lib/persist-reminder.mjs +22 -10
  75. package/lib/plan-reader.mjs +2 -2
  76. package/lib/private-strip.mjs +3 -3
  77. package/lib/proc-lock.mjs +15 -3
  78. package/lib/proxy-fetch.mjs +49 -13
  79. package/lib/quiet-scope.mjs +42 -0
  80. package/lib/recall-core.mjs +19 -7
  81. package/lib/recent-core.mjs +18 -5
  82. package/lib/registry-core.mjs +12 -6
  83. package/lib/release-digest.mjs +4 -1
  84. package/lib/relevance-floor.mjs +5 -2
  85. package/lib/reread-guard.mjs +16 -8
  86. package/lib/resolve-data-dir.mjs +1 -1
  87. package/lib/rrf.mjs +4 -1
  88. package/lib/save-enrich.mjs +36 -18
  89. package/lib/save-observation.mjs +78 -35
  90. package/lib/scrub-record.mjs +20 -6
  91. package/lib/search-core.mjs +456 -123
  92. package/lib/shard-gc.mjs +50 -0
  93. package/lib/startup-dashboard.mjs +20 -9
  94. package/lib/stats-core.mjs +101 -35
  95. package/lib/stats-quality.mjs +56 -20
  96. package/lib/summary-extractor.mjs +2 -2
  97. package/lib/task-imperative.mjs +12 -4
  98. package/lib/timeline-core.mjs +88 -28
  99. package/lib/tmp-fixture-sweep.mjs +32 -9
  100. package/lib/transcript-scan.mjs +20 -4
  101. package/lib/upgrade-banner.mjs +11 -7
  102. package/mem-cli.mjs +1314 -506
  103. package/memdir.mjs +53 -21
  104. package/nlp.mjs +68 -38
  105. package/npm-shrinkwrap.json +2 -2
  106. package/package.json +9 -2
  107. package/plugin-cache-guard.mjs +31 -15
  108. package/project-utils.mjs +44 -20
  109. package/registry-enricher.mjs +26 -6
  110. package/registry-github.mjs +4 -2
  111. package/registry-importer.mjs +151 -40
  112. package/registry-recommend.mjs +189 -96
  113. package/registry-retriever.mjs +229 -70
  114. package/registry-scanner.mjs +11 -6
  115. package/registry.mjs +163 -66
  116. package/rerank.mjs +18 -4
  117. package/resource-discovery.mjs +14 -4
  118. package/schema.mjs +258 -99
  119. package/scoring-sql.mjs +21 -10
  120. package/scripts/binding-probe-cli.mjs +17 -12
  121. package/scripts/hook-launcher.mjs +148 -31
  122. package/scripts/launch-preflight.mjs +5 -7
  123. package/scripts/launch.mjs +21 -9
  124. package/scripts/post-tool-recall.js +21 -5
  125. package/scripts/pre-agent-inject.js +51 -13
  126. package/scripts/pre-skill-bridge.js +22 -7
  127. package/scripts/pre-tool-recall.js +157 -77
  128. package/scripts/prompt-search-utils.mjs +70 -25
  129. package/scripts/setup.sh +16 -1
  130. package/scripts/user-prompt-search.js +238 -97
  131. package/search-engine.mjs +329 -96
  132. package/search-scoring.mjs +75 -33
  133. package/secret-scrub.mjs +48 -12
  134. package/server/fts-check.mjs +12 -9
  135. package/server.mjs +674 -243
  136. package/skip-tools.mjs +19 -8
  137. package/source-files.mjs +54 -21
  138. package/stop-words.mjs +152 -19
  139. package/synonyms.mjs +579 -164
  140. package/tfidf.mjs +130 -40
  141. package/tier.mjs +1 -1
  142. package/tool-schemas.mjs +381 -131
  143. package/utils.mjs +184 -36
package/claudemd.mjs CHANGED
@@ -24,20 +24,34 @@ import { memdirPath, removePluginSection, removePluginDoc, isAdopted as memdirIs
24
24
 
25
25
  // ─── Path helpers ────────────────────────────────────────────────────────────
26
26
 
27
- function slugSnake(slug) { return String(slug).replace(/[^a-zA-Z0-9]/g, '_'); }
27
+ function slugSnake(slug) {
28
+ return String(slug).replace(/[^a-zA-Z0-9]/g, '_');
29
+ }
28
30
 
29
- export function claudeMdPath(cwd) { return join(cwd, 'CLAUDE.md'); }
30
- function dotClaudeDir(cwd) { return join(cwd, '.claude'); }
31
- export function detailDocPath(cwd, slug) { return join(dotClaudeDir(cwd), `plugin_${slugSnake(slug)}.md`); }
32
- function stateFilePath(cwd, slug) { return join(dotClaudeDir(cwd), `.plugin_${slugSnake(slug)}_state.json`); }
31
+ export function claudeMdPath(cwd) {
32
+ return join(cwd, 'CLAUDE.md');
33
+ }
34
+ function dotClaudeDir(cwd) {
35
+ return join(cwd, '.claude');
36
+ }
37
+ export function detailDocPath(cwd, slug) {
38
+ return join(dotClaudeDir(cwd), `plugin_${slugSnake(slug)}.md`);
39
+ }
40
+ function stateFilePath(cwd, slug) {
41
+ return join(dotClaudeDir(cwd), `.plugin_${slugSnake(slug)}_state.json`);
42
+ }
33
43
 
34
44
  // First line of the detail doc — an invisible (in rendered markdown) marker that
35
45
  // lets us distinguish our generated copy from a user's same-named file.
36
- function managedByMarker(slug) { return `<!-- managed-by: ${slug} -->`; }
46
+ function managedByMarker(slug) {
47
+ return `<!-- managed-by: ${slug} -->`;
48
+ }
37
49
 
38
50
  // ─── Sentinel rendering & parsing ────────────────────────────────────────────
39
51
 
40
- function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
52
+ function escapeRe(s) {
53
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
54
+ }
41
55
 
42
56
  // Slug-scoped so stripping our block never disturbs another plugin's block
43
57
  // (e.g. code-graph-mcp's `<!-- code-graph-mcp:begin -->`) sitting in the same
@@ -45,15 +59,23 @@ function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
45
59
  // are `\r?\n` (not bare `\n`) so a CLAUDE.md re-saved with Windows CRLF endings
46
60
  // still matches — otherwise the block read as "absent" and a fresh LF copy got
47
61
  // appended every SessionStart, growing the file without bound (review C1/H2).
48
- function blockBody(esc) { return `<!-- ${esc}:begin (v\\d+) -->\\r?\\n([\\s\\S]*?)\\r?\\n<!-- ${esc}:end -->`; }
49
- function blockRegex(slug) { return new RegExp(blockBody(escapeRe(slug))); }
50
- function blockRegexG(slug) { return new RegExp(blockBody(escapeRe(slug)), 'g'); }
62
+ function blockBody(esc) {
63
+ return `<!-- ${esc}:begin (v\\d+) -->\\r?\\n([\\s\\S]*?)\\r?\\n<!-- ${esc}:end -->`;
64
+ }
65
+ function blockRegex(slug) {
66
+ return new RegExp(blockBody(escapeRe(slug)));
67
+ }
68
+ function blockRegexG(slug) {
69
+ return new RegExp(blockBody(escapeRe(slug)), 'g');
70
+ }
51
71
 
52
72
  function renderBlock(slug, version, body) {
53
73
  return `<!-- ${slug}:begin ${version} -->\n${body}\n<!-- ${slug}:end -->`;
54
74
  }
55
75
 
56
- function sha256(s) { return createHash('sha256').update(s).digest('hex'); }
76
+ function sha256(s) {
77
+ return createHash('sha256').update(s).digest('hex');
78
+ }
57
79
 
58
80
  // `atomicWriteFileSync`, not a local temp+rename (audit 2026-09-02 P0-5). The local twin
59
81
  // renamed onto the PATH; when a project's CLAUDE.md is a symlink into a dotfiles repo
@@ -71,7 +93,12 @@ function writeState(cwd, slug, state) {
71
93
 
72
94
  function clearState(cwd, slug) {
73
95
  const p = stateFilePath(cwd, slug);
74
- if (existsSync(p)) try { unlinkSync(p); } catch { /* best-effort */ }
96
+ if (existsSync(p))
97
+ try {
98
+ unlinkSync(p);
99
+ } catch {
100
+ /* best-effort */
101
+ }
75
102
  }
76
103
 
77
104
  // ─── Public API ──────────────────────────────────────────────────────────────
@@ -110,9 +137,11 @@ export function isAdopted(cwd, slug) {
110
137
  * removed it. removeManaged cleans all three pieces, so sweep on any of them.
111
138
  */
112
139
  export function hasResidue(cwd, slug) {
113
- return readBlock(cwd, slug).body !== null
114
- || existsSync(detailDocPath(cwd, slug))
115
- || existsSync(stateFilePath(cwd, slug));
140
+ return (
141
+ readBlock(cwd, slug).body !== null ||
142
+ existsSync(detailDocPath(cwd, slug)) ||
143
+ existsSync(stateFilePath(cwd, slug))
144
+ );
116
145
  }
117
146
 
118
147
  /**
@@ -131,7 +160,11 @@ export function needsRefresh(cwd, { slug, version, block, doc }) {
131
160
  const dp = detailDocPath(cwd, slug);
132
161
  if (!existsSync(dp)) return true;
133
162
  let cur;
134
- try { cur = readFileSync(dp, 'utf8'); } catch { return true; }
163
+ try {
164
+ cur = readFileSync(dp, 'utf8');
165
+ } catch {
166
+ return true;
167
+ }
135
168
  return cur !== `${managedByMarker(slug)}\n${doc}`;
136
169
  }
137
170
 
@@ -227,21 +260,32 @@ export function removeManaged(cwd, slug) {
227
260
  // unadopt fully restores the pre-adopt state — mirrors the emptied-.claude/
228
261
  // cleanup below ("unadopt leaves no trace").
229
262
  if (raw.trim() === '') {
230
- try { unlinkSync(p); } catch { atomicWrite(p, raw); }
263
+ try {
264
+ unlinkSync(p);
265
+ } catch {
266
+ atomicWrite(p, raw);
267
+ }
231
268
  } else {
232
269
  atomicWrite(p, raw);
233
270
  }
234
271
  }
235
272
  }
236
273
  const dp = detailDocPath(cwd, slug);
237
- if (existsSync(dp)) try { unlinkSync(dp); } catch { /* best-effort */ }
274
+ if (existsSync(dp))
275
+ try {
276
+ unlinkSync(dp);
277
+ } catch {
278
+ /* best-effort */
279
+ }
238
280
  clearState(cwd, slug);
239
281
  // Drop an emptied .claude/ so unadopt leaves no trace (skips if it holds
240
282
  // anything else — e.g. settings.local.json).
241
283
  try {
242
284
  const dir = dotClaudeDir(cwd);
243
285
  if (existsSync(dir) && readdirSync(dir).length === 0) rmdirSync(dir);
244
- } catch { /* best-effort */ }
286
+ } catch {
287
+ /* best-effort */
288
+ }
245
289
  return { action };
246
290
  }
247
291
 
package/cli/activity.mjs CHANGED
@@ -16,7 +16,7 @@ import { parseIntFlag, isNumericToken } from '../lib/cli-flags.mjs';
16
16
 
17
17
  function formatActivityResults(rows) {
18
18
  if (!rows || rows.length === 0) return '(no events)';
19
- return rows.map(r => `#${r.id} [${r.event_type}] ${r.title}`).join('\n');
19
+ return rows.map((r) => `#${r.id} [${r.event_type}] ${r.title}`).join('\n');
20
20
  }
21
21
 
22
22
  export async function cmdActivity(db, args) {
@@ -27,7 +27,8 @@ export async function cmdActivity(db, args) {
27
27
  }
28
28
 
29
29
  const { positional, flags } = parseArgs(args.slice(1));
30
- const { saveEvent, searchEvents, recentEvents, getEvent, EVENT_TYPES, promoteInsightEvents } = await import('../lib/activity.mjs');
30
+ const { saveEvent, searchEvents, recentEvents, getEvent, EVENT_TYPES, promoteInsightEvents } =
31
+ await import('../lib/activity.mjs');
31
32
  const VALID_EVENT_TYPES = new Set(EVENT_TYPES);
32
33
  // `save` CREATES a row, so it keeps plain inferProject(): the DB-aware fallback is a read
33
34
  // affordance, and applying it to a write absorbs a not-yet-born subproject's first event
@@ -35,7 +36,9 @@ export async function cmdActivity(db, args) {
35
36
  // operates on rows that already exist, where falling back is what finds them.
36
37
  const project = flags.project
37
38
  ? resolveProject(db, flags.project)
38
- : (sub === 'save' ? inferProject() : cliProject(db));
39
+ : sub === 'save'
40
+ ? inferProject()
41
+ : cliProject(db);
39
42
 
40
43
  if (sub === 'save') {
41
44
  // Reject value-less string flags before they reach saveEvent as a boolean `true`
@@ -54,16 +57,23 @@ export async function cmdActivity(db, args) {
54
57
  const body = flags.body || null;
55
58
  // Accept both --file (singular, backward compat) and --files (plural,
56
59
  // comma-split, preferred — matches cmdSave). Merge both sources.
57
- const filesFromPlural = flags.files && typeof flags.files === 'string'
58
- ? flags.files.split(',').map(s => s.trim()).filter(Boolean)
59
- : [];
60
+ const filesFromPlural =
61
+ flags.files && typeof flags.files === 'string'
62
+ ? flags.files
63
+ .split(',')
64
+ .map((s) => s.trim())
65
+ .filter(Boolean)
66
+ : [];
60
67
  const filesFromSingular = flags.file && typeof flags.file === 'string' ? [flags.file] : [];
61
68
  const file_paths_merged = [...filesFromSingular, ...filesFromPlural];
62
69
  const file_paths = file_paths_merged.length > 0 ? file_paths_merged : null;
63
70
  const rawImp = flags.importance !== undefined ? parseInt(flags.importance, 10) : 2;
64
71
  // isNumericToken first (mirrors cmdSave): bare parseInt coerces "3xyz"→3 and would
65
72
  // persist a wrong importance that silently skews ranking. Float literals truncate (#8277).
66
- if (flags.importance !== undefined && (!isNumericToken(flags.importance) || isNaN(rawImp) || rawImp < 1 || rawImp > 3)) {
73
+ if (
74
+ flags.importance !== undefined &&
75
+ (!isNumericToken(flags.importance) || isNaN(rawImp) || rawImp < 1 || rawImp > 3)
76
+ ) {
67
77
  fail(`[mem] Invalid importance "${flags.importance}". Must be 1, 2, or 3.`);
68
78
  return;
69
79
  }
@@ -101,9 +111,10 @@ export async function cmdActivity(db, args) {
101
111
  // through parseIntFlag so garbage ("2abc"), negatives (SQLite LIMIT -1 = UNLIMITED
102
112
  // full-table dump), and uncapped huge values warn + clamp to default/max, matching
103
113
  // the search/recent/browse siblings.
104
- const limit = positional.length > 0
105
- ? parseIntFlag(positional[0], { name: 'count', defaultValue: 20, max: 1000 })
106
- : parseIntFlag(flags.limit, { name: '--limit', defaultValue: 20, max: 1000 });
114
+ const limit =
115
+ positional.length > 0
116
+ ? parseIntFlag(positional[0], { name: 'count', defaultValue: 20, max: 1000 })
117
+ : parseIntFlag(flags.limit, { name: '--limit', defaultValue: 20, max: 1000 });
107
118
  const type = flags.type || null;
108
119
  if (type !== null && !VALID_EVENT_TYPES.has(type)) {
109
120
  fail(`[mem] activity recent: invalid --type "${type}". Valid: ${[...VALID_EVENT_TYPES].join(', ')}`);
@@ -142,18 +153,21 @@ export async function cmdActivity(db, args) {
142
153
  fail('[mem] Usage: claude-mem-lite activity delete <id1,id2,...> [--confirm]');
143
154
  return;
144
155
  }
145
- const ids = idStr.split(',')
146
- .map(s => s.trim())
156
+ const ids = idStr
157
+ .split(',')
158
+ .map((s) => s.trim())
147
159
  .filter(Boolean)
148
- .map(s => parseInt(s, 10))
149
- .filter(n => Number.isInteger(n) && n > 0);
160
+ .map((s) => parseInt(s, 10))
161
+ .filter((n) => Number.isInteger(n) && n > 0);
150
162
  if (ids.length === 0) {
151
163
  fail('[mem] activity delete: no valid IDs provided (must be positive integers)');
152
164
  return;
153
165
  }
154
166
 
155
167
  const placeholders = ids.map(() => '?').join(',');
156
- const rows = db.prepare(`SELECT id, event_type, title FROM events WHERE id IN (${placeholders})`).all(...ids);
168
+ const rows = db
169
+ .prepare(`SELECT id, event_type, title FROM events WHERE id IN (${placeholders})`)
170
+ .all(...ids);
157
171
  if (rows.length === 0) {
158
172
  fail(`[mem] activity delete: no events found for ID(s) ${ids.join(', ')}`);
159
173
  return;
@@ -166,7 +180,7 @@ export async function cmdActivity(db, args) {
166
180
  const titleStr = (r.title || '').slice(0, 100);
167
181
  out(` #${r.id} [${r.event_type}] ${titleStr}`);
168
182
  }
169
- const missingIds = ids.filter(i => !rows.some(r => r.id === i));
183
+ const missingIds = ids.filter((i) => !rows.some((r) => r.id === i));
170
184
  if (missingIds.length > 0) {
171
185
  out(`[mem] Note: ${missingIds.length} ID(s) not found and will be skipped: ${missingIds.join(', ')}`);
172
186
  }
@@ -192,11 +206,15 @@ export async function cmdActivity(db, args) {
192
206
  const execute = flags.execute === true || flags.execute === 'true';
193
207
  const r = promoteInsightEvents(db, { project: projectFilter, minImportance: minImp, execute });
194
208
  if (!execute) {
195
- out(`[mem] Preview: ${r.eligible} insight-bearing event(s) (body + importance>=${minImp})${projectFilter ? ` in ${projectFilter}` : ' across all projects'} would be promoted to searchable observations.`);
209
+ out(
210
+ `[mem] Preview: ${r.eligible} insight-bearing event(s) (body + importance>=${minImp})${projectFilter ? ` in ${projectFilter}` : ' across all projects'} would be promoted to searchable observations.`,
211
+ );
196
212
  out('[mem] Run with --execute to apply. Source events are kept (marked promoted).');
197
213
  return;
198
214
  }
199
- out(`[mem] Promoted ${r.promoted} event(s) to observations${r.deduped ? ` (${r.deduped} already had an equivalent observation)` : ''}.`);
215
+ out(
216
+ `[mem] Promoted ${r.promoted} event(s) to observations${r.deduped ? ` (${r.deduped} already had an equivalent observation)` : ''}.`,
217
+ );
200
218
  return;
201
219
  }
202
220
 
package/cli/common.mjs CHANGED
@@ -173,7 +173,7 @@ export function rejectBareStringFlags(flags, keys) {
173
173
  * @returns {string|null} Resolved value, or null after a conflict fail().
174
174
  */
175
175
  export function resolvePositionalAlias(positionalStr, flags, aliasKeys) {
176
- const given = aliasKeys.filter(k => typeof flags[k] === 'string' && flags[k].trim() !== '');
176
+ const given = aliasKeys.filter((k) => typeof flags[k] === 'string' && flags[k].trim() !== '');
177
177
  if (given.length > 1) {
178
178
  fail(`[mem] Both --${given[0]} and --${given[1]} provided — pass the value once.`);
179
179
  return null;
@@ -197,21 +197,99 @@ export function resolvePositionalAlias(positionalStr, flags, aliasKeys) {
197
197
  * them to a command — same maintenance contract as JSON_SUPPORTED_CMDS in mem-cli.
198
198
  */
199
199
  export const KNOWN_CLI_FLAGS = new Set([
200
- 'after', 'age-days', 'all', 'anchor', 'batch', 'before', 'benchmark', 'body', 'branch',
201
- 'capability-summary', 'category', 'closes-deferred', 'concepts', 'confirm', 'days', 'deep',
202
- 'detail', 'domain-tags', 'dry-run', 'enrich', 'execute', 'fields', 'file', 'files', 'floors',
203
- 'force', 'format', 'from', 'help', 'id', 'ids', 'content', 'importance', 'include-compressed', 'include-noise',
204
- 'intent-tags', 'invocation-name', 'json', 'key', 'keywords', 'lesson', 'lesson-learned', 'limit',
205
- 'local-path', 'margins', 'max', 'memdir', 'merge-ids', 'metrics', 'name', 'narrative', 'no-deep',
206
- 'offset', 'ops', 'or', 'priority', 'project', 'quality', 'query', 'reason', 'repo-url',
207
- 'rerank', 'resource-type', 'retain-days', 'retry', 'run', 'run-all', 'scope', 'session-audit',
208
- 'sidechain', 'since', 'sort', 'source', 'status', 'sweep', 'task', 'tech-stack', 'text', 'tier', 'title',
209
- 'to', 'trigger-patterns', 'type', 'use-cases', 'verbose',
200
+ 'after',
201
+ 'age-days',
202
+ 'all',
203
+ 'anchor',
204
+ 'batch',
205
+ 'before',
206
+ 'benchmark',
207
+ 'body',
208
+ 'branch',
209
+ 'capability-summary',
210
+ 'category',
211
+ 'closes-deferred',
212
+ 'concepts',
213
+ 'confirm',
214
+ 'days',
215
+ 'deep',
216
+ 'detail',
217
+ 'domain-tags',
218
+ 'dry-run',
219
+ 'enrich',
220
+ 'execute',
221
+ 'fields',
222
+ 'file',
223
+ 'files',
224
+ 'floors',
225
+ 'force',
226
+ 'format',
227
+ 'from',
228
+ 'help',
229
+ 'id',
230
+ 'ids',
231
+ 'content',
232
+ 'importance',
233
+ 'include-compressed',
234
+ 'include-noise',
235
+ 'intent-tags',
236
+ 'invocation-name',
237
+ 'json',
238
+ 'key',
239
+ 'keywords',
240
+ 'lesson',
241
+ 'lesson-learned',
242
+ 'limit',
243
+ 'local-path',
244
+ 'margins',
245
+ 'max',
246
+ 'memdir',
247
+ 'merge-ids',
248
+ 'metrics',
249
+ 'name',
250
+ 'narrative',
251
+ 'no-deep',
252
+ 'offset',
253
+ 'ops',
254
+ 'or',
255
+ 'priority',
256
+ 'project',
257
+ 'quality',
258
+ 'query',
259
+ 'reason',
260
+ 'repo-url',
261
+ 'rerank',
262
+ 'resource-type',
263
+ 'retain-days',
264
+ 'retry',
265
+ 'run',
266
+ 'run-all',
267
+ 'scope',
268
+ 'session-audit',
269
+ 'sidechain',
270
+ 'since',
271
+ 'sort',
272
+ 'source',
273
+ 'status',
274
+ 'sweep',
275
+ 'task',
276
+ 'tech-stack',
277
+ 'text',
278
+ 'tier',
279
+ 'title',
280
+ 'to',
281
+ 'trigger-patterns',
282
+ 'type',
283
+ 'use-cases',
284
+ 'verbose',
210
285
  // Catalogued 2026-08-13 when suggestUnknownFlags started reporting EVERY unknown
211
286
  // flag: these are real, code-read flags that the old edit-distance gate happened to
212
287
  // stay silent about (`adopt --disable/--enable`, `activity --min-importance`,
213
288
  // `save --supersedes`). Verified by running each command and checking for a warning.
214
- 'disable', 'enable', 'min-importance', 'supersedes',
289
+ 'disable',
290
+ 'enable',
291
+ 'min-importance',
292
+ 'supersedes',
215
293
  // `doctor --benchmark --prompts-limit N` — read off raw argv in cli/doctor.mjs, so
216
294
  // it never appeared in a `flags.x` grep. Caught by independent review after the
217
295
  // warn-on-every-unknown-flag flip turned the omission into a false warning on a
@@ -230,7 +308,8 @@ export const KNOWN_CLI_FLAGS = new Set([
230
308
 
231
309
  /** Levenshtein distance, early-exit past `max` (cheap enough for a handful of flags). */
232
310
  function editDistance(a, b, max = 2) {
233
- const m = a.length, n = b.length;
311
+ const m = a.length,
312
+ n = b.length;
234
313
  if (Math.abs(m - n) > max) return max + 1;
235
314
  let prev = Array.from({ length: n + 1 }, (_, j) => j);
236
315
  for (let i = 1; i <= m; i++) {
@@ -261,10 +340,14 @@ export function suggestUnknownFlags(flags) {
261
340
  const result = [];
262
341
  for (const key of Object.keys(flags)) {
263
342
  if (!key || KNOWN_CLI_FLAGS.has(key)) continue;
264
- let best = null, bestDist = 3;
343
+ let best = null,
344
+ bestDist = 3;
265
345
  for (const known of KNOWN_CLI_FLAGS) {
266
346
  const d = editDistance(key, known);
267
- if (d < bestDist) { bestDist = d; best = known; }
347
+ if (d < bestDist) {
348
+ bestDist = d;
349
+ best = known;
350
+ }
268
351
  }
269
352
  // Report EVERY unknown flag; the suggestion is a bonus when a near-miss exists.
270
353
  // Previously an unknown flag with no neighbour within distance 2 produced no
@@ -365,9 +448,9 @@ export { parseIdToken } from '../lib/id-routing.mjs';
365
448
  */
366
449
  export function formatProbeHints(probe) {
367
450
  const hints = [];
368
- if (probe.obs.length > 0) hints.push(`#${probe.obs.join(', #')} (obs)`);
451
+ if (probe.obs.length > 0) hints.push(`#${probe.obs.join(', #')} (obs)`);
369
452
  if (probe.session.length > 0) hints.push(`S#${probe.session.join(', S#')} (session)`);
370
- if (probe.prompt.length > 0) hints.push(`P#${probe.prompt.join(', P#')} (prompt)`);
371
- if (probe.event?.length > 0) hints.push(`E#${probe.event.join(', E#')} (event)`);
453
+ if (probe.prompt.length > 0) hints.push(`P#${probe.prompt.join(', P#')} (prompt)`);
454
+ if (probe.event?.length > 0) hints.push(`E#${probe.event.join(', E#')} (event)`);
372
455
  return hints;
373
456
  }
package/cli/doctor.mjs CHANGED
@@ -25,7 +25,9 @@ export async function cmdDoctor(db, args) {
25
25
  const parsed = parseInt(args[limitIdx + 1], 10);
26
26
  if (Number.isFinite(parsed) && parsed > 0 && parsed <= 1000) limit = parsed;
27
27
  }
28
- const rows = db.prepare(`
28
+ const rows = db
29
+ .prepare(
30
+ `
29
31
  SELECT p.prompt_text
30
32
  FROM user_prompts p
31
33
  JOIN sdk_sessions s ON p.content_session_id = s.content_session_id
@@ -34,9 +36,13 @@ export async function cmdDoctor(db, args) {
34
36
  AND length(p.prompt_text) >= 15
35
37
  ORDER BY p.created_at_epoch DESC
36
38
  LIMIT ?
37
- `).all(project, limit);
38
- prompts = rows.map(r => r.prompt_text).filter(Boolean);
39
- } catch { /* missing/empty tables on a fresh DB → leave prompts=[] */ }
39
+ `,
40
+ )
41
+ .all(project, limit);
42
+ prompts = rows.map((r) => r.prompt_text).filter(Boolean);
43
+ } catch {
44
+ /* missing/empty tables on a fresh DB → leave prompts=[] */
45
+ }
40
46
  const result = runBenchmark(db, { project, prompts });
41
47
  out(JSON.stringify(result, null, 2));
42
48
  return;
@@ -79,14 +85,28 @@ export async function cmdDoctor(db, args) {
79
85
  out(` obs_importance_null (NULL importance, P3-14): ${audit.obs_importance_null}`);
80
86
  if (audit.id_mix_other > 0 && audit.id_mix_uuid_shape === 0) {
81
87
  out('\n Notes:');
82
- out(' • id_mix_other > 0 with uuid_shape=0 is typically benign — usually means insertSession({id:\'X\'}) test scaffold or pre-v30 data with non-UUID equal values. Does NOT drive failure.');
88
+ out(
89
+ " • id_mix_other > 0 with uuid_shape=0 is typically benign — usually means insertSession({id:'X'}) test scaffold or pre-v30 data with non-UUID equal values. Does NOT drive failure.",
90
+ );
83
91
  }
84
92
  if (!audit.healthy) {
85
93
  out('\n Notes:');
86
- if (audit.id_mix_uuid_shape > 0) out(' • id_mix_uuid_shape > 0 — production v2.33.1 bug-pattern rows present. Investigate via SQL: SELECT * FROM sdk_sessions WHERE memory_session_id = content_session_id AND length(memory_session_id) = 36;');
87
- if (audit.missing_mem_id > 0) out(' • missing_mem_id rows are sessions whose mem-internal ID was never populated — likely SessionStart write that didn\'t reach Stop');
88
- if (audit.orphan_obs > 0) out(' • orphan_obs are observations referencing a sdk_sessions row that was deleted (FK CASCADE failed historically before v28)');
89
- if (audit.obs_importance_null > 0) out(' • obs_importance_null rows read as importance 1 by the CLI/hook maintenance pass (COALESCE) and as "skip" by the MCP idle pass (bare `importance <= 1`), so they decay on one face and not the other. New rows cannot reach this state (lib/observation-write.mjs coerces nullish to 1); these predate that or were written around it. Fix: UPDATE observations SET importance = 1 WHERE importance IS NULL;');
94
+ if (audit.id_mix_uuid_shape > 0)
95
+ out(
96
+ ' • id_mix_uuid_shape > 0 — production v2.33.1 bug-pattern rows present. Investigate via SQL: SELECT * FROM sdk_sessions WHERE memory_session_id = content_session_id AND length(memory_session_id) = 36;',
97
+ );
98
+ if (audit.missing_mem_id > 0)
99
+ out(
100
+ " • missing_mem_id rows are sessions whose mem-internal ID was never populated — likely SessionStart write that didn't reach Stop",
101
+ );
102
+ if (audit.orphan_obs > 0)
103
+ out(
104
+ ' • orphan_obs are observations referencing a sdk_sessions row that was deleted (FK CASCADE failed historically before v28)',
105
+ );
106
+ if (audit.obs_importance_null > 0)
107
+ out(
108
+ ' • obs_importance_null rows read as importance 1 by the CLI/hook maintenance pass (COALESCE) and as "skip" by the MCP idle pass (bare `importance <= 1`), so they decay on one face and not the other. New rows cannot reach this state (lib/observation-write.mjs coerces nullish to 1); these predate that or were written around it. Fix: UPDATE observations SET importance = 1 WHERE importance IS NULL;',
109
+ );
90
110
  }
91
111
  }
92
112
  if (!audit.healthy) process.exitCode = 1;
package/cli.mjs CHANGED
@@ -1,6 +1,46 @@
1
1
  #!/usr/bin/env node
2
- const CLI_COMMANDS = new Set(['search', 'recent', 'recall', 'get', 'timeline', 'save', 'stats', 'context', 'browse', 'citation-stats', 'delete', 'update', 'export', 'restore', 'compress', 'maintain', 'optimize', 'fts-check', 'registry', 'import', 'import-jsonl', 'enrich', 'activity', 'adopt', 'unadopt', 'memdir-audit', 'defer', 'help']);
3
- const INSTALL_COMMANDS = new Set(['install', 'uninstall', 'status', 'doctor', 'cleanup', 'cleanup-hooks', 'self-update', 'repair', 'rebuild-binding', 'release']);
2
+ const CLI_COMMANDS = new Set([
3
+ 'search',
4
+ 'recent',
5
+ 'recall',
6
+ 'get',
7
+ 'timeline',
8
+ 'save',
9
+ 'stats',
10
+ 'context',
11
+ 'browse',
12
+ 'citation-stats',
13
+ 'delete',
14
+ 'update',
15
+ 'export',
16
+ 'restore',
17
+ 'compress',
18
+ 'maintain',
19
+ 'optimize',
20
+ 'fts-check',
21
+ 'registry',
22
+ 'import',
23
+ 'import-jsonl',
24
+ 'enrich',
25
+ 'activity',
26
+ 'adopt',
27
+ 'unadopt',
28
+ 'memdir-audit',
29
+ 'defer',
30
+ 'help',
31
+ ]);
32
+ const INSTALL_COMMANDS = new Set([
33
+ 'install',
34
+ 'uninstall',
35
+ 'status',
36
+ 'doctor',
37
+ 'cleanup',
38
+ 'cleanup-hooks',
39
+ 'self-update',
40
+ 'repair',
41
+ 'rebuild-binding',
42
+ 'release',
43
+ ]);
4
44
 
5
45
  const cmd = process.argv[2];
6
46
 
@@ -17,7 +57,10 @@ if (cmd === '--version' || cmd === '-v' || cmd === '-V' || cmd === 'version') {
17
57
  } else if (cmd === '--help' || cmd === '-h') {
18
58
  const { run } = await import('./mem-cli.mjs');
19
59
  await run(['help']);
20
- } else if (cmd === 'doctor' && process.argv.slice(3).some(a => a === '--benchmark' || a === '--metrics' || a === '--session-audit')) {
60
+ } else if (
61
+ cmd === 'doctor' &&
62
+ process.argv.slice(3).some((a) => a === '--benchmark' || a === '--metrics' || a === '--session-audit')
63
+ ) {
21
64
  // Per #8217: the DB-layer doctor modes (--benchmark / --metrics / --session-audit,
22
65
  // each implemented in cli/doctor.mjs) route to mem-cli. Everything else — plain
23
66
  // `doctor`, `doctor --` (POSIX end-of-options), and `doctor --json` — stays with
@@ -56,19 +99,35 @@ if (cmd === '--version' || cmd === '-v' || cmd === '-V' || cmd === 'version') {
56
99
  process.stderr.write(`[mem] Unknown command: "${cmd}"\n`);
57
100
  // Suggest closest command by edit distance
58
101
  const allCmds = [...CLI_COMMANDS, ...INSTALL_COMMANDS];
59
- let best = null, bestDist = Infinity;
102
+ let best = null,
103
+ bestDist = Infinity;
60
104
  for (const c of allCmds) {
61
- const a = cmd.toLowerCase(), b = c;
62
- const m = a.length, n = b.length;
105
+ const a = cmd.toLowerCase(),
106
+ b = c;
107
+ const m = a.length,
108
+ n = b.length;
63
109
  if (Math.abs(m - n) > 2) continue;
64
- const d = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
65
- for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) d[i][j] = Math.min(d[i-1][j] + 1, d[i][j-1] + 1, d[i-1][j-1] + (a[i-1] !== b[j-1] ? 1 : 0));
66
- if (d[m][n] < bestDist) { bestDist = d[m][n]; best = c; }
110
+ const d = Array.from({ length: m + 1 }, (_, i) =>
111
+ Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
112
+ );
113
+ for (let i = 1; i <= m; i++)
114
+ for (let j = 1; j <= n; j++)
115
+ d[i][j] = Math.min(
116
+ d[i - 1][j] + 1,
117
+ d[i][j - 1] + 1,
118
+ d[i - 1][j - 1] + (a[i - 1] !== b[j - 1] ? 1 : 0),
119
+ );
120
+ if (d[m][n] < bestDist) {
121
+ bestDist = d[m][n];
122
+ best = c;
123
+ }
67
124
  }
68
125
  if (best && bestDist <= 2) {
69
126
  process.stderr.write(`[mem] Did you mean: ${best}?\n`);
70
127
  } else {
71
- process.stderr.write('[mem] Run "claude-mem-lite help" for CLI commands or "claude-mem-lite install" for setup\n');
128
+ process.stderr.write(
129
+ '[mem] Run "claude-mem-lite help" for CLI commands or "claude-mem-lite install" for setup\n',
130
+ );
72
131
  }
73
132
  process.exitCode = 1;
74
133
  }