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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/adopt-cli.mjs +125 -51
- package/bash-utils.mjs +105 -29
- package/claudemd.mjs +63 -19
- package/cli/activity.mjs +36 -18
- package/cli/common.mjs +101 -18
- package/cli/doctor.mjs +29 -9
- package/cli.mjs +69 -10
- package/deep-search.mjs +63 -20
- package/format-utils.mjs +15 -7
- package/haiku-client.mjs +189 -111
- package/hash-utils.mjs +15 -6
- package/hook-context.mjs +135 -46
- package/hook-episode.mjs +76 -23
- package/hook-handoff.mjs +216 -84
- package/hook-llm.mjs +476 -216
- package/hook-memory.mjs +106 -43
- package/hook-optimize.mjs +272 -109
- package/hook-precompact.mjs +1 -1
- package/hook-semaphore.mjs +46 -12
- package/hook-shared.mjs +101 -131
- package/hook-update.mjs +286 -88
- package/hook.mjs +1207 -617
- package/install-metadata.mjs +246 -125
- package/install.mjs +1327 -917
- package/lib/activity.mjs +52 -29
- package/lib/atomic-write.mjs +23 -4
- package/lib/binding-probe.mjs +40 -38
- package/lib/browse-core.mjs +22 -8
- package/lib/citation-tracker.mjs +189 -79
- package/lib/cite-back-hint.mjs +37 -19
- package/lib/cite-recall-path.mjs +3 -1
- package/lib/cli-flags.mjs +4 -2
- package/lib/cli-project.mjs +8 -5
- package/lib/compress-core.mjs +34 -9
- package/lib/cooldown-path.mjs +3 -1
- package/lib/db-backup.mjs +33 -11
- package/lib/deferred-work.mjs +73 -32
- package/lib/delete-core.mjs +25 -9
- package/lib/doctor-benchmark.mjs +5 -1
- package/lib/doctor-drift.mjs +25 -8
- package/lib/edge-attribution.mjs +23 -9
- package/lib/env-number.mjs +7 -8
- package/lib/err-sampler.mjs +22 -11
- package/lib/error-recall-core.mjs +15 -17
- package/lib/events-injection.mjs +25 -8
- package/lib/export-columns.mjs +49 -10
- package/lib/fast-summary.mjs +22 -9
- package/lib/file-edge-match.mjs +4 -2
- package/lib/file-intel.mjs +38 -13
- package/lib/frontmatter.mjs +22 -5
- package/lib/get-core.mjs +78 -11
- package/lib/handoff-constants.mjs +18 -0
- package/lib/hook-stdin.mjs +10 -3
- package/lib/hook-stdout.mjs +21 -7
- package/lib/hook-telemetry.mjs +44 -17
- package/lib/id-routing.mjs +11 -6
- package/lib/import-jsonl.mjs +92 -41
- package/lib/inject-search-core.mjs +5 -2
- package/lib/injected-ids.mjs +19 -12
- package/lib/install-shape.mjs +47 -23
- package/lib/keyctx-marker.mjs +3 -1
- package/lib/lesson-bridge.mjs +8 -5
- package/lib/llm-call.mjs +76 -0
- package/lib/llm-provider-probe.mjs +24 -12
- package/lib/low-signal-patterns.mjs +25 -22
- package/lib/maintain-core.mjs +221 -92
- package/lib/mem-override.mjs +4 -2
- package/lib/metrics.mjs +30 -25
- package/lib/native-binding-hint.mjs +29 -8
- package/lib/observation-write.mjs +74 -19
- package/lib/patha-exclude-meter.mjs +10 -6
- package/lib/persist-reminder.mjs +22 -10
- package/lib/plan-reader.mjs +2 -2
- package/lib/private-strip.mjs +3 -3
- package/lib/proc-lock.mjs +15 -3
- package/lib/proxy-fetch.mjs +49 -13
- package/lib/quiet-scope.mjs +42 -0
- package/lib/recall-core.mjs +19 -7
- package/lib/recent-core.mjs +18 -5
- package/lib/registry-core.mjs +12 -6
- package/lib/release-digest.mjs +4 -1
- package/lib/relevance-floor.mjs +5 -2
- package/lib/reread-guard.mjs +16 -8
- package/lib/resolve-data-dir.mjs +1 -1
- package/lib/rrf.mjs +4 -1
- package/lib/save-enrich.mjs +36 -18
- package/lib/save-observation.mjs +78 -35
- package/lib/scrub-record.mjs +20 -6
- package/lib/search-core.mjs +456 -123
- package/lib/shard-gc.mjs +50 -0
- package/lib/startup-dashboard.mjs +20 -9
- package/lib/stats-core.mjs +101 -35
- package/lib/stats-quality.mjs +56 -20
- package/lib/summary-extractor.mjs +2 -2
- package/lib/task-imperative.mjs +12 -4
- package/lib/timeline-core.mjs +88 -28
- package/lib/tmp-fixture-sweep.mjs +32 -9
- package/lib/transcript-scan.mjs +20 -4
- package/lib/upgrade-banner.mjs +11 -7
- package/mem-cli.mjs +1314 -506
- package/memdir.mjs +53 -21
- package/nlp.mjs +68 -38
- package/npm-shrinkwrap.json +2 -2
- package/package.json +9 -2
- package/plugin-cache-guard.mjs +31 -15
- package/project-utils.mjs +44 -20
- package/registry-enricher.mjs +26 -6
- package/registry-github.mjs +4 -2
- package/registry-importer.mjs +151 -40
- package/registry-recommend.mjs +189 -96
- package/registry-retriever.mjs +229 -70
- package/registry-scanner.mjs +11 -6
- package/registry.mjs +163 -66
- package/rerank.mjs +18 -4
- package/resource-discovery.mjs +14 -4
- package/schema.mjs +258 -99
- package/scoring-sql.mjs +21 -10
- package/scripts/binding-probe-cli.mjs +17 -12
- package/scripts/hook-launcher.mjs +148 -31
- package/scripts/launch-preflight.mjs +5 -7
- package/scripts/launch.mjs +21 -9
- package/scripts/post-tool-recall.js +21 -5
- package/scripts/pre-agent-inject.js +51 -13
- package/scripts/pre-skill-bridge.js +22 -7
- package/scripts/pre-tool-recall.js +157 -77
- package/scripts/prompt-search-utils.mjs +70 -25
- package/scripts/setup.sh +16 -1
- package/scripts/user-prompt-search.js +238 -97
- package/search-engine.mjs +329 -96
- package/search-scoring.mjs +75 -33
- package/secret-scrub.mjs +48 -12
- package/server/fts-check.mjs +12 -9
- package/server.mjs +674 -243
- package/skip-tools.mjs +19 -8
- package/source-files.mjs +54 -21
- package/stop-words.mjs +152 -19
- package/synonyms.mjs +579 -164
- package/tfidf.mjs +130 -40
- package/tier.mjs +1 -1
- package/tool-schemas.mjs +381 -131
- 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) {
|
|
27
|
+
function slugSnake(slug) {
|
|
28
|
+
return String(slug).replace(/[^a-zA-Z0-9]/g, '_');
|
|
29
|
+
}
|
|
28
30
|
|
|
29
|
-
export function claudeMdPath(cwd) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
function
|
|
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) {
|
|
46
|
+
function managedByMarker(slug) {
|
|
47
|
+
return `<!-- managed-by: ${slug} -->`;
|
|
48
|
+
}
|
|
37
49
|
|
|
38
50
|
// ─── Sentinel rendering & parsing ────────────────────────────────────────────
|
|
39
51
|
|
|
40
|
-
function escapeRe(s) {
|
|
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) {
|
|
49
|
-
|
|
50
|
-
|
|
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) {
|
|
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))
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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 {
|
|
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 {
|
|
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))
|
|
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 {
|
|
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 } =
|
|
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
|
-
:
|
|
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 =
|
|
58
|
-
|
|
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 (
|
|
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 =
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
146
|
-
.
|
|
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
|
|
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(
|
|
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(
|
|
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',
|
|
201
|
-
'
|
|
202
|
-
'
|
|
203
|
-
'
|
|
204
|
-
'
|
|
205
|
-
'
|
|
206
|
-
'
|
|
207
|
-
'
|
|
208
|
-
'
|
|
209
|
-
'
|
|
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',
|
|
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,
|
|
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,
|
|
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) {
|
|
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)
|
|
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)
|
|
371
|
-
if (probe.event?.length > 0)
|
|
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
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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(
|
|
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)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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([
|
|
3
|
-
|
|
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 (
|
|
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,
|
|
102
|
+
let best = null,
|
|
103
|
+
bestDist = Infinity;
|
|
60
104
|
for (const c of allCmds) {
|
|
61
|
-
const a = cmd.toLowerCase(),
|
|
62
|
-
|
|
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) =>
|
|
65
|
-
|
|
66
|
-
|
|
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(
|
|
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
|
}
|