claude-mem-lite 2.24.2 → 2.26.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/.mcp.json +0 -0
- package/LICENSE +0 -0
- package/README.md +0 -0
- package/README.zh-CN.md +0 -0
- package/bash-utils.mjs +0 -0
- package/cli.mjs +16 -1
- package/commands/mem.md +0 -0
- package/commands/memory.md +0 -0
- package/commands/recall.md +0 -0
- package/commands/recent.md +0 -0
- package/commands/search.md +0 -0
- package/commands/timeline.md +0 -0
- package/commands/tools.md +0 -0
- package/commands/update.md +0 -0
- package/format-utils.mjs +0 -0
- package/haiku-client.mjs +0 -0
- package/hash-utils.mjs +0 -0
- package/hook-context.mjs +0 -0
- package/hook-episode.mjs +0 -1
- package/hook-handoff.mjs +0 -0
- package/hook-llm.mjs +5 -4
- package/hook-memory.mjs +0 -0
- package/hook-semaphore.mjs +0 -0
- package/hook-shared.mjs +0 -0
- package/hook-update.mjs +0 -0
- package/hook.mjs +98 -36
- package/hooks/hooks.json +12 -0
- package/install-metadata.mjs +0 -0
- package/install.mjs +13 -8
- package/mem-cli.mjs +40 -7
- package/nlp.mjs +0 -0
- package/package.json +2 -1
- package/project-utils.mjs +0 -0
- package/registry/preinstalled.json +0 -0
- package/registry-indexer.mjs +0 -0
- package/registry-retriever.mjs +0 -0
- package/registry-scanner.mjs +0 -0
- package/registry.mjs +0 -0
- package/resource-discovery.mjs +0 -0
- package/schema.mjs +0 -0
- package/scoring-sql.mjs +0 -0
- package/scripts/launch.mjs +0 -0
- package/scripts/pre-tool-recall.js +126 -0
- package/scripts/prompt-search-utils.mjs +0 -0
- package/scripts/user-prompt-search.js +0 -0
- package/secret-scrub.mjs +0 -0
- package/server-internals.mjs +0 -0
- package/server.mjs +2 -1
- package/skill.md +0 -0
- package/skip-tools.mjs +0 -0
- package/stop-words.mjs +0 -0
- package/synonyms.mjs +0 -0
- package/tfidf.mjs +0 -0
- package/tier.mjs +0 -0
- package/tool-schemas.mjs +0 -0
- package/utils.mjs +0 -0
package/.mcp.json
CHANGED
|
File without changes
|
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/README.zh-CN.md
CHANGED
|
File without changes
|
package/bash-utils.mjs
CHANGED
|
File without changes
|
package/cli.mjs
CHANGED
|
@@ -21,6 +21,21 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
21
21
|
await main(process.argv.slice(2));
|
|
22
22
|
} else {
|
|
23
23
|
process.stderr.write(`[mem] Unknown command: "${cmd}"\n`);
|
|
24
|
-
|
|
24
|
+
// Suggest closest command by edit distance
|
|
25
|
+
const allCmds = [...CLI_COMMANDS, ...INSTALL_COMMANDS];
|
|
26
|
+
let best = null, bestDist = Infinity;
|
|
27
|
+
for (const c of allCmds) {
|
|
28
|
+
const a = cmd.toLowerCase(), b = c;
|
|
29
|
+
const m = a.length, n = b.length;
|
|
30
|
+
if (Math.abs(m - n) > 2) continue;
|
|
31
|
+
const d = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
|
|
32
|
+
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));
|
|
33
|
+
if (d[m][n] < bestDist) { bestDist = d[m][n]; best = c; }
|
|
34
|
+
}
|
|
35
|
+
if (best && bestDist <= 2) {
|
|
36
|
+
process.stderr.write(`[mem] Did you mean: ${best}?\n`);
|
|
37
|
+
} else {
|
|
38
|
+
process.stderr.write('[mem] Run "claude-mem-lite help" for CLI commands or "claude-mem-lite install" for setup\n');
|
|
39
|
+
}
|
|
25
40
|
process.exitCode = 1;
|
|
26
41
|
}
|
package/commands/mem.md
CHANGED
|
File without changes
|
package/commands/memory.md
CHANGED
|
File without changes
|
package/commands/recall.md
CHANGED
|
File without changes
|
package/commands/recent.md
CHANGED
|
File without changes
|
package/commands/search.md
CHANGED
|
File without changes
|
package/commands/timeline.md
CHANGED
|
File without changes
|
package/commands/tools.md
CHANGED
|
File without changes
|
package/commands/update.md
CHANGED
|
File without changes
|
package/format-utils.mjs
CHANGED
|
File without changes
|
package/haiku-client.mjs
CHANGED
|
File without changes
|
package/hash-utils.mjs
CHANGED
|
File without changes
|
package/hook-context.mjs
CHANGED
|
File without changes
|
package/hook-episode.mjs
CHANGED
package/hook-handoff.mjs
CHANGED
|
File without changes
|
package/hook-llm.mjs
CHANGED
|
@@ -63,7 +63,7 @@ export function saveObservation(obs, projectOverride, sessionIdOverride, externa
|
|
|
63
63
|
// "Error in X", "Modified X" titles are low-specificity → use longer dedup window
|
|
64
64
|
// 7-day exact match prevents cross-day accumulation of "Modified package.json" noise;
|
|
65
65
|
// 3-day Jaccard catches near-duplicates without blocking legitimately new observations
|
|
66
|
-
const LOW_SIGNAL = /^(Error (while working|in)|Modified |Worked on |Reviewed \d+ files
|
|
66
|
+
const LOW_SIGNAL = /^(Error (while working|in)|Error: |Modified |Worked on |Reviewed \d+ files:|# |node |npm |npx |\(no description\)|\(error\)$)/;
|
|
67
67
|
if (obs.title && LOW_SIGNAL.test(obs.title)) {
|
|
68
68
|
const sevenDaysAgo = now.getTime() - 7 * 86400000;
|
|
69
69
|
const threeDaysAgo = now.getTime() - 3 * 86400000;
|
|
@@ -264,9 +264,10 @@ export function buildDegradedTitle(episode) {
|
|
|
264
264
|
if (hasEdit) return `Modified ${names}${suffix}`;
|
|
265
265
|
return `Worked on ${names}${suffix}`;
|
|
266
266
|
}
|
|
267
|
-
// No files: strip raw JSON
|
|
267
|
+
// No files: strip raw output (JSON, arrays, long tails) from Bash descriptions
|
|
268
268
|
const desc = episode.entries[0]?.desc || '(no description)';
|
|
269
|
-
return desc.replace(/ → (?:ERROR: )
|
|
269
|
+
return desc.replace(/ → (?:ERROR: )?[\[{].*$/, hasError ? ' (error)' : '')
|
|
270
|
+
.replace(/ → .*---EXIT:\d+$/, hasError ? ' (error)' : '');
|
|
270
271
|
}
|
|
271
272
|
|
|
272
273
|
/**
|
|
@@ -300,7 +301,7 @@ export function buildImmediateObservation(episode) {
|
|
|
300
301
|
const ruleImportance = computeRuleImportance(episode);
|
|
301
302
|
// Low-signal degraded titles ("Error in...", "Modified...") should not inflate importance.
|
|
302
303
|
// Cap at 1 unless rule-based signals indicate genuine importance (error-in-test → 3, config → 2).
|
|
303
|
-
const LOW_SIGNAL = /^(Error (while working|in)|Modified |Worked on |Reviewed \d+ files
|
|
304
|
+
const LOW_SIGNAL = /^(Error (while working|in)|Error: |Modified |Worked on |Reviewed \d+ files:|# |node |npm |npx |\(no description\)|\(error\)$)/;
|
|
304
305
|
const isLowSignal = LOW_SIGNAL.test(title);
|
|
305
306
|
let importance;
|
|
306
307
|
if (isReviewPattern) {
|
package/hook-memory.mjs
CHANGED
|
File without changes
|
package/hook-semaphore.mjs
CHANGED
|
File without changes
|
package/hook-shared.mjs
CHANGED
|
File without changes
|
package/hook-update.mjs
CHANGED
|
File without changes
|
package/hook.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
spawnBackground,
|
|
29
29
|
} from './hook-shared.mjs';
|
|
30
30
|
import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObservation } from './hook-llm.mjs';
|
|
31
|
-
import { searchRelevantMemories
|
|
31
|
+
import { searchRelevantMemories } from './hook-memory.mjs';
|
|
32
32
|
import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
33
33
|
import { checkForUpdate } from './hook-update.mjs';
|
|
34
34
|
import { SKIP_TOOLS, SKIP_PREFIXES } from './skip-tools.mjs';
|
|
@@ -225,28 +225,7 @@ async function handlePostToolUse() {
|
|
|
225
225
|
episode.lastAt = Date.now();
|
|
226
226
|
addFileToEpisode(episode, files);
|
|
227
227
|
|
|
228
|
-
//
|
|
229
|
-
// Uses recallForFile for importance>=2 with lesson context
|
|
230
|
-
if (EDIT_TOOLS.has(tool_name) && files.length > 0) {
|
|
231
|
-
const d = getDb();
|
|
232
|
-
if (d) {
|
|
233
|
-
for (const f of files) {
|
|
234
|
-
if (episode.fileHistoryShown?.includes(f)) continue;
|
|
235
|
-
try {
|
|
236
|
-
const recalled = recallForFile(d, f, project);
|
|
237
|
-
if (recalled.length > 0) {
|
|
238
|
-
const hints = recalled.map(r => {
|
|
239
|
-
const lesson = r.lesson_learned ? ` | ${r.lesson_learned}` : '';
|
|
240
|
-
return ` #${r.id} [${r.type}] ${truncate(r.title, 60)}${lesson}`;
|
|
241
|
-
}).join('\n');
|
|
242
|
-
process.stdout.write(`[claude-mem-lite] History for ${basename(f)}:\n${hints}\n`);
|
|
243
|
-
}
|
|
244
|
-
} catch (e) { debugCatch(e, 'fileHistory'); }
|
|
245
|
-
if (!episode.fileHistoryShown) episode.fileHistoryShown = [];
|
|
246
|
-
episode.fileHistoryShown.push(f);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
228
|
+
// File history injection moved to PreToolUse hook (scripts/pre-tool-recall.js)
|
|
250
229
|
|
|
251
230
|
writeEpisode(episode);
|
|
252
231
|
|
|
@@ -466,7 +445,7 @@ async function handleSessionStart() {
|
|
|
466
445
|
}
|
|
467
446
|
})();
|
|
468
447
|
|
|
469
|
-
// Auto-
|
|
448
|
+
// Auto-maintain: cleanup + decay + boost + purge, gated to once per 24h
|
|
470
449
|
const maintainFile = join(RUNTIME_DIR, 'last-auto-maintain.json');
|
|
471
450
|
let shouldMaintain = true;
|
|
472
451
|
try {
|
|
@@ -475,13 +454,69 @@ async function handleSessionStart() {
|
|
|
475
454
|
} catch {}
|
|
476
455
|
if (shouldMaintain) {
|
|
477
456
|
try {
|
|
457
|
+
const STALE_AGE = Date.now() - 30 * 86400000;
|
|
458
|
+
const OP_CAP = 500;
|
|
459
|
+
|
|
460
|
+
// Purge FIRST: delete entries already marked pending-purge from previous cycles (7-day retention)
|
|
461
|
+
// Must run before decay/idle-mark to avoid same-cycle delete of newly-marked entries
|
|
478
462
|
const purged = db.prepare(`
|
|
479
463
|
DELETE FROM observations WHERE compressed_into = ${COMPRESSED_PENDING_PURGE}
|
|
480
464
|
AND created_at_epoch < ?
|
|
481
465
|
`).run(Date.now() - 7 * 86400000);
|
|
482
|
-
if (purged.changes > 0) {
|
|
483
|
-
|
|
484
|
-
|
|
466
|
+
if (purged.changes > 0) debugLog('DEBUG', 'auto-maintain', `purged ${purged.changes} stale observations`);
|
|
467
|
+
|
|
468
|
+
// Cleanup: remove broken observations (no title AND no narrative)
|
|
469
|
+
const cleaned = db.prepare(`
|
|
470
|
+
DELETE FROM observations WHERE id IN (
|
|
471
|
+
SELECT id FROM observations
|
|
472
|
+
WHERE COALESCE(compressed_into, 0) = 0
|
|
473
|
+
AND (title IS NULL OR title = '') AND (narrative IS NULL OR narrative = '')
|
|
474
|
+
LIMIT ${OP_CAP}
|
|
475
|
+
)
|
|
476
|
+
`).run();
|
|
477
|
+
if (cleaned.changes > 0) debugLog('DEBUG', 'auto-maintain', `cleaned ${cleaned.changes} broken observations`);
|
|
478
|
+
|
|
479
|
+
// Decay: reduce importance of old, never-accessed observations
|
|
480
|
+
const decayed = db.prepare(`
|
|
481
|
+
UPDATE observations SET importance = MAX(1, COALESCE(importance, 1) - 1)
|
|
482
|
+
WHERE id IN (
|
|
483
|
+
SELECT id FROM observations
|
|
484
|
+
WHERE COALESCE(compressed_into, 0) = 0
|
|
485
|
+
AND COALESCE(importance, 1) > 1
|
|
486
|
+
AND COALESCE(access_count, 0) = 0
|
|
487
|
+
AND created_at_epoch < ?
|
|
488
|
+
LIMIT ${OP_CAP}
|
|
489
|
+
)
|
|
490
|
+
`).run(STALE_AGE);
|
|
491
|
+
if (decayed.changes > 0) debugLog('DEBUG', 'auto-maintain', `decayed ${decayed.changes} stale observations`);
|
|
492
|
+
|
|
493
|
+
// Mark idle: importance=1, never-accessed, old → pending-purge (will be purged next cycle)
|
|
494
|
+
const idleMarked = db.prepare(`
|
|
495
|
+
UPDATE observations SET compressed_into = ${COMPRESSED_PENDING_PURGE}
|
|
496
|
+
WHERE id IN (
|
|
497
|
+
SELECT id FROM observations
|
|
498
|
+
WHERE COALESCE(compressed_into, 0) = 0
|
|
499
|
+
AND COALESCE(importance, 1) = 1
|
|
500
|
+
AND COALESCE(access_count, 0) = 0
|
|
501
|
+
AND created_at_epoch < ?
|
|
502
|
+
LIMIT ${OP_CAP}
|
|
503
|
+
)
|
|
504
|
+
`).run(STALE_AGE);
|
|
505
|
+
if (idleMarked.changes > 0) debugLog('DEBUG', 'auto-maintain', `marked ${idleMarked.changes} idle as pending-purge`);
|
|
506
|
+
|
|
507
|
+
// Boost: increase importance of frequently-accessed observations
|
|
508
|
+
const boosted = db.prepare(`
|
|
509
|
+
UPDATE observations SET importance = MIN(3, COALESCE(importance, 1) + 1)
|
|
510
|
+
WHERE id IN (
|
|
511
|
+
SELECT id FROM observations
|
|
512
|
+
WHERE COALESCE(compressed_into, 0) = 0
|
|
513
|
+
AND COALESCE(access_count, 0) > 3
|
|
514
|
+
AND COALESCE(importance, 1) < 3
|
|
515
|
+
LIMIT ${OP_CAP}
|
|
516
|
+
)
|
|
517
|
+
`).run();
|
|
518
|
+
if (boosted.changes > 0) debugLog('DEBUG', 'auto-maintain', `boosted ${boosted.changes} frequently-accessed observations`);
|
|
519
|
+
|
|
485
520
|
// Mark maintenance as done (24h gate) — even though compression runs in background
|
|
486
521
|
writeFileSync(maintainFile, JSON.stringify({ epoch: Date.now() }));
|
|
487
522
|
// Weekly summary grouping runs in background to avoid blocking SessionStart
|
|
@@ -650,23 +685,50 @@ async function handleSessionStart() {
|
|
|
650
685
|
const summaryLines = buildSummaryLines(latestSummary);
|
|
651
686
|
|
|
652
687
|
// Key context: top high-importance observations for CLAUDE.md persistence
|
|
688
|
+
// Split into "File Lessons" (actionable, has lesson + file) and "Key Context" (informational)
|
|
653
689
|
const keyObs = db.prepare(`
|
|
654
|
-
SELECT id, type, title, lesson_learned FROM observations
|
|
655
|
-
WHERE project = ? AND COALESCE(compressed_into, 0) = 0
|
|
656
|
-
AND
|
|
657
|
-
|
|
690
|
+
SELECT o.id, o.type, o.title, o.lesson_learned, o.files_modified FROM observations o
|
|
691
|
+
WHERE o.project = ? AND COALESCE(o.compressed_into, 0) = 0
|
|
692
|
+
AND o.superseded_at IS NULL
|
|
693
|
+
AND COALESCE(o.importance, 1) >= 2
|
|
694
|
+
ORDER BY o.created_at_epoch DESC LIMIT 10
|
|
658
695
|
`).all(project);
|
|
696
|
+
|
|
659
697
|
if (keyObs.length > 0) {
|
|
660
|
-
|
|
698
|
+
const fileLessons = [];
|
|
699
|
+
const keyContext = [];
|
|
700
|
+
|
|
661
701
|
for (const o of keyObs) {
|
|
662
|
-
// Strip raw JSON output from degraded Bash-style titles
|
|
663
702
|
const clean = (o.title || '(untitled)')
|
|
664
703
|
.replace(/ → (?:ERROR: )?\{".*$/, '')
|
|
665
704
|
.replace(/ → (?:ERROR: )?\{[^}]*\.{3}$/, '');
|
|
666
|
-
const
|
|
667
|
-
|
|
705
|
+
const hasLesson = o.lesson_learned && o.lesson_learned.trim();
|
|
706
|
+
const hasFiles = o.files_modified && o.files_modified !== '[]';
|
|
707
|
+
|
|
708
|
+
if (hasLesson && hasFiles) {
|
|
709
|
+
try {
|
|
710
|
+
const files = JSON.parse(o.files_modified);
|
|
711
|
+
const fname = basename(Array.isArray(files) && files.length > 0 ? files[0] : '');
|
|
712
|
+
if (fname) {
|
|
713
|
+
fileLessons.push(`- ${fname}: ${truncate(o.lesson_learned, 100)} (#${o.id})`);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
} catch {}
|
|
717
|
+
}
|
|
718
|
+
const lesson = hasLesson ? ` — ${truncate(o.lesson_learned, 60)}` : '';
|
|
719
|
+
keyContext.push(`- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})${lesson}`);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (fileLessons.length > 0) {
|
|
723
|
+
summaryLines.push('### File Lessons');
|
|
724
|
+
summaryLines.push(...fileLessons.slice(0, 5));
|
|
725
|
+
summaryLines.push('');
|
|
726
|
+
}
|
|
727
|
+
if (keyContext.length > 0) {
|
|
728
|
+
summaryLines.push('### Key Context');
|
|
729
|
+
summaryLines.push(...keyContext.slice(0, 5));
|
|
730
|
+
summaryLines.push('');
|
|
668
731
|
}
|
|
669
|
-
summaryLines.push('');
|
|
670
732
|
} else if (!latestSummary) {
|
|
671
733
|
// Fallback: no summary AND no key observations — show recent activity
|
|
672
734
|
const recentObs = (observations.length >= 3 ? observations : fallbackObs).slice(0, 3);
|
package/hooks/hooks.json
CHANGED
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
]
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
|
+
"PreToolUse": [
|
|
22
|
+
{
|
|
23
|
+
"matcher": "Edit|Write|NotebookEdit",
|
|
24
|
+
"hooks": [
|
|
25
|
+
{
|
|
26
|
+
"type": "command",
|
|
27
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-recall.js\"",
|
|
28
|
+
"timeout": 3
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
21
33
|
"PostToolUse": [
|
|
22
34
|
{
|
|
23
35
|
"matcher": "*",
|
package/install-metadata.mjs
CHANGED
|
File without changes
|
package/install.mjs
CHANGED
|
@@ -452,20 +452,25 @@ async function install() {
|
|
|
452
452
|
]
|
|
453
453
|
};
|
|
454
454
|
|
|
455
|
+
const memPreToolUse = {
|
|
456
|
+
matcher: 'Edit|Write|NotebookEdit',
|
|
457
|
+
hooks: [
|
|
458
|
+
{
|
|
459
|
+
type: 'command',
|
|
460
|
+
command: `node "${join(SCRIPTS_PATH, 'pre-tool-recall.js')}"`,
|
|
461
|
+
timeout: 3
|
|
462
|
+
}
|
|
463
|
+
]
|
|
464
|
+
};
|
|
465
|
+
|
|
455
466
|
// Filter out existing mem hooks, then append fresh ones
|
|
456
|
-
for (const [event, config] of [['PostToolUse', memPostToolUse], ['SessionStart', memSessionStart], ['Stop', memStop], ['UserPromptSubmit', memUserPrompt]]) {
|
|
467
|
+
for (const [event, config] of [['PreToolUse', memPreToolUse], ['PostToolUse', memPostToolUse], ['SessionStart', memSessionStart], ['Stop', memStop], ['UserPromptSubmit', memUserPrompt]]) {
|
|
457
468
|
const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event].filter(cfg => !isMemHook(cfg)) : [];
|
|
458
469
|
settings.hooks[event] = [...existing, config];
|
|
459
470
|
}
|
|
460
471
|
|
|
461
|
-
// Clean up stale PreToolUse hook from previous versions
|
|
462
|
-
if (Array.isArray(settings.hooks.PreToolUse)) {
|
|
463
|
-
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(cfg => !isMemHook(cfg));
|
|
464
|
-
if (settings.hooks.PreToolUse.length === 0) delete settings.hooks.PreToolUse;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
472
|
writeSettings(settings);
|
|
468
|
-
ok('Hooks configured (PostToolUse, SessionStart, Stop, UserPromptSubmit)');
|
|
473
|
+
ok('Hooks configured (PreToolUse, PostToolUse, SessionStart, Stop, UserPromptSubmit)');
|
|
469
474
|
|
|
470
475
|
// 5. Migrate from old ~/.claude-mem/ if needed
|
|
471
476
|
if (existsSync(join(OLD_DATA_DIR, 'claude-mem.db')) && !existsSync(DB_PATH) && !existsSync(join(DATA_DIR, 'claude-mem.db'))) {
|
package/mem-cli.mjs
CHANGED
|
@@ -82,8 +82,14 @@ function cmdSearch(db, args) {
|
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
const
|
|
85
|
+
const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
|
|
86
|
+
const limit = Number.isInteger(rawLimit) ? Math.max(1, rawLimit) : 20;
|
|
86
87
|
const type = flags.type || null;
|
|
88
|
+
const validObsTypes = new Set(['decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change']);
|
|
89
|
+
if (type && !validObsTypes.has(type)) {
|
|
90
|
+
fail(`[mem] Invalid --type "${type}". Valid: ${[...validObsTypes].join(', ')}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
87
93
|
const source = flags.source || null; // observations|sessions|prompts (null = all)
|
|
88
94
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
89
95
|
const dateFrom = flags.from ? new Date(flags.from).getTime() : null;
|
|
@@ -91,10 +97,18 @@ function cmdSearch(db, args) {
|
|
|
91
97
|
if (dateTo && flags.to && /^\d{4}-\d{2}-\d{2}$/.test(flags.to)) dateTo += 86400000 - 1;
|
|
92
98
|
if (flags.from && isNaN(dateFrom)) { fail(`[mem] Invalid --from date: "${flags.from}". Use YYYY-MM-DD or ISO 8601.`); return; }
|
|
93
99
|
if (flags.to && isNaN(dateTo)) { fail(`[mem] Invalid --to date: "${flags.to}". Use YYYY-MM-DD or ISO 8601.`); return; }
|
|
94
|
-
const minImportance = flags.importance ? parseInt(flags.importance, 10) : null;
|
|
100
|
+
const minImportance = flags.importance !== undefined ? parseInt(flags.importance, 10) : null;
|
|
101
|
+
if (minImportance !== null && (isNaN(minImportance) || minImportance < 1 || minImportance > 3)) {
|
|
102
|
+
fail(`[mem] Invalid --importance "${flags.importance}". Must be 1, 2, or 3.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
95
105
|
const branch = flags.branch || null;
|
|
96
106
|
const offset = Math.max(0, parseInt(flags.offset, 10) || 0);
|
|
97
107
|
const tier = flags.tier || null;
|
|
108
|
+
if (tier && !['working', 'active', 'archive'].includes(tier)) {
|
|
109
|
+
fail(`[mem] Invalid --tier "${tier}". Use: working, active, archive`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
98
112
|
const sort = flags.sort || 'relevance';
|
|
99
113
|
if (!['relevance', 'time', 'importance'].includes(sort)) {
|
|
100
114
|
fail(`[mem] Invalid --sort "${sort}". Use: relevance, time, importance`);
|
|
@@ -305,7 +319,8 @@ function cmdSearch(db, args) {
|
|
|
305
319
|
}
|
|
306
320
|
|
|
307
321
|
const showTime = sort === 'time';
|
|
308
|
-
|
|
322
|
+
const hasMixed = paged.some(r => r._source === 'session' || r._source === 'prompt');
|
|
323
|
+
out(`[mem] ${paged.length} result${paged.length !== 1 ? 's' : ''} for "${query}":${hasMixed ? ' (# observation, S# session, P# prompt)' : ''}`);
|
|
309
324
|
for (const r of paged) {
|
|
310
325
|
const timeStr = showTime && r.created_at_epoch ? ` (${relativeTime(r.created_at_epoch)})` : '';
|
|
311
326
|
if (r._source === 'session') {
|
|
@@ -464,7 +479,8 @@ function cmdRecall(db, args) {
|
|
|
464
479
|
}
|
|
465
480
|
|
|
466
481
|
const filename = basename(file);
|
|
467
|
-
const
|
|
482
|
+
const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
|
|
483
|
+
const limit = Number.isInteger(rawLimit) ? Math.max(1, rawLimit) : 10;
|
|
468
484
|
|
|
469
485
|
// Search via observation_files junction table for indexed filename lookups
|
|
470
486
|
const escaped = filename.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
@@ -549,7 +565,19 @@ function cmdGet(db, args) {
|
|
|
549
565
|
|
|
550
566
|
// Default: observations (aligned with MCP mem_get)
|
|
551
567
|
const OBS_FIELDS = ['id', 'type', 'title', 'subtitle', 'narrative', 'text', 'facts', 'concepts', 'lesson_learned', 'search_aliases', 'files_read', 'files_modified', 'project', 'created_at', 'memory_session_id', 'prompt_number', 'importance', 'related_ids', 'access_count', 'branch', 'superseded_at', 'superseded_by', 'last_accessed_at'];
|
|
552
|
-
|
|
568
|
+
let requestedFields = null;
|
|
569
|
+
if (flags.fields) {
|
|
570
|
+
const allRequested = flags.fields.split(',').map(s => s.trim());
|
|
571
|
+
const invalid = allRequested.filter(f => !OBS_FIELDS.includes(f));
|
|
572
|
+
if (invalid.length > 0) {
|
|
573
|
+
process.stderr.write(`[mem] Unknown field(s): ${invalid.join(', ')}. Valid: ${OBS_FIELDS.join(', ')}\n`);
|
|
574
|
+
}
|
|
575
|
+
requestedFields = allRequested.filter(f => OBS_FIELDS.includes(f));
|
|
576
|
+
if (requestedFields.length === 0) {
|
|
577
|
+
fail('[mem] No valid fields specified');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
553
581
|
|
|
554
582
|
// Update access_count + auto-boost (aligned with MCP mem_get)
|
|
555
583
|
db.prepare(`UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id IN (${placeholders})`).run(Date.now(), ...ids);
|
|
@@ -616,6 +644,9 @@ function cmdTimeline(db, args) {
|
|
|
616
644
|
|
|
617
645
|
// No anchor: show most recent observations (aligned with MCP mem_timeline fallback)
|
|
618
646
|
if (!anchorId || isNaN(anchorId)) {
|
|
647
|
+
if (queryStr) {
|
|
648
|
+
process.stderr.write(`[mem] No anchor found for "${queryStr}", showing recent timeline\n`);
|
|
649
|
+
}
|
|
619
650
|
const compressedFilter = 'COALESCE(compressed_into, 0) = 0';
|
|
620
651
|
const projectFilter = project ? `WHERE ${compressedFilter} AND project = ?` : `WHERE ${compressedFilter}`;
|
|
621
652
|
const fallbackParams = project ? [project, before + after + 1] : [before + after + 1];
|
|
@@ -952,7 +983,8 @@ function cmdBrowse(db, args) {
|
|
|
952
983
|
fail(`[mem] Invalid tier: "${tierFilter}". Use: working, active, or archive`);
|
|
953
984
|
return;
|
|
954
985
|
}
|
|
955
|
-
const
|
|
986
|
+
const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
|
|
987
|
+
const limit = Number.isInteger(rawLimit) ? Math.max(1, rawLimit) : (tierFilter ? 20 : 5);
|
|
956
988
|
const now = Date.now();
|
|
957
989
|
|
|
958
990
|
const ctx = {
|
|
@@ -1185,7 +1217,8 @@ function cmdExport(db, args) {
|
|
|
1185
1217
|
wheres.push('created_at_epoch <= ?'); params.push(epoch);
|
|
1186
1218
|
}
|
|
1187
1219
|
|
|
1188
|
-
const
|
|
1220
|
+
const rawLimit = flags.limit !== undefined ? parseInt(flags.limit, 10) : NaN;
|
|
1221
|
+
const limit = Math.min(Number.isInteger(rawLimit) ? Math.max(1, rawLimit) : 200, 1000);
|
|
1189
1222
|
const format = flags.format || 'json';
|
|
1190
1223
|
if (!['json', 'jsonl'].includes(format)) {
|
|
1191
1224
|
fail(`[mem] Invalid format "${format}". Use: json or jsonl`);
|
package/nlp.mjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.26.0",
|
|
4
4
|
"description": "Lightweight persistent memory system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"scripts/setup.sh",
|
|
73
73
|
"scripts/post-tool-use.sh",
|
|
74
74
|
"scripts/user-prompt-search.js",
|
|
75
|
+
"scripts/pre-tool-recall.js",
|
|
75
76
|
"scripts/prompt-search-utils.mjs",
|
|
76
77
|
".mcp.json",
|
|
77
78
|
".claude-plugin/plugin.json",
|
package/project-utils.mjs
CHANGED
|
File without changes
|
|
File without changes
|
package/registry-indexer.mjs
CHANGED
|
File without changes
|
package/registry-retriever.mjs
CHANGED
|
File without changes
|
package/registry-scanner.mjs
CHANGED
|
File without changes
|
package/registry.mjs
CHANGED
|
File without changes
|
package/resource-discovery.mjs
CHANGED
|
File without changes
|
package/schema.mjs
CHANGED
|
File without changes
|
package/scoring-sql.mjs
CHANGED
|
File without changes
|
package/scripts/launch.mjs
CHANGED
|
File without changes
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// claude-mem-lite: PreToolUse file recall — injects lessons before Edit/Write
|
|
3
|
+
// Lightweight standalone (~30ms): only imports better-sqlite3, fs, path, os
|
|
4
|
+
// Safety: readonly DB, exit 0 always, 3s timeout
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
7
|
+
import { basename, join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
|
|
10
|
+
const DB_PATH = join(homedir(), '.claude-mem-lite', 'claude-mem-lite.db');
|
|
11
|
+
const RUNTIME_DIR = join(homedir(), '.claude-mem-lite', 'runtime');
|
|
12
|
+
const COOLDOWN_PATH = join(RUNTIME_DIR, 'pre-recall-cooldown.json');
|
|
13
|
+
const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
14
|
+
const STALE_MS = 10 * 60 * 1000; // 10 minutes cleanup threshold
|
|
15
|
+
|
|
16
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function inferProject() {
|
|
19
|
+
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
20
|
+
const base = basename(dir);
|
|
21
|
+
const parent = basename(join(dir, '..'));
|
|
22
|
+
let project = (parent && parent !== '.' && parent !== '/')
|
|
23
|
+
? `${parent}--${base}` : base;
|
|
24
|
+
project = project.replace(/[^a-zA-Z0-9_.-]/g, '-') || 'unknown';
|
|
25
|
+
return project;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readCooldown() {
|
|
29
|
+
try { return JSON.parse(readFileSync(COOLDOWN_PATH, 'utf8')); } catch { return {}; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeCooldown(data) {
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
35
|
+
// Clean stale entries
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const cleaned = {};
|
|
38
|
+
for (const [k, v] of Object.entries(data)) {
|
|
39
|
+
if (now - v < STALE_MS) cleaned[k] = v;
|
|
40
|
+
}
|
|
41
|
+
writeFileSync(COOLDOWN_PATH, JSON.stringify(cleaned));
|
|
42
|
+
} catch { /* silent */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Skip if recursive hook
|
|
49
|
+
if (process.env.CLAUDE_MEM_HOOK_RUNNING) process.exit(0);
|
|
50
|
+
|
|
51
|
+
// Skip if DB doesn't exist
|
|
52
|
+
if (!existsSync(DB_PATH)) process.exit(0);
|
|
53
|
+
|
|
54
|
+
// Read stdin
|
|
55
|
+
let input = '';
|
|
56
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
57
|
+
|
|
58
|
+
// Parse event
|
|
59
|
+
let filePath;
|
|
60
|
+
try {
|
|
61
|
+
const event = JSON.parse(input);
|
|
62
|
+
filePath = event.tool_input?.file_path;
|
|
63
|
+
} catch { process.exit(0); }
|
|
64
|
+
|
|
65
|
+
if (!filePath) process.exit(0);
|
|
66
|
+
|
|
67
|
+
// Cooldown check (full path as key)
|
|
68
|
+
const cooldown = readCooldown();
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
if (cooldown[filePath] && (now - cooldown[filePath]) < COOLDOWN_MS) {
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Open DB readonly
|
|
75
|
+
const Database = (await import('better-sqlite3')).default;
|
|
76
|
+
let db;
|
|
77
|
+
try {
|
|
78
|
+
db = new Database(DB_PATH, { readonly: true });
|
|
79
|
+
db.pragma('busy_timeout = 1000');
|
|
80
|
+
} catch { process.exit(0); }
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const project = inferProject();
|
|
84
|
+
const fname = basename(filePath);
|
|
85
|
+
// Escape LIKE wildcards
|
|
86
|
+
const escaped = fname.replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
87
|
+
const likePattern = `%${escaped}`;
|
|
88
|
+
// 60-day lookback to avoid surfacing ancient observations
|
|
89
|
+
const cutoff = Date.now() - 60 * 86400000;
|
|
90
|
+
|
|
91
|
+
const rows = db.prepare(`
|
|
92
|
+
SELECT DISTINCT o.id, o.type, o.title, o.lesson_learned
|
|
93
|
+
FROM observations o
|
|
94
|
+
JOIN observation_files of2 ON of2.obs_id = o.id
|
|
95
|
+
WHERE o.project = ?
|
|
96
|
+
AND o.importance >= 2
|
|
97
|
+
AND o.lesson_learned IS NOT NULL
|
|
98
|
+
AND o.lesson_learned != ''
|
|
99
|
+
AND COALESCE(o.compressed_into, 0) = 0
|
|
100
|
+
AND o.superseded_at IS NULL
|
|
101
|
+
AND o.created_at_epoch > ?
|
|
102
|
+
AND (of2.filename = ? OR of2.filename LIKE ? ESCAPE '\\')
|
|
103
|
+
ORDER BY o.created_at_epoch DESC
|
|
104
|
+
LIMIT 2
|
|
105
|
+
`).all(project, cutoff, filePath, likePattern);
|
|
106
|
+
|
|
107
|
+
if (rows.length > 0) {
|
|
108
|
+
console.log(`[mem] Lessons for ${fname}:`);
|
|
109
|
+
for (const r of rows) {
|
|
110
|
+
const lesson = r.lesson_learned.length > 120
|
|
111
|
+
? r.lesson_learned.slice(0, 117) + '...'
|
|
112
|
+
: r.lesson_learned;
|
|
113
|
+
console.log(` #${r.id} [${r.type}] ${lesson}`);
|
|
114
|
+
}
|
|
115
|
+
// Update cooldown
|
|
116
|
+
cooldown[filePath] = now;
|
|
117
|
+
writeCooldown(cooldown);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// Silent failure — never block editing
|
|
121
|
+
} finally {
|
|
122
|
+
try { db.close(); } catch {}
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Top-level catch — exit 0 no matter what
|
|
126
|
+
}
|
|
File without changes
|
|
File without changes
|
package/secret-scrub.mjs
CHANGED
|
File without changes
|
package/server-internals.mjs
CHANGED
|
File without changes
|
package/server.mjs
CHANGED
|
@@ -488,7 +488,8 @@ function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, isCros
|
|
|
488
488
|
const countLabel = isCrossSource && totalCount > paginatedResults.length
|
|
489
489
|
? `${paginatedResults.length} of ${totalCount}`
|
|
490
490
|
: `${paginatedResults.length}`;
|
|
491
|
-
|
|
491
|
+
const hasMixed = paginatedResults.some(r => r.source === 'session' || r.source === 'prompt');
|
|
492
|
+
lines.push(`Found ${countLabel} result(s)${args.query ? ` for "${args.query}"` : ''}:${hasMixed ? ' (# observation, S# session, P# prompt)' : ''}\n`);
|
|
492
493
|
|
|
493
494
|
for (const r of paginatedResults) {
|
|
494
495
|
if (r.source === 'obs') {
|
package/skill.md
CHANGED
|
File without changes
|
package/skip-tools.mjs
CHANGED
|
File without changes
|
package/stop-words.mjs
CHANGED
|
File without changes
|
package/synonyms.mjs
CHANGED
|
File without changes
|
package/tfidf.mjs
CHANGED
|
File without changes
|
package/tier.mjs
CHANGED
|
File without changes
|
package/tool-schemas.mjs
CHANGED
|
File without changes
|
package/utils.mjs
CHANGED
|
File without changes
|