claude-mem-lite 2.28.1 → 2.29.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/cli.mjs +13 -1
- package/commands/mem.md +2 -1
- package/commands/memory.md +2 -1
- package/commands/tools.md +2 -1
- package/commands/update.md +2 -1
- package/haiku-client.mjs +103 -0
- package/hook-episode.mjs +1 -1
- package/hook-memory.mjs +31 -15
- package/hook-semaphore.mjs +5 -4
- package/hook-update.mjs +1 -1
- package/hook.mjs +20 -1
- package/install.mjs +8 -11
- package/mem-cli.mjs +81 -11
- package/nlp.mjs +26 -0
- package/package.json +1 -5
- package/project-utils.mjs +14 -1
- package/registry-importer.mjs +9 -3
- package/registry.mjs +20 -17
- package/schema.mjs +6 -5
- package/scoring-sql.mjs +7 -6
- package/scripts/post-tool-use.sh +1 -1
- package/scripts/pre-skill-bridge.js +6 -1
- package/scripts/pre-tool-recall.js +22 -7
- package/scripts/prompt-search-utils.mjs +39 -14
- package/scripts/user-prompt-search.js +8 -4
- package/server.mjs +121 -26
- package/skill.md +13 -26
- package/synonyms.mjs +79 -1
- package/tfidf.mjs +2 -2
- package/tool-schemas.mjs +10 -0
- package/utils.mjs +17 -2
- package/commands/recall.md +0 -9
- package/commands/recent.md +0 -7
- package/commands/search.md +0 -9
- package/commands/timeline.md +0 -7
package/cli.mjs
CHANGED
|
@@ -16,7 +16,19 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
16
16
|
} else if (CLI_COMMANDS.has(cmd)) {
|
|
17
17
|
const { run } = await import('./mem-cli.mjs');
|
|
18
18
|
await run(process.argv.slice(2));
|
|
19
|
-
} else if (!cmd
|
|
19
|
+
} else if (!cmd) {
|
|
20
|
+
// No command: show CLI help if installed, install help if not
|
|
21
|
+
const { existsSync } = await import('fs');
|
|
22
|
+
const { join } = await import('path');
|
|
23
|
+
const dbPath = join(process.env.HOME || '', '.claude-mem-lite', 'claude-mem-lite.db');
|
|
24
|
+
if (existsSync(dbPath)) {
|
|
25
|
+
const { run } = await import('./mem-cli.mjs');
|
|
26
|
+
await run(['help']);
|
|
27
|
+
} else {
|
|
28
|
+
const { main } = await import('./install.mjs');
|
|
29
|
+
await main([]);
|
|
30
|
+
}
|
|
31
|
+
} else if (INSTALL_COMMANDS.has(cmd)) {
|
|
20
32
|
const { main } = await import('./install.mjs');
|
|
21
33
|
await main(process.argv.slice(2));
|
|
22
34
|
} else {
|
package/commands/mem.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
name: mem
|
|
3
|
+
description: "Use when: querying past work, managing memories, or checking project history"
|
|
3
4
|
---
|
|
4
5
|
|
|
5
6
|
# Memory
|
package/commands/memory.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
name: memory
|
|
3
|
+
description: "Use when: user asks to remember something, after solving a non-obvious problem, or to capture key session findings"
|
|
3
4
|
---
|
|
4
5
|
|
|
5
6
|
# Memory Save
|
package/commands/tools.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
name: tools
|
|
3
|
+
description: "Use when: importing skills/agents from GitHub, managing registry resources, or searching for tools to solve a problem"
|
|
3
4
|
---
|
|
4
5
|
|
|
5
6
|
# Tool Import
|
package/commands/update.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
2
|
+
name: update
|
|
3
|
+
description: "Use when: search results seem noisy, after bulk imports, or for periodic memory/registry maintenance"
|
|
3
4
|
---
|
|
4
5
|
|
|
5
6
|
# Memory & Registry Maintenance
|
package/haiku-client.mjs
CHANGED
|
@@ -100,6 +100,109 @@ export async function callHaikuJSON(prompt, opts) {
|
|
|
100
100
|
return parseJsonFromLLM(result.text);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// ─── Model-Selectable API ────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Call LLM with explicit model selection. Supports 'haiku' and 'sonnet'.
|
|
107
|
+
* Reuses existing API/CLI dual-mode infrastructure.
|
|
108
|
+
* Never throws — returns null on any error.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} prompt The prompt text
|
|
111
|
+
* @param {'haiku'|'sonnet'} model Model to use (default: 'haiku')
|
|
112
|
+
* @param {object} [opts] Options
|
|
113
|
+
* @param {number} [opts.timeout=15000] Timeout in milliseconds
|
|
114
|
+
* @param {number} [opts.maxTokens=1000] Max tokens in response
|
|
115
|
+
* @returns {Promise<{text: string}|null>} Response or null on failure
|
|
116
|
+
*/
|
|
117
|
+
export async function callLLMWithModel(prompt, model = 'haiku', { timeout = 15000, maxTokens = 1000 } = {}) {
|
|
118
|
+
if (!prompt) return null;
|
|
119
|
+
const resolvedModel = MODEL_MAP[model] ? model : 'haiku';
|
|
120
|
+
const mode = detectMode();
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
if (mode === 'api') {
|
|
124
|
+
return await callModelAPI(prompt, resolvedModel, { timeout, maxTokens });
|
|
125
|
+
}
|
|
126
|
+
return callModelCLI(prompt, resolvedModel, { timeout });
|
|
127
|
+
} catch (e) {
|
|
128
|
+
debugCatch(e, `callLLMWithModel:${resolvedModel}`);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Call LLM with model selection and parse JSON response.
|
|
135
|
+
* @param {string} prompt
|
|
136
|
+
* @param {'haiku'|'sonnet'} model
|
|
137
|
+
* @param {object} [opts]
|
|
138
|
+
* @returns {Promise<object|null>}
|
|
139
|
+
*/
|
|
140
|
+
export async function callModelJSON(prompt, model = 'haiku', opts) {
|
|
141
|
+
const result = await callLLMWithModel(prompt, model, opts);
|
|
142
|
+
if (!result?.text) return null;
|
|
143
|
+
return parseJsonFromLLM(result.text);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function callModelAPI(prompt, model, { timeout, maxTokens }) {
|
|
147
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
148
|
+
if (!apiKey) return null;
|
|
149
|
+
|
|
150
|
+
const modelId = MODEL_MAP[model];
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: {
|
|
158
|
+
'Content-Type': 'application/json',
|
|
159
|
+
'x-api-key': apiKey,
|
|
160
|
+
'anthropic-version': '2023-06-01',
|
|
161
|
+
},
|
|
162
|
+
body: JSON.stringify({
|
|
163
|
+
model: modelId,
|
|
164
|
+
max_tokens: maxTokens,
|
|
165
|
+
messages: [{ role: 'user', content: prompt }],
|
|
166
|
+
}),
|
|
167
|
+
signal: controller.signal,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
debugLog('WARN', `${model}-api`, `HTTP ${res.status}`);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const data = await res.json();
|
|
176
|
+
const text = data.content?.[0]?.text;
|
|
177
|
+
return text ? { text } : null;
|
|
178
|
+
} finally {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function callModelCLI(prompt, model, { timeout }) {
|
|
184
|
+
const modelName = MODEL_MAP[model] ? model : 'haiku';
|
|
185
|
+
try {
|
|
186
|
+
const result = execFileSync(getClaudePath(), ['-p', '--model', modelName], {
|
|
187
|
+
input: prompt,
|
|
188
|
+
timeout,
|
|
189
|
+
encoding: 'utf8',
|
|
190
|
+
env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1' },
|
|
191
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
192
|
+
cwd: '/tmp',
|
|
193
|
+
});
|
|
194
|
+
const text = result.trim();
|
|
195
|
+
return text ? { text } : null;
|
|
196
|
+
} catch (e) {
|
|
197
|
+
const out = e.stdout?.toString?.()?.trim() || e.output?.[1]?.toString?.()?.trim();
|
|
198
|
+
if (out && out.startsWith('{') && out.endsWith('}')) {
|
|
199
|
+
try { JSON.parse(out); return { text: out }; } catch {}
|
|
200
|
+
}
|
|
201
|
+
debugCatch(e, `${model}-cli`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
103
206
|
// ─── API Mode ────────────────────────────────────────────────────────────────
|
|
104
207
|
|
|
105
208
|
async function callHaikuAPI(prompt, { timeout, maxTokens }) {
|
package/hook-episode.mjs
CHANGED
|
@@ -107,7 +107,7 @@ export function readEpisode() {
|
|
|
107
107
|
*/
|
|
108
108
|
export function writeEpisode(episode) {
|
|
109
109
|
const target = episodeFile();
|
|
110
|
-
const tmp = target +
|
|
110
|
+
const tmp = target + `.tmp-${process.pid}`;
|
|
111
111
|
const { _fileSet, ...serializable } = episode;
|
|
112
112
|
writeFileSync(tmp, JSON.stringify(serializable));
|
|
113
113
|
try {
|
package/hook-memory.mjs
CHANGED
|
@@ -6,8 +6,13 @@ import { sanitizeFtsQuery, relaxFtsQueryToOr, debugCatch, OBS_BM25 } from './uti
|
|
|
6
6
|
const MAX_MEMORY_INJECTIONS = 3;
|
|
7
7
|
const MEMORY_LOOKBACK_MS = 60 * 86400000; // 60 days
|
|
8
8
|
// Aligned with TYPE_QUALITY_CASE: high-signal types > noisy types
|
|
9
|
-
// Bugfix
|
|
10
|
-
const MEMORY_TYPE_BOOST = { decision: 1.5, discovery: 1.3, feature: 1.2, refactor: 1.0, change: 0.8, bugfix: 0.
|
|
9
|
+
// Bugfix raised from 0.5→0.75 to match scoring-sql.mjs; lesson_learned boost (1.5×) stacks
|
|
10
|
+
const MEMORY_TYPE_BOOST = { decision: 1.5, discovery: 1.3, feature: 1.2, refactor: 1.0, change: 0.8, bugfix: 0.75 };
|
|
11
|
+
// Adaptive BM25 thresholds — scale with corpus size to filter noise.
|
|
12
|
+
// Larger corpora produce more weak matches from common words.
|
|
13
|
+
const BM25_THRESHOLD = { TINY: 0, SMALL: 1.5, MEDIUM: 2.5, LARGE: 3.5 };
|
|
14
|
+
// OR fallback max token count — queries with 3+ tokens that fail AND are likely off-topic
|
|
15
|
+
const OR_FALLBACK_MAX_TOKENS = 2;
|
|
11
16
|
|
|
12
17
|
const FILE_RECALL_LOOKBACK_MS = 60 * 86400000; // 60 days
|
|
13
18
|
const MAX_FILE_RECALL = 2;
|
|
@@ -47,18 +52,25 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
47
52
|
LIMIT 10
|
|
48
53
|
`);
|
|
49
54
|
let rows = selectStmt.all(ftsQuery, project, cutoff);
|
|
55
|
+
let usedOrFallback = false;
|
|
50
56
|
|
|
51
|
-
// OR fallback when AND returns nothing
|
|
57
|
+
// OR fallback when AND returns nothing — only for short queries (specific enough).
|
|
58
|
+
// 3+ token queries that fail AND are likely off-topic; OR would match individual common words.
|
|
59
|
+
// Count original search terms (AND-separated groups), not expanded synonym tokens.
|
|
60
|
+
const queryTokenCount = ftsQuery.includes(' AND ')
|
|
61
|
+
? ftsQuery.split(' AND ').length
|
|
62
|
+
: ftsQuery.split(/\s+/).filter(t => t && !t.startsWith('(') || !t.endsWith(')')).length;
|
|
52
63
|
if (rows.length === 0) {
|
|
53
64
|
const orQuery = relaxFtsQueryToOr(ftsQuery);
|
|
54
|
-
if (orQuery) {
|
|
55
|
-
try { rows = selectStmt.all(orQuery, project, cutoff); } catch {}
|
|
65
|
+
if (orQuery && queryTokenCount <= OR_FALLBACK_MAX_TOKENS) {
|
|
66
|
+
try { rows = selectStmt.all(orQuery, project, cutoff); usedOrFallback = true; } catch {}
|
|
56
67
|
}
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
// Phase 2: Cross-project search for high-value decisions/discoveries
|
|
60
71
|
// These are transferable insights (debugging patterns, architectural reasons, gotchas)
|
|
61
72
|
let crossRows = [];
|
|
73
|
+
let crossUsedOr = false;
|
|
62
74
|
try {
|
|
63
75
|
const crossStmt = db.prepare(`
|
|
64
76
|
SELECT o.id, o.type, o.title, o.importance, o.lesson_learned, o.project,
|
|
@@ -78,40 +90,44 @@ export function searchRelevantMemories(db, userPrompt, project, excludeIds = [])
|
|
|
78
90
|
crossRows = crossStmt.all(ftsQuery, project, cutoff);
|
|
79
91
|
if (crossRows.length === 0) {
|
|
80
92
|
const orQuery = relaxFtsQueryToOr(ftsQuery);
|
|
81
|
-
if (orQuery) {
|
|
82
|
-
try { crossRows = crossStmt.all(orQuery, project, cutoff); } catch {}
|
|
93
|
+
if (orQuery && queryTokenCount <= OR_FALLBACK_MAX_TOKENS) {
|
|
94
|
+
try { crossRows = crossStmt.all(orQuery, project, cutoff); crossUsedOr = true; } catch {}
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
97
|
} catch (e) { debugCatch(e, 'crossProjectSearch'); }
|
|
86
98
|
|
|
87
99
|
// Merge and score: same-project full weight, cross-project 0.7x
|
|
88
|
-
|
|
100
|
+
// OR-fallback results get 0.4x penalty — they matched individual words, not the full intent
|
|
101
|
+
const allRows = [...rows.map(r => ({ ...r, _or: usedOrFallback })), ...crossRows.map(r => ({ ...r, _or: crossUsedOr }))];
|
|
89
102
|
const scored = allRows
|
|
90
103
|
.filter(r => !excludeSet.has(r.id))
|
|
91
104
|
.map(r => {
|
|
92
105
|
const crossProjectPenalty = r.project === project ? 1.0 : 0.7;
|
|
106
|
+
const orFallbackPenalty = r._or ? 0.4 : 1.0;
|
|
93
107
|
return {
|
|
94
108
|
...r,
|
|
95
109
|
score: Math.abs(r.relevance)
|
|
96
110
|
* (MEMORY_TYPE_BOOST[r.type] || 1.0)
|
|
97
111
|
* (r.lesson_learned ? 1.5 : 1.0)
|
|
98
112
|
* (r.importance >= 2 ? 1.0 : 0.6)
|
|
99
|
-
* crossProjectPenalty
|
|
113
|
+
* crossProjectPenalty
|
|
114
|
+
* orFallbackPenalty,
|
|
100
115
|
};
|
|
101
116
|
})
|
|
102
117
|
.sort((a, b) => b.score - a.score);
|
|
103
118
|
|
|
104
|
-
// Adaptive threshold:
|
|
105
|
-
//
|
|
106
|
-
// meaningful discrimination and the calibrated 1.5 threshold works well.
|
|
119
|
+
// Adaptive threshold: scales with corpus size to filter noise.
|
|
120
|
+
// Each result must individually exceed the threshold (not just the top one).
|
|
107
121
|
const obsCount = db.prepare(
|
|
108
122
|
'SELECT COUNT(*) as c FROM observations WHERE project = ? AND COALESCE(compressed_into, 0) = 0',
|
|
109
123
|
).get(project)?.c || 0;
|
|
110
|
-
const
|
|
111
|
-
|
|
124
|
+
const { TINY, SMALL, MEDIUM, LARGE } = BM25_THRESHOLD;
|
|
125
|
+
const threshold = obsCount < 5 ? TINY : obsCount < 100 ? SMALL : obsCount < 500 ? MEDIUM : LARGE;
|
|
126
|
+
const aboveThreshold = scored.filter(r => r.score >= threshold);
|
|
127
|
+
if (aboveThreshold.length === 0) return [];
|
|
112
128
|
|
|
113
129
|
// Update access_count for injected memories
|
|
114
|
-
const result =
|
|
130
|
+
const result = aboveThreshold.slice(0, MAX_MEMORY_INJECTIONS);
|
|
115
131
|
const now = Date.now();
|
|
116
132
|
const updateStmt = db.prepare('UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id = ?');
|
|
117
133
|
for (const r of result) {
|
package/hook-semaphore.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Limits concurrent claude -p calls to prevent resource contention
|
|
3
3
|
|
|
4
4
|
import { join } from 'path';
|
|
5
|
-
import { readFileSync,
|
|
5
|
+
import { readFileSync, unlinkSync, readdirSync, openSync, closeSync, writeSync, constants as fsConstants } from 'fs';
|
|
6
6
|
import { RUNTIME_DIR } from './hook-shared.mjs';
|
|
7
7
|
|
|
8
8
|
export const LLM_SEM_MAX = 2;
|
|
@@ -21,7 +21,7 @@ export async function acquireLLMSlot() {
|
|
|
21
21
|
|
|
22
22
|
while (Date.now() < deadline) {
|
|
23
23
|
// Acquire-then-verify: atomically create our slot first, then check total count
|
|
24
|
-
let created
|
|
24
|
+
let created;
|
|
25
25
|
try {
|
|
26
26
|
let fd;
|
|
27
27
|
try {
|
|
@@ -33,8 +33,9 @@ export async function acquireLLMSlot() {
|
|
|
33
33
|
if (fd !== undefined) closeSync(fd);
|
|
34
34
|
}
|
|
35
35
|
} catch {
|
|
36
|
-
// Slot file already exists for this PID —
|
|
37
|
-
|
|
36
|
+
// Slot file already exists for this PID — stale cleanup should have removed it;
|
|
37
|
+
// retry and let O_CREAT|O_EXCL succeed on next iteration (avoid non-atomic fallback)
|
|
38
|
+
continue;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
if (!created) { await sleepMs(200 + Math.random() * 800); continue; }
|
package/hook-update.mjs
CHANGED
|
@@ -214,7 +214,7 @@ async function downloadAndInstall(tarballUrl) {
|
|
|
214
214
|
|
|
215
215
|
// Download tarball via curl (available on all supported platforms)
|
|
216
216
|
// Validate URL to prevent command injection via crafted tarball URLs
|
|
217
|
-
if (!/^https:\/\/[a-zA-Z0-9
|
|
217
|
+
if (!/^https:\/\/(?:api\.)?github\.com\/[a-zA-Z0-9./_-]+$/.test(tarballUrl)) {
|
|
218
218
|
debugLog('WARN', 'hook-update', `Rejected suspicious tarball URL: ${tarballUrl}`);
|
|
219
219
|
return false;
|
|
220
220
|
}
|
package/hook.mjs
CHANGED
|
@@ -31,13 +31,14 @@ import { handleLLMEpisode, handleLLMSummary, saveObservation, buildImmediateObse
|
|
|
31
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
|
+
import { handleLLMOptimize } from './hook-optimize.mjs';
|
|
34
35
|
import { SKIP_TOOLS, SKIP_PREFIXES } from './skip-tools.mjs';
|
|
35
36
|
import { getVocabulary } from './tfidf.mjs';
|
|
36
37
|
|
|
37
38
|
// Prevent recursive hooks from background claude -p calls
|
|
38
39
|
// Background workers (llm-episode, llm-summary) are exempt — they're ours
|
|
39
40
|
const event = process.argv[2];
|
|
40
|
-
const BG_EVENTS = new Set(['llm-episode', 'llm-summary']);
|
|
41
|
+
const BG_EVENTS = new Set(['llm-episode', 'llm-summary', 'auto-compress', 'llm-optimize']);
|
|
41
42
|
|
|
42
43
|
// Respect Claude Code plugin disable state even when legacy settings.json hooks remain.
|
|
43
44
|
// install.mjs writes direct hooks into ~/.claude/settings.json, so disabling the plugin
|
|
@@ -122,6 +123,22 @@ function flushEpisode(episode) {
|
|
|
122
123
|
|
|
123
124
|
if (isSignificant) {
|
|
124
125
|
spawnBackground('llm-episode', flushFile);
|
|
126
|
+
|
|
127
|
+
// P3: Auto-save hint — detect error→fix pattern (error entry followed by Edit/Write)
|
|
128
|
+
// and nudge Claude to save the lesson for future recall
|
|
129
|
+
try {
|
|
130
|
+
const entries = episode.entries || [];
|
|
131
|
+
const hasError = entries.some(e => e.isError);
|
|
132
|
+
const hasEdit = entries.some(e => EDIT_TOOLS.has(e.tool));
|
|
133
|
+
if (hasError && hasEdit && entries.length >= 3) {
|
|
134
|
+
const editFiles = entries.filter(e => EDIT_TOOLS.has(e.tool)).flatMap(e => e.files || []);
|
|
135
|
+
const uniqueFiles = [...new Set(editFiles)].slice(0, 3);
|
|
136
|
+
const filesHint = uniqueFiles.length > 0 ? ` (files: ${uniqueFiles.join(', ')})` : '';
|
|
137
|
+
process.stdout.write(
|
|
138
|
+
`[mem] 💡 Error→fix pattern detected${filesHint}. Consider: mem_save(type="bugfix", lesson_learned="root cause & fix")\n`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
} catch { /* never block on hint */ }
|
|
125
142
|
} else {
|
|
126
143
|
try { unlinkSync(flushFile); } catch {}
|
|
127
144
|
}
|
|
@@ -539,6 +556,7 @@ async function handleSessionStart() {
|
|
|
539
556
|
writeFileSync(maintainFile, JSON.stringify({ epoch: Date.now() }));
|
|
540
557
|
// Weekly summary grouping runs in background to avoid blocking SessionStart
|
|
541
558
|
spawnBackground('auto-compress');
|
|
559
|
+
if (!process.env.CLAUDE_MEM_SKIP_OPTIMIZE) spawnBackground('llm-optimize');
|
|
542
560
|
} catch (e) { debugCatch(e, 'auto-maintain'); }
|
|
543
561
|
}
|
|
544
562
|
|
|
@@ -1060,6 +1078,7 @@ try {
|
|
|
1060
1078
|
case 'llm-episode': await handleLLMEpisode(); break;
|
|
1061
1079
|
case 'llm-summary': await handleLLMSummary(); break;
|
|
1062
1080
|
case 'auto-compress': handleAutoCompress(); break;
|
|
1081
|
+
case 'llm-optimize': await handleLLMOptimize(); break;
|
|
1063
1082
|
}
|
|
1064
1083
|
} catch (err) {
|
|
1065
1084
|
// Always log fatal errors (ungated) with structured format
|
package/install.mjs
CHANGED
|
@@ -204,7 +204,7 @@ async function install() {
|
|
|
204
204
|
const SOURCE_FILES = [
|
|
205
205
|
'cli.mjs', 'server.mjs', 'server-internals.mjs', 'tool-schemas.mjs',
|
|
206
206
|
'hook.mjs', 'hook-shared.mjs', 'hook-llm.mjs', 'hook-memory.mjs', 'skip-tools.mjs',
|
|
207
|
-
'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs',
|
|
207
|
+
'hook-semaphore.mjs', 'hook-episode.mjs', 'hook-context.mjs', 'hook-handoff.mjs', 'hook-update.mjs', 'hook-optimize.mjs',
|
|
208
208
|
'haiku-client.mjs', 'utils.mjs', 'schema.mjs', 'package.json', 'package-lock.json', 'skill.md',
|
|
209
209
|
'registry.mjs', 'registry-scanner.mjs', 'registry-indexer.mjs',
|
|
210
210
|
'registry-retriever.mjs', 'resource-discovery.mjs',
|
|
@@ -529,8 +529,9 @@ async function install() {
|
|
|
529
529
|
}
|
|
530
530
|
|
|
531
531
|
// 6. Install pre-installed resources (skills + agents)
|
|
532
|
-
|
|
533
|
-
|
|
532
|
+
if (process.env.CLAUDE_MEM_SKIP_REPOS) {
|
|
533
|
+
ok('Skill/agent registry: skipped (CLAUDE_MEM_SKIP_REPOS)');
|
|
534
|
+
} else try {
|
|
534
535
|
const manifestPath = join(INSTALL_DIR, 'registry', 'preinstalled.json');
|
|
535
536
|
if (!existsSync(manifestPath)) {
|
|
536
537
|
// For git-clone mode, check PROJECT_DIR
|
|
@@ -565,7 +566,7 @@ async function install() {
|
|
|
565
566
|
};
|
|
566
567
|
|
|
567
568
|
for (const [repoUrl, entries] of repos) {
|
|
568
|
-
const repoName = repoUrl.split('/').slice(-2).join('-');
|
|
569
|
+
const repoName = repoUrl.split('/').slice(-2).join('-').replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
569
570
|
const clonePath = join(managedDir, 'repos', repoName);
|
|
570
571
|
let repoReady = false;
|
|
571
572
|
|
|
@@ -959,12 +960,8 @@ async function status() {
|
|
|
959
960
|
|
|
960
961
|
// CLI
|
|
961
962
|
try {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
ok('CLI: claude-mem-lite command available');
|
|
965
|
-
} else {
|
|
966
|
-
warn('CLI: command not on PATH');
|
|
967
|
-
}
|
|
963
|
+
execFileSync('claude-mem-lite', ['--help'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
|
|
964
|
+
ok('CLI: claude-mem-lite command available');
|
|
968
965
|
} catch {
|
|
969
966
|
warn('CLI: command not on PATH — run install again to create symlink');
|
|
970
967
|
}
|
|
@@ -1087,7 +1084,7 @@ async function doctor() {
|
|
|
1087
1084
|
|
|
1088
1085
|
// Check for stale processes
|
|
1089
1086
|
try {
|
|
1090
|
-
const procs =
|
|
1087
|
+
const procs = execFileSync('pgrep', ['-af', 'chroma|claude-mem.*worker'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' }).trim();
|
|
1091
1088
|
// Filter out the pgrep process itself (matches its own pattern)
|
|
1092
1089
|
const real = procs.split('\n').filter(l => !l.includes('pgrep'));
|
|
1093
1090
|
if (real.length > 0) {
|
package/mem-cli.mjs
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { ensureDb, DB_PATH, REGISTRY_DB_PATH, checkFTSIntegrity, rebuildFTS } from './schema.mjs';
|
|
7
7
|
import { sanitizeFtsQuery, relaxFtsQueryToOr, truncate, typeIcon, inferProject, jaccardSimilarity, computeMinHash, estimateJaccardFromMinHash, scrubSecrets, cjkBigrams, isoWeekKey, COMPRESSED_PENDING_PURGE, OBS_BM25, SESS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, DEFAULT_DECAY_HALF_LIFE_MS, getCurrentBranch } from './utils.mjs';
|
|
8
|
+
import { extractCjkLikePatterns } from './nlp.mjs';
|
|
8
9
|
import { resolveProject } from './project-utils.mjs';
|
|
9
10
|
import { computeTier, TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
10
11
|
import { getVocabulary, computeVector, vectorSearch, rrfMerge, VECTOR_SCAN_LIMIT, rebuildVocabulary, _resetVocabCache } from './tfidf.mjs';
|
|
11
12
|
import { autoBoostIfNeeded, reRankWithContext, markSuperseded, extractPRFTerms, expandQueryByConcepts } from './server-internals.mjs';
|
|
12
13
|
import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
13
14
|
import { searchResources } from './registry-retriever.mjs';
|
|
15
|
+
import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
14
16
|
import { basename, join } from 'path';
|
|
15
17
|
import { readFileSync } from 'fs';
|
|
16
18
|
|
|
@@ -52,7 +54,7 @@ function out(text) {
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function fail(text) {
|
|
55
|
-
process.
|
|
57
|
+
process.stderr.write(text + '\n');
|
|
56
58
|
process.exitCode = 1;
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -115,13 +117,15 @@ function cmdSearch(db, args) {
|
|
|
115
117
|
fail(`[mem] Invalid --sort "${sort}". Use: relevance, time, importance`);
|
|
116
118
|
return;
|
|
117
119
|
}
|
|
120
|
+
const useOr = flags.or === true || flags.or === 'true';
|
|
118
121
|
|
|
119
122
|
if (source && !['observations', 'sessions', 'prompts'].includes(source)) {
|
|
120
123
|
fail(`[mem] Invalid --source "${source}". Use: observations, sessions, prompts`);
|
|
121
124
|
return;
|
|
122
125
|
}
|
|
123
126
|
|
|
124
|
-
|
|
127
|
+
let ftsQuery = sanitizeFtsQuery(query);
|
|
128
|
+
if (ftsQuery && useOr) ftsQuery = relaxFtsQueryToOr(ftsQuery) || ftsQuery;
|
|
125
129
|
if (!ftsQuery) {
|
|
126
130
|
fail(`[mem] No valid search terms in "${query}"`);
|
|
127
131
|
return;
|
|
@@ -278,6 +282,31 @@ function cmdSearch(db, args) {
|
|
|
278
282
|
LIMIT ? OFFSET ?
|
|
279
283
|
`).all(...promptParams);
|
|
280
284
|
for (const r of promptRows) results.push({ ...r, _source: 'prompt' });
|
|
285
|
+
// CJK LIKE fallback: FTS5 unicode61 can't tokenize CJK substrings in prompts
|
|
286
|
+
if (promptRows.length === 0) {
|
|
287
|
+
const cjkPatterns = extractCjkLikePatterns(query);
|
|
288
|
+
if (cjkPatterns.length > 0) {
|
|
289
|
+
const likeConds = cjkPatterns.map(() => 'p.prompt_text LIKE ?');
|
|
290
|
+
const likeParams = cjkPatterns.map(p => `%${p}%`);
|
|
291
|
+
if (project) likeParams.push(project);
|
|
292
|
+
if (dateFrom) likeParams.push(dateFrom);
|
|
293
|
+
if (dateTo) likeParams.push(dateTo);
|
|
294
|
+
likeParams.push(effectiveSource ? limit : limit, effectiveSource ? offset : 0);
|
|
295
|
+
const fallbackRows = db.prepare(`
|
|
296
|
+
SELECT p.id, p.prompt_text, p.content_session_id, p.created_at, p.created_at_epoch
|
|
297
|
+
FROM user_prompts p
|
|
298
|
+
JOIN sdk_sessions s ON p.content_session_id = s.content_session_id
|
|
299
|
+
WHERE (${likeConds.join(' OR ')})
|
|
300
|
+
AND p.prompt_text NOT LIKE '<task-notification>%'
|
|
301
|
+
${project ? 'AND s.project = ?' : ''}
|
|
302
|
+
${dateFrom ? 'AND p.created_at_epoch >= ?' : ''}
|
|
303
|
+
${dateTo ? 'AND p.created_at_epoch <= ?' : ''}
|
|
304
|
+
ORDER BY p.created_at_epoch DESC
|
|
305
|
+
LIMIT ? OFFSET ?
|
|
306
|
+
`).all(...likeParams);
|
|
307
|
+
for (const r of fallbackRows) results.push({ ...r, _source: 'prompt', score: 0 });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
281
310
|
} catch { /* prompt FTS may not exist in older DBs */ }
|
|
282
311
|
}
|
|
283
312
|
|
|
@@ -449,7 +478,7 @@ function searchFts(db, ftsQuery, { type, project, limit, dateFrom, dateTo, minIm
|
|
|
449
478
|
function cmdRecent(db, args) {
|
|
450
479
|
const { positional, flags } = parseArgs(args);
|
|
451
480
|
const rawLimit = parseInt(positional[0], 10);
|
|
452
|
-
const limit =
|
|
481
|
+
const limit = (Number.isInteger(rawLimit) && rawLimit > 0) ? rawLimit : 10;
|
|
453
482
|
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
454
483
|
|
|
455
484
|
const params = [];
|
|
@@ -511,7 +540,9 @@ function cmdRecall(db, args) {
|
|
|
511
540
|
// Update access_count for recalled observations (aligned with MCP mem_recall)
|
|
512
541
|
const recalledIds = rows.map(r => r.id);
|
|
513
542
|
const recallPh = recalledIds.map(() => '?').join(',');
|
|
514
|
-
|
|
543
|
+
try {
|
|
544
|
+
db.prepare(`UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id IN (${recallPh})`).run(Date.now(), ...recalledIds);
|
|
545
|
+
} catch { /* non-critical: FTS5 trigger may fail on corrupted index */ }
|
|
515
546
|
|
|
516
547
|
out(`[mem] History for ${filename} (${rows.length}):`);
|
|
517
548
|
for (const r of rows) {
|
|
@@ -588,8 +619,10 @@ function cmdGet(db, args) {
|
|
|
588
619
|
}
|
|
589
620
|
|
|
590
621
|
// Update access_count + auto-boost (aligned with MCP mem_get)
|
|
591
|
-
|
|
592
|
-
|
|
622
|
+
try {
|
|
623
|
+
db.prepare(`UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id IN (${placeholders})`).run(Date.now(), ...ids);
|
|
624
|
+
autoBoostIfNeeded(db, ids);
|
|
625
|
+
} catch { /* non-critical: FTS5 trigger may fail on corrupted index */ }
|
|
593
626
|
|
|
594
627
|
const rows = db.prepare(`
|
|
595
628
|
SELECT * FROM observations
|
|
@@ -680,7 +713,9 @@ function cmdTimeline(db, args) {
|
|
|
680
713
|
}
|
|
681
714
|
|
|
682
715
|
// Update access_count for anchor (aligned with MCP mem_timeline)
|
|
683
|
-
|
|
716
|
+
try {
|
|
717
|
+
db.prepare('UPDATE observations SET access_count = COALESCE(access_count, 0) + 1, last_accessed_at = ? WHERE id = ?').run(Date.now(), anchorId);
|
|
718
|
+
} catch { /* non-critical: FTS5 trigger may fail on corrupted index */ }
|
|
684
719
|
|
|
685
720
|
// Get anchor epoch
|
|
686
721
|
const anchorRow = db.prepare('SELECT created_at_epoch, project FROM observations WHERE id = ?').get(anchorId);
|
|
@@ -1786,6 +1821,7 @@ Commands:
|
|
|
1786
1821
|
--offset N Skip first N results (pagination)
|
|
1787
1822
|
--tier T Filter by tier (working|active|archive, observations only)
|
|
1788
1823
|
--sort S Sort: relevance (default), time, importance
|
|
1824
|
+
--or Use OR instead of AND between search terms
|
|
1789
1825
|
|
|
1790
1826
|
recent [N] Show N most recent observations (default 10)
|
|
1791
1827
|
--project P Filter by project
|
|
@@ -1869,7 +1905,7 @@ DB: ${DB_PATH}`);
|
|
|
1869
1905
|
|
|
1870
1906
|
// ─── Import (GitHub) ────────────────────────────────────────────────────────
|
|
1871
1907
|
|
|
1872
|
-
async function cmdImport(
|
|
1908
|
+
async function cmdImport(argv) {
|
|
1873
1909
|
const { positional, flags } = parseArgs(argv);
|
|
1874
1910
|
const url = positional[0];
|
|
1875
1911
|
|
|
@@ -1923,7 +1959,7 @@ async function cmdImport(db, argv) {
|
|
|
1923
1959
|
|
|
1924
1960
|
// ─── Enrich ─────────────────────────────────────────────────────────────────
|
|
1925
1961
|
|
|
1926
|
-
async function cmdEnrich(
|
|
1962
|
+
async function cmdEnrich(argv) {
|
|
1927
1963
|
const { positional, flags } = parseArgs(argv);
|
|
1928
1964
|
const name = positional[0];
|
|
1929
1965
|
|
|
@@ -1971,6 +2007,39 @@ async function cmdEnrich(db, argv) {
|
|
|
1971
2007
|
}
|
|
1972
2008
|
}
|
|
1973
2009
|
|
|
2010
|
+
async function cmdOptimize(db, args) {
|
|
2011
|
+
const run = args.includes('--run');
|
|
2012
|
+
const runAll = args.includes('--run-all');
|
|
2013
|
+
const taskIdx = args.indexOf('--task');
|
|
2014
|
+
const tasks = taskIdx >= 0 && args[taskIdx + 1] ? [args[taskIdx + 1]] : undefined;
|
|
2015
|
+
const maxIdx = args.indexOf('--max');
|
|
2016
|
+
const maxItems = maxIdx >= 0 ? parseInt(args[maxIdx + 1], 10) || 15 : 15;
|
|
2017
|
+
|
|
2018
|
+
if (!run && !runAll) {
|
|
2019
|
+
const preview = optimizePreview(db);
|
|
2020
|
+
out('[mem] 🔍 LLM Optimization Preview:');
|
|
2021
|
+
out(` Re-enrich candidates: ${preview.reenrich}`);
|
|
2022
|
+
out(` Normalize: ${preview.normalizeGateOpen ? `${preview.normalize} unique concepts` : 'gate closed (7-day interval)'}`);
|
|
2023
|
+
out(` Cluster-merge: ${preview.clusterMerge} clusters`);
|
|
2024
|
+
out(` Smart-compress: ${preview.smartCompress} clusters`);
|
|
2025
|
+
out(` Total: ${preview.total} items`);
|
|
2026
|
+
out('');
|
|
2027
|
+
out('Run with --run to execute, --run-all to bypass gates.');
|
|
2028
|
+
return;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
out('[mem] Running LLM optimization...');
|
|
2032
|
+
const results = await optimizeRun(db, { tasks, maxItems, force: runAll });
|
|
2033
|
+
|
|
2034
|
+
if (results.reenrich) out(` Re-enrich: ${results.reenrich.processed || 0} processed, ${results.reenrich.skipped || 0} skipped`);
|
|
2035
|
+
if (results.normalize) {
|
|
2036
|
+
if (results.normalize.skipped) out(` Normalize: skipped (${results.normalize.reason})`);
|
|
2037
|
+
else out(` Normalize: ${results.normalize.processed || 0} updated, ${results.normalize.groups || 0} synonym groups`);
|
|
2038
|
+
}
|
|
2039
|
+
if (results.clusterMerge) out(` Cluster-merge: ${results.clusterMerge.merged || 0} merged of ${results.clusterMerge.processed || 0} clusters`);
|
|
2040
|
+
if (results.smartCompress) out(` Smart-compress: ${results.smartCompress.compressed || 0} compressed of ${results.smartCompress.processed || 0} clusters`);
|
|
2041
|
+
}
|
|
2042
|
+
|
|
1974
2043
|
// ─── Main Entry Point ────────────────────────────────────────────────────────
|
|
1975
2044
|
|
|
1976
2045
|
export async function run(argv) {
|
|
@@ -2011,13 +2080,14 @@ export async function run(argv) {
|
|
|
2011
2080
|
case 'export': cmdExport(db, cmdArgs); break;
|
|
2012
2081
|
case 'compress': cmdCompress(db, cmdArgs); break;
|
|
2013
2082
|
case 'maintain': cmdMaintain(db, cmdArgs); break;
|
|
2083
|
+
case 'optimize': await cmdOptimize(db, cmdArgs); break;
|
|
2014
2084
|
case 'fts-check': cmdFtsCheck(db, cmdArgs); break;
|
|
2015
2085
|
case 'stats': cmdStats(db, cmdArgs); break;
|
|
2016
2086
|
case 'context': cmdContext(db, cmdArgs); break;
|
|
2017
2087
|
case 'browse': cmdBrowse(db, cmdArgs); break;
|
|
2018
2088
|
case 'registry': cmdRegistry(db, cmdArgs); break;
|
|
2019
|
-
case 'import': await cmdImport(
|
|
2020
|
-
case 'enrich': await cmdEnrich(
|
|
2089
|
+
case 'import': await cmdImport(cmdArgs); break;
|
|
2090
|
+
case 'enrich': await cmdEnrich(cmdArgs); break;
|
|
2021
2091
|
default:
|
|
2022
2092
|
out(`[mem] Unknown command: ${cmd}`);
|
|
2023
2093
|
out('[mem] Run "claude-mem-lite help" for usage');
|