claude-mem-lite 2.28.2 → 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/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-memory.mjs +31 -15
- package/hook.mjs +20 -1
- package/install.mjs +1 -1
- package/mem-cli.mjs +61 -0
- package/nlp.mjs +26 -0
- package/package.json +1 -5
- package/project-utils.mjs +14 -1
- package/schema.mjs +2 -1
- package/scoring-sql.mjs +4 -3
- package/scripts/pre-tool-recall.js +22 -7
- package/scripts/prompt-search-utils.mjs +39 -14
- package/server.mjs +97 -17
- package/skill.md +13 -26
- package/synonyms.mjs +79 -1
- package/tool-schemas.mjs +10 -0
- package/utils.mjs +1 -1
- 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/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-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.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', 'auto-compress']);
|
|
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',
|
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
|
|
|
@@ -280,6 +282,31 @@ function cmdSearch(db, args) {
|
|
|
280
282
|
LIMIT ? OFFSET ?
|
|
281
283
|
`).all(...promptParams);
|
|
282
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
|
+
}
|
|
283
310
|
} catch { /* prompt FTS may not exist in older DBs */ }
|
|
284
311
|
}
|
|
285
312
|
|
|
@@ -1980,6 +2007,39 @@ async function cmdEnrich(argv) {
|
|
|
1980
2007
|
}
|
|
1981
2008
|
}
|
|
1982
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
|
+
|
|
1983
2043
|
// ─── Main Entry Point ────────────────────────────────────────────────────────
|
|
1984
2044
|
|
|
1985
2045
|
export async function run(argv) {
|
|
@@ -2020,6 +2080,7 @@ export async function run(argv) {
|
|
|
2020
2080
|
case 'export': cmdExport(db, cmdArgs); break;
|
|
2021
2081
|
case 'compress': cmdCompress(db, cmdArgs); break;
|
|
2022
2082
|
case 'maintain': cmdMaintain(db, cmdArgs); break;
|
|
2083
|
+
case 'optimize': await cmdOptimize(db, cmdArgs); break;
|
|
2023
2084
|
case 'fts-check': cmdFtsCheck(db, cmdArgs); break;
|
|
2024
2085
|
case 'stats': cmdStats(db, cmdArgs); break;
|
|
2025
2086
|
case 'context': cmdContext(db, cmdArgs); break;
|
package/nlp.mjs
CHANGED
|
@@ -108,6 +108,22 @@ export function extractCjkKeywords(text) {
|
|
|
108
108
|
return found;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Extract CJK patterns suitable for SQL LIKE fallback when FTS5 fails on CJK text.
|
|
113
|
+
* Uses dictionary extraction + bigram fallback for unmatched portions.
|
|
114
|
+
* @param {string} query Raw query text
|
|
115
|
+
* @returns {string[]} CJK patterns (≥2 chars each), empty if no CJK content
|
|
116
|
+
*/
|
|
117
|
+
export function extractCjkLikePatterns(query) {
|
|
118
|
+
if (!query || !/[\u4e00-\u9fff\u3400-\u4dbf]{2,}/.test(query)) return [];
|
|
119
|
+
const keywords = extractCjkKeywords(query);
|
|
120
|
+
// Bigrams for unmatched CJK portions
|
|
121
|
+
let remainder = query;
|
|
122
|
+
for (const w of keywords) remainder = remainder.split(w).join(' ');
|
|
123
|
+
const bigrams = cjkBigrams(remainder).split(' ').filter(Boolean);
|
|
124
|
+
return [...new Set([...keywords, ...bigrams])];
|
|
125
|
+
}
|
|
126
|
+
|
|
111
127
|
// ─── FTS5 Token Formatting ──────────────────────────────────────────────────
|
|
112
128
|
|
|
113
129
|
// Format a term for FTS5: quote if it contains spaces, hyphens, or special chars
|
|
@@ -166,6 +182,16 @@ export function sanitizeFtsQuery(query) {
|
|
|
166
182
|
if (cjkWords.length > 0) {
|
|
167
183
|
expandedTokens.push(...cjkWords);
|
|
168
184
|
cjkExtracted = true;
|
|
185
|
+
// Preserve unmatched CJK portions as bigrams (don't silently drop them)
|
|
186
|
+
const matched = new Set(cjkWords);
|
|
187
|
+
let remainder = t;
|
|
188
|
+
for (const w of matched) remainder = remainder.split(w).join(' ');
|
|
189
|
+
const gapBigrams = cjkBigrams(remainder);
|
|
190
|
+
if (gapBigrams) {
|
|
191
|
+
for (const bg of gapBigrams.split(' ')) {
|
|
192
|
+
if (bg && !CJK_STOP_WORDS.has(bg) && !matched.has(bg)) expandedTokens.push(bg);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
169
195
|
continue;
|
|
170
196
|
}
|
|
171
197
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.29.0",
|
|
4
4
|
"description": "Lightweight persistent memory system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -64,10 +64,6 @@
|
|
|
64
64
|
"skill.md",
|
|
65
65
|
"commands/mem.md",
|
|
66
66
|
"commands/memory.md",
|
|
67
|
-
"commands/search.md",
|
|
68
|
-
"commands/recall.md",
|
|
69
|
-
"commands/recent.md",
|
|
70
|
-
"commands/timeline.md",
|
|
71
67
|
"commands/update.md",
|
|
72
68
|
"commands/tools.md",
|
|
73
69
|
"hooks/hooks.json",
|
package/project-utils.mjs
CHANGED
|
@@ -20,12 +20,25 @@ export function resolveProject(db, name) {
|
|
|
20
20
|
|
|
21
21
|
// Short name: prefer the canonical "parent--name" form (from inferProject())
|
|
22
22
|
// which typically has far more data than manually-saved short names.
|
|
23
|
+
// 1) Exact suffix match: "mem" → "projects--mem"
|
|
23
24
|
const suffixed = db.prepare(
|
|
24
25
|
'SELECT project FROM observations WHERE project LIKE ? GROUP BY project ORDER BY COUNT(*) DESC LIMIT 1'
|
|
25
26
|
).get(`%--${name}`);
|
|
26
27
|
if (suffixed) { _cache.set(name, suffixed.project); return suffixed.project; }
|
|
27
28
|
|
|
28
|
-
//
|
|
29
|
+
// 2) Prefix-in-suffix match: "code-graph" → "projects--code-graph-mcp"
|
|
30
|
+
const prefixed = db.prepare(
|
|
31
|
+
'SELECT project FROM observations WHERE project LIKE ? GROUP BY project ORDER BY COUNT(*) DESC LIMIT 1'
|
|
32
|
+
).get(`%--${name}%`);
|
|
33
|
+
if (prefixed) { _cache.set(name, prefixed.project); return prefixed.project; }
|
|
34
|
+
|
|
35
|
+
// 3) Substring match: broader fallback for partial names
|
|
36
|
+
const substr = db.prepare(
|
|
37
|
+
'SELECT project FROM observations WHERE project LIKE ? GROUP BY project ORDER BY COUNT(*) DESC LIMIT 1'
|
|
38
|
+
).get(`%${name}%`);
|
|
39
|
+
if (substr) { _cache.set(name, substr.project); return substr.project; }
|
|
40
|
+
|
|
41
|
+
// 4) Fallback: synthesize canonical form from current directory
|
|
29
42
|
const inferred = inferProject();
|
|
30
43
|
if (inferred.endsWith(`--${name}`)) { _cache.set(name, inferred); return inferred; }
|
|
31
44
|
|
package/schema.mjs
CHANGED
|
@@ -13,7 +13,7 @@ export const DB_PATH = join(DB_DIR, 'claude-mem-lite.db');
|
|
|
13
13
|
export const REGISTRY_DB_PATH = join(DB_DIR, 'resource-registry.db');
|
|
14
14
|
|
|
15
15
|
// Increment when schema changes (tables, columns, indexes, FTS, migrations)
|
|
16
|
-
export const CURRENT_SCHEMA_VERSION =
|
|
16
|
+
export const CURRENT_SCHEMA_VERSION = 21;
|
|
17
17
|
|
|
18
18
|
const CORE_SCHEMA = `
|
|
19
19
|
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
|
@@ -111,6 +111,7 @@ const MIGRATIONS = [
|
|
|
111
111
|
'ALTER TABLE observations ADD COLUMN superseded_at INTEGER DEFAULT NULL',
|
|
112
112
|
'ALTER TABLE observations ADD COLUMN superseded_by INTEGER DEFAULT NULL',
|
|
113
113
|
'ALTER TABLE observations ADD COLUMN last_accessed_at INTEGER DEFAULT NULL',
|
|
114
|
+
'ALTER TABLE observations ADD COLUMN optimized_at INTEGER DEFAULT NULL',
|
|
114
115
|
];
|
|
115
116
|
|
|
116
117
|
/**
|
package/scoring-sql.mjs
CHANGED
|
@@ -41,8 +41,9 @@ export const TYPE_DECAY_CASE = `(
|
|
|
41
41
|
)`;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Type quality multiplier —
|
|
45
|
-
*
|
|
44
|
+
* Type quality multiplier — promotes high-signal types (decisions, discoveries).
|
|
45
|
+
* Bugfix raised from 0.35→0.75: lesson_learned bugfixes are valuable during
|
|
46
|
+
* active debugging; raw error logs are filtered by importance, not type penalty.
|
|
46
47
|
* Applied as: BM25 × time_decay × TYPE_QUALITY × project_boost × importance
|
|
47
48
|
*/
|
|
48
49
|
export const TYPE_QUALITY_CASE = `(
|
|
@@ -52,7 +53,7 @@ export const TYPE_QUALITY_CASE = `(
|
|
|
52
53
|
WHEN 'feature' THEN 1.2
|
|
53
54
|
WHEN 'refactor' THEN 1.0
|
|
54
55
|
WHEN 'change' THEN 0.8
|
|
55
|
-
WHEN 'bugfix' THEN 0.
|
|
56
|
+
WHEN 'bugfix' THEN 0.75
|
|
56
57
|
ELSE 1.0
|
|
57
58
|
END
|
|
58
59
|
)`;
|
|
@@ -88,29 +88,44 @@ try {
|
|
|
88
88
|
// 60-day lookback to avoid surfacing ancient observations
|
|
89
89
|
const cutoff = Date.now() - 60 * 86400000;
|
|
90
90
|
|
|
91
|
+
// Surface actionable lessons first, then high-importance bugfix/decision observations.
|
|
92
|
+
// Priority: 1) observations with lesson_learned (most actionable for preventing repeat bugs)
|
|
93
|
+
// 2) bugfix/decision types with importance>=2 (contextual history)
|
|
94
|
+
// Skip pure change/discovery without lessons — they add noise without actionable value.
|
|
91
95
|
const rows = db.prepare(`
|
|
92
96
|
SELECT DISTINCT o.id, o.type, o.title, o.lesson_learned
|
|
93
97
|
FROM observations o
|
|
94
98
|
JOIN observation_files of2 ON of2.obs_id = o.id
|
|
95
99
|
WHERE o.project = ?
|
|
96
100
|
AND o.importance >= 2
|
|
97
|
-
AND o.lesson_learned IS NOT NULL
|
|
98
|
-
AND o.lesson_learned != ''
|
|
99
101
|
AND COALESCE(o.compressed_into, 0) = 0
|
|
100
102
|
AND o.superseded_at IS NULL
|
|
101
103
|
AND o.created_at_epoch > ?
|
|
102
104
|
AND (of2.filename = ? OR of2.filename LIKE ? ESCAPE '\\')
|
|
103
|
-
|
|
105
|
+
AND (
|
|
106
|
+
(o.lesson_learned IS NOT NULL AND o.lesson_learned != '')
|
|
107
|
+
OR o.type IN ('bugfix', 'decision')
|
|
108
|
+
)
|
|
109
|
+
ORDER BY
|
|
110
|
+
CASE WHEN o.lesson_learned IS NOT NULL AND o.lesson_learned != '' THEN 0 ELSE 1 END,
|
|
111
|
+
o.created_at_epoch DESC
|
|
104
112
|
LIMIT 2
|
|
105
113
|
`).all(project, cutoff, filePath, likePattern);
|
|
106
114
|
|
|
107
115
|
if (rows.length > 0) {
|
|
108
116
|
console.log(`[mem] Lessons for ${fname}:`);
|
|
109
117
|
for (const r of rows) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
if (r.lesson_learned) {
|
|
119
|
+
const lesson = r.lesson_learned.length > 120
|
|
120
|
+
? r.lesson_learned.slice(0, 117) + '...'
|
|
121
|
+
: r.lesson_learned;
|
|
122
|
+
console.log(` #${r.id} [${r.type}] ${lesson}`);
|
|
123
|
+
} else {
|
|
124
|
+
const title = (r.title || '').length > 120
|
|
125
|
+
? r.title.slice(0, 117) + '...'
|
|
126
|
+
: (r.title || '');
|
|
127
|
+
console.log(` #${r.id} [${r.type}] ${title}`);
|
|
128
|
+
}
|
|
114
129
|
}
|
|
115
130
|
// Update cooldown
|
|
116
131
|
cooldown[filePath] = now;
|
|
@@ -25,12 +25,32 @@ export function shouldSkip(text) {
|
|
|
25
25
|
// ─── Intent Detection ───────────────────────────────────────────────────────
|
|
26
26
|
|
|
27
27
|
export const INTENTS = [
|
|
28
|
-
// Error/debug intent
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
// Error/debug intent — highest priority, most actionable
|
|
29
|
+
// CJK: 不工作/有问题/挂了 from real prompts; 异常/失败/排查/定位/诊断 from dev vocabulary
|
|
30
|
+
{ pattern: /error|bug|crash|broken|fail(?:ed|ing|ure)?|fix(?:ed|ing)?|debug|调试|报错|出错|错误|崩溃|修复|故障|不工作|有问题|出了问题|挂了|异常|失败|解决|排查|定位|诊断/i, type: 'bugfix', limit: 3 },
|
|
31
|
+
// Test intent — test failures surface bugfix memories
|
|
32
|
+
// CJK: 跑测试/写测试/测试用例/覆盖率 from real prompts
|
|
33
|
+
{ pattern: /\btest(?:s|ing)?\b|spec\b|assert|单元测试|测试失败|test fail|测试|跑测试|写测试|测试用例|覆盖率/i, type: 'bugfix', limit: 3 },
|
|
34
|
+
// Review/audit intent — from real data: 审查(6x), 检查(9x), 审核, 代码审核
|
|
35
|
+
{ pattern: /\breview\b|audit|inspect|审查|审核|检查|代码审核|审阅|code.?review/i, type: 'discovery', limit: 3 },
|
|
36
|
+
// Refactor intent — surface past refactor decisions and patterns
|
|
37
|
+
// CJK: 拆分/提取/简化/解耦/清理 from real prompts; 优化代码 = refactor (not perf)
|
|
38
|
+
{ pattern: /refactor|restructur|cleanup|clean up|重构|整理|代码质量|拆分|提取|简化|解耦|清理/i, type: 'refactor', limit: 3 },
|
|
39
|
+
// Performance intent — before decision (so "slow" doesn't get classified as decision)
|
|
40
|
+
// CJK: 卡顿/超时/内存泄漏/优化 from real prompts; 加速/提速 from dev vocabulary
|
|
41
|
+
{ pattern: /performance|perf\b|slow|latency|bottleneck|optimiz|性能|慢|延迟|耗时|效率低|卡顿|超时|内存泄漏|优化|加速|提速/i, type: 'discovery', limit: 3 },
|
|
42
|
+
// Decision/architecture intent
|
|
43
|
+
// CJK: 方案/原因/考虑/权衡/思路 from real prompts
|
|
44
|
+
{ pattern: /why\b|decided|architecture|design\b|为什么|决定|架构|设计|方案|原因|考虑|权衡|思路/i, type: 'decision', limit: 3 },
|
|
45
|
+
// Database/schema intent — surface migration decisions
|
|
46
|
+
// CJK: 索引/查询/建表/改表 from dev vocabulary
|
|
47
|
+
{ pattern: /schema|migration|数据库|迁移|database\b|表结构|字段|索引|查询|建表|改表/i, type: 'decision', limit: 3 },
|
|
48
|
+
// Implementation intent — surface related feature history (no type filter for broader recall)
|
|
49
|
+
// CJK: 开发/编写/创建/构建/做一个/写一个 from real prompts
|
|
50
|
+
{ pattern: /implement|feature\b|add\s+(?:a\s+)?new|实现|添加|新功能|新增|开发|编写|创建|构建|做一个|加一个|写一个/i, type: null, limit: 3 },
|
|
32
51
|
// Recall/history intent (catch-all temporal, lowest priority)
|
|
33
|
-
|
|
52
|
+
// CJK: 刚才/历史/回顾 from real prompts
|
|
53
|
+
{ pattern: /before|previously|last time|remember|之前|上次|以前|记得|刚才|历史|回顾/i, type: null, limit: 5, useRecent: true },
|
|
34
54
|
];
|
|
35
55
|
|
|
36
56
|
export function detectIntent(text) {
|
|
@@ -42,15 +62,15 @@ export function detectIntent(text) {
|
|
|
42
62
|
if (matches.length === 0) return null;
|
|
43
63
|
if (matches.length === 1) return matches[0];
|
|
44
64
|
|
|
45
|
-
// Disambiguation:
|
|
46
|
-
// position-based resolution — the pattern appearing earlier in text wins.
|
|
65
|
+
// Disambiguation: when recall intent overlaps with an actionable intent,
|
|
66
|
+
// use position-based resolution — the pattern appearing earlier in text wins.
|
|
47
67
|
// "I remember we fixed..." → recall leads. "fix the bug from before" → bugfix leads.
|
|
48
68
|
const first = matches[0];
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
const
|
|
52
|
-
const recallPos = text.search(
|
|
53
|
-
if (recallPos <
|
|
69
|
+
const recallMatch = matches.find(m => m.useRecent);
|
|
70
|
+
if (recallMatch && first !== recallMatch) {
|
|
71
|
+
const actionPos = text.search(first.pattern);
|
|
72
|
+
const recallPos = text.search(recallMatch.pattern);
|
|
73
|
+
if (recallPos < actionPos) return recallMatch;
|
|
54
74
|
}
|
|
55
75
|
return first;
|
|
56
76
|
}
|
|
@@ -112,8 +132,13 @@ export function matchRegistrySkillName(text, skillNames) {
|
|
|
112
132
|
|
|
113
133
|
// ─── File Path Detection ─────────────────────────────────────────────────────
|
|
114
134
|
|
|
115
|
-
/** Detect file paths in text */
|
|
135
|
+
/** Detect file paths in text — excludes URLs and pure version numbers */
|
|
116
136
|
export function extractFiles(text) {
|
|
117
137
|
const matches = text.match(/[\w./-]+\.\w{1,10}/g) || [];
|
|
118
|
-
return matches.filter(m =>
|
|
138
|
+
return matches.filter(m =>
|
|
139
|
+
m.includes('.') &&
|
|
140
|
+
!m.startsWith('http') &&
|
|
141
|
+
!m.includes('//') &&
|
|
142
|
+
!/^\d+\.\d+$/.test(m) // Exclude pure version numbers like "3.14" (not paths like "1.0/config.json")
|
|
143
|
+
);
|
|
119
144
|
}
|
package/server.mjs
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
7
|
import { jaccardSimilarity, truncate, typeIcon, sanitizeFtsQuery, relaxFtsQueryToOr, inferProject, computeMinHash, estimateJaccardFromMinHash, scrubSecrets, cjkBigrams, fmtDate, isoWeekKey, debugLog, debugCatch, COMPRESSED_PENDING_PURGE, OBS_BM25, SESS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, getCurrentBranch, DEFAULT_DECAY_HALF_LIFE_MS, isPathConfined } from './utils.mjs';
|
|
8
|
+
import { extractCjkLikePatterns } from './nlp.mjs';
|
|
8
9
|
import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
|
|
9
10
|
import { ensureDb, DB_PATH, REGISTRY_DB_PATH, checkFTSIntegrity, rebuildFTS } from './schema.mjs';
|
|
10
11
|
import { reRankWithContext, markSuperseded, extractPRFTerms, expandQueryByConcepts, autoBoostIfNeeded, runIdleCleanup } from './server-internals.mjs';
|
|
11
12
|
import { computeTier, TIER_CASE_SQL, tierSqlParams } from './tier.mjs';
|
|
12
|
-
import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema } from './tool-schemas.mjs';
|
|
13
|
+
import { memSearchSchema, memRecentSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema, memMaintainSchema, memOptimizeSchema, memUpdateSchema, memExportSchema, memRecallSchema, memFtsCheckSchema, memRegistrySchema, memBrowseSchema, memUseSchema } from './tool-schemas.mjs';
|
|
14
|
+
import { optimizePreview, optimizeRun } from './hook-optimize.mjs';
|
|
13
15
|
import { basename, join } from 'path';
|
|
14
16
|
import { homedir } from 'os';
|
|
15
17
|
import { ensureRegistryDb, upsertResource } from './registry.mjs';
|
|
@@ -110,12 +112,14 @@ const server = new McpServer(
|
|
|
110
112
|
'mem_save: Save non-obvious insights (bugfix lessons, architecture decisions).',
|
|
111
113
|
'Search tips: short keywords (2-3 words), filter with obs_type when relevant.',
|
|
112
114
|
'',
|
|
113
|
-
'WHEN TO USE (proactive triggers):',
|
|
114
|
-
' •
|
|
115
|
-
' •
|
|
116
|
-
' •
|
|
117
|
-
' •
|
|
118
|
-
' •
|
|
115
|
+
'WHEN TO USE (proactive triggers during coding):',
|
|
116
|
+
' • About to Edit/Write a file → mem_recall(file="path") FIRST — past bugfixes & lessons',
|
|
117
|
+
' • Test failure or error → mem_search(query="error keywords", obs_type="bugfix")',
|
|
118
|
+
' • Before refactoring → mem_search(query="module-name", obs_type="refactor") for past decisions',
|
|
119
|
+
' • Starting new feature → mem_search(query="feature area") for prior art & patterns',
|
|
120
|
+
' • After fixing a tricky bug → mem_save(type="bugfix", lesson_learned="root cause & fix")',
|
|
121
|
+
' • After architecture decision → mem_save(type="decision", lesson_learned="rationale")',
|
|
122
|
+
' • Hook-injected context mentions #ID → mem_get(ids=[ID]) for full details',
|
|
119
123
|
'',
|
|
120
124
|
'Decision rules (use INSTEAD OF multi-step search):',
|
|
121
125
|
' • "what happened recently?" → mem_recent (NOT search with empty query)',
|
|
@@ -453,6 +457,35 @@ function searchPrompts(ctx) {
|
|
|
453
457
|
for (const r of rows) {
|
|
454
458
|
results.push({ source: 'prompt', id: r.id, text: r.prompt_text, session: r.content_session_id, date: r.created_at, score: r.score });
|
|
455
459
|
}
|
|
460
|
+
// CJK LIKE fallback: FTS5 unicode61 can't tokenize CJK substrings in prompts
|
|
461
|
+
if (rows.length === 0 && args.query) {
|
|
462
|
+
const cjkPatterns = extractCjkLikePatterns(args.query);
|
|
463
|
+
if (cjkPatterns.length > 0) {
|
|
464
|
+
const likeConds = cjkPatterns.map(() => 'p.prompt_text LIKE ?');
|
|
465
|
+
const likeParams = cjkPatterns.map(p => `%${p}%`);
|
|
466
|
+
const fallbackRows = db.prepare(`
|
|
467
|
+
SELECT p.id, p.prompt_text, p.content_session_id, p.created_at
|
|
468
|
+
FROM user_prompts p
|
|
469
|
+
JOIN sdk_sessions s ON p.content_session_id = s.content_session_id
|
|
470
|
+
WHERE (${likeConds.join(' OR ')})
|
|
471
|
+
AND p.prompt_text NOT LIKE '<task-notification>%'
|
|
472
|
+
AND (? IS NULL OR s.project = ?)
|
|
473
|
+
AND (? IS NULL OR p.created_at_epoch >= ?)
|
|
474
|
+
AND (? IS NULL OR p.created_at_epoch <= ?)
|
|
475
|
+
ORDER BY p.created_at_epoch DESC
|
|
476
|
+
LIMIT ? OFFSET ?
|
|
477
|
+
`).all(
|
|
478
|
+
...likeParams,
|
|
479
|
+
args.project ?? null, args.project ?? null,
|
|
480
|
+
epochFrom, epochFrom,
|
|
481
|
+
epochTo, epochTo,
|
|
482
|
+
perSourceLimit, perSourceOffset
|
|
483
|
+
);
|
|
484
|
+
for (const r of fallbackRows) {
|
|
485
|
+
results.push({ source: 'prompt', id: r.id, text: r.prompt_text, session: r.content_session_id, date: r.created_at, score: 0 });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
456
489
|
} else if (searchType === 'prompts') {
|
|
457
490
|
const params = [];
|
|
458
491
|
const wheres = [];
|
|
@@ -524,7 +557,7 @@ function formatSearchOutput(paginatedResults, args, ftsQuery, totalCount, isCros
|
|
|
524
557
|
server.registerTool(
|
|
525
558
|
'mem_search',
|
|
526
559
|
{
|
|
527
|
-
description: 'Search project memory for past bugfixes, decisions, and discoveries. Use when: encountering
|
|
560
|
+
description: 'Search project memory for past bugfixes, decisions, and discoveries. Use proactively when: encountering an error (search with obs_type="bugfix"), investigating a module before changes, or looking for prior art. Returns compact index (use mem_get for full details).',
|
|
528
561
|
inputSchema: memSearchSchema,
|
|
529
562
|
},
|
|
530
563
|
safeHandler(async (args) => {
|
|
@@ -699,7 +732,7 @@ server.registerTool(
|
|
|
699
732
|
server.registerTool(
|
|
700
733
|
'mem_timeline',
|
|
701
734
|
{
|
|
702
|
-
description: 'Browse observations as a timeline around an anchor point. Use when: exploring what happened before/after a specific observation, understanding the sequence of changes that led to a bug, or reviewing a session chronologically.',
|
|
735
|
+
description: 'Browse observations as a timeline around an anchor point. Accepts anchor ID or a query string to auto-find the anchor via FTS. Use when: exploring what happened before/after a specific observation, understanding the sequence of changes that led to a bug, or reviewing a session chronologically. Example: mem_timeline(query="FTS5 search bug") or mem_timeline(anchor=42).',
|
|
703
736
|
inputSchema: memTimelineSchema,
|
|
704
737
|
},
|
|
705
738
|
safeHandler(async (args) => {
|
|
@@ -802,7 +835,7 @@ server.registerTool(
|
|
|
802
835
|
server.registerTool(
|
|
803
836
|
'mem_get',
|
|
804
837
|
{
|
|
805
|
-
description: 'Get full details for one or more records by ID. Use when: hook-injected context mentions a relevant observation ID, or after mem_search to drill into specific results for narrative, lesson_learned, and file details.',
|
|
838
|
+
description: 'Get full details for one or more records by ID. Use when: hook-injected context mentions a relevant observation ID, or after mem_search to drill into specific results for narrative, lesson_learned, and file details. For session results (S#15), pass source="session". For prompt results (P#22), pass source="prompt".',
|
|
806
839
|
inputSchema: memGetSchema,
|
|
807
840
|
},
|
|
808
841
|
safeHandler(async (args) => {
|
|
@@ -925,7 +958,7 @@ server.registerTool(
|
|
|
925
958
|
server.registerTool(
|
|
926
959
|
'mem_save',
|
|
927
960
|
{
|
|
928
|
-
description: 'Save a memory/observation. Use
|
|
961
|
+
description: 'Save a memory/observation with optional lesson_learned. Use after: solving a non-obvious bug (pass lesson_learned="root cause & fix"), making an architecture decision (pass lesson_learned="rationale"), or discovering something not obvious from code. Also when user asks to remember something.',
|
|
929
962
|
inputSchema: memSaveSchema,
|
|
930
963
|
},
|
|
931
964
|
safeHandler(async (args) => {
|
|
@@ -960,18 +993,20 @@ server.registerTool(
|
|
|
960
993
|
|
|
961
994
|
const safeContent = scrubSecrets(args.content);
|
|
962
995
|
const safeTitle = scrubSecrets(title);
|
|
996
|
+
const safeLesson = args.lesson_learned ? scrubSecrets(args.lesson_learned) : null;
|
|
963
997
|
const minhashSig = computeMinHash(safeTitle + ' ' + safeContent);
|
|
964
998
|
// Append CJK bigrams to text field for FTS5 indexing of Chinese content
|
|
965
|
-
const
|
|
999
|
+
const indexText = [safeTitle, safeContent, safeLesson].filter(Boolean).join(' ');
|
|
1000
|
+
const bigramText = cjkBigrams(indexText);
|
|
966
1001
|
const textField = bigramText ? safeContent + ' ' + bigramText : safeContent;
|
|
967
1002
|
|
|
968
1003
|
// Atomic: insert observation + observation_files + TF-IDF vector in one transaction
|
|
969
1004
|
const saveFiles = args.files || [];
|
|
970
1005
|
const saveTx = db.transaction(() => {
|
|
971
1006
|
const result = db.prepare(`
|
|
972
|
-
INSERT INTO observations (memory_session_id, project, text, type, title, narrative, concepts, facts, files_read, files_modified, importance, minhash_sig, branch, created_at, created_at_epoch)
|
|
973
|
-
VALUES (?, ?, ?, ?, ?, ?, '', '', '[]', ?, ?, ?, ?, ?, ?)
|
|
974
|
-
`).run(sessionId, project, textField, type, safeTitle, safeContent, JSON.stringify(saveFiles), args.importance ?? 2, minhashSig, getCurrentBranch(), now.toISOString(), now.getTime());
|
|
1007
|
+
INSERT INTO observations (memory_session_id, project, text, type, title, narrative, concepts, facts, files_read, files_modified, importance, minhash_sig, lesson_learned, branch, created_at, created_at_epoch)
|
|
1008
|
+
VALUES (?, ?, ?, ?, ?, ?, '', '', '[]', ?, ?, ?, ?, ?, ?, ?)
|
|
1009
|
+
`).run(sessionId, project, textField, type, safeTitle, safeContent, JSON.stringify(saveFiles), args.importance ?? 2, minhashSig, safeLesson, getCurrentBranch(), now.toISOString(), now.getTime());
|
|
975
1010
|
const savedId = Number(result.lastInsertRowid);
|
|
976
1011
|
|
|
977
1012
|
// Populate observation_files junction table
|
|
@@ -998,7 +1033,8 @@ server.registerTool(
|
|
|
998
1033
|
});
|
|
999
1034
|
const result = saveTx();
|
|
1000
1035
|
|
|
1001
|
-
|
|
1036
|
+
const lessonNote = safeLesson ? ` 💡lesson captured` : '';
|
|
1037
|
+
return { content: [{ type: 'text', text: `Saved as observation #${result.lastInsertRowid} [${type}] in project "${project}".${lessonNote}` }] };
|
|
1002
1038
|
})
|
|
1003
1039
|
);
|
|
1004
1040
|
|
|
@@ -1486,6 +1522,50 @@ server.registerTool(
|
|
|
1486
1522
|
})
|
|
1487
1523
|
);
|
|
1488
1524
|
|
|
1525
|
+
// ─── Tool: mem_optimize ────────────────────────────────────────────────────
|
|
1526
|
+
|
|
1527
|
+
server.registerTool(
|
|
1528
|
+
'mem_optimize',
|
|
1529
|
+
{
|
|
1530
|
+
description: 'LLM-powered database optimization: re-enrich degraded records, normalize concepts, merge related observations, smart-compress old data. Use when: database quality seems low, search results are noisy, or for periodic deep maintenance.',
|
|
1531
|
+
inputSchema: memOptimizeSchema,
|
|
1532
|
+
},
|
|
1533
|
+
safeHandler(async (args) => {
|
|
1534
|
+
const action = args.action || 'preview';
|
|
1535
|
+
|
|
1536
|
+
if (action === 'preview') {
|
|
1537
|
+
const preview = optimizePreview(db);
|
|
1538
|
+
const lines = [
|
|
1539
|
+
`🔍 LLM Optimization Preview:`,
|
|
1540
|
+
` Re-enrich candidates: ${preview.reenrich}`,
|
|
1541
|
+
` Normalize: ${preview.normalizeGateOpen ? `${preview.normalize} unique concepts` : 'gate closed (7-day interval)'}`,
|
|
1542
|
+
` Cluster-merge candidates: ${preview.clusterMerge} clusters`,
|
|
1543
|
+
` Smart-compress candidates: ${preview.smartCompress} clusters`,
|
|
1544
|
+
` Total: ${preview.total} items`,
|
|
1545
|
+
];
|
|
1546
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
const force = action === 'run_all';
|
|
1550
|
+
const results = await optimizeRun(db, {
|
|
1551
|
+
tasks: args.tasks,
|
|
1552
|
+
maxItems: args.max_items || 15,
|
|
1553
|
+
force,
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
const lines = ['🔧 LLM Optimization Results:'];
|
|
1557
|
+
if (results.reenrich) lines.push(` Re-enrich: ${results.reenrich.processed || 0} processed, ${results.reenrich.skipped || 0} skipped`);
|
|
1558
|
+
if (results.normalize) {
|
|
1559
|
+
if (results.normalize.skipped) lines.push(` Normalize: skipped (${results.normalize.reason})`);
|
|
1560
|
+
else lines.push(` Normalize: ${results.normalize.processed || 0} updated, ${results.normalize.groups || 0} synonym groups`);
|
|
1561
|
+
}
|
|
1562
|
+
if (results.clusterMerge) lines.push(` Cluster-merge: ${results.clusterMerge.merged || 0} merged of ${results.clusterMerge.processed || 0} clusters`);
|
|
1563
|
+
if (results.smartCompress) lines.push(` Smart-compress: ${results.smartCompress.compressed || 0} compressed of ${results.smartCompress.processed || 0} clusters`);
|
|
1564
|
+
|
|
1565
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
1566
|
+
})
|
|
1567
|
+
);
|
|
1568
|
+
|
|
1489
1569
|
// ─── Tool: mem_registry ─────────────────────────────────────────────────────
|
|
1490
1570
|
|
|
1491
1571
|
server.registerTool(
|
|
@@ -1872,7 +1952,7 @@ server.registerTool(
|
|
|
1872
1952
|
server.registerTool(
|
|
1873
1953
|
'mem_recall',
|
|
1874
1954
|
{
|
|
1875
|
-
description: 'Recall observations related to a file.
|
|
1955
|
+
description: 'Recall observations related to a file. ALWAYS use before editing a file with known issues. Also use when: investigating a file, or before refactoring to recall past bugfixes, decisions, and context.',
|
|
1876
1956
|
inputSchema: memRecallSchema,
|
|
1877
1957
|
},
|
|
1878
1958
|
safeHandler(async (args) => {
|
package/skill.md
CHANGED
|
@@ -1,35 +1,22 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mem
|
|
3
|
-
description:
|
|
3
|
+
description: "Use when: querying past work, managing memories, checking project history, or saving session findings"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Memory
|
|
7
|
-
|
|
8
|
-
Search and browse your project memory efficiently.
|
|
6
|
+
# Memory
|
|
9
7
|
|
|
10
8
|
## Commands
|
|
11
9
|
|
|
12
|
-
- `/mem search <query>` — FTS5 full-text search
|
|
13
|
-
- `/mem recent [n]` —
|
|
14
|
-
- `/mem
|
|
15
|
-
- `/mem
|
|
16
|
-
- `/mem
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
1. **Search** → `mem_search(query="...")` → get compact ID index
|
|
21
|
-
2. **Browse** → `mem_timeline(anchor=ID)` → see surrounding context
|
|
22
|
-
3. **Detail** → `mem_get(ids=[...])` → get full content for specific IDs
|
|
23
|
-
|
|
24
|
-
## Instructions
|
|
25
|
-
|
|
26
|
-
When the user invokes `/mem`, parse their intent:
|
|
10
|
+
- `/mem search <query>` — FTS5 full-text search
|
|
11
|
+
- `/mem recent [n]` — Recent observations (default 5)
|
|
12
|
+
- `/mem recall <file>` — File history before editing
|
|
13
|
+
- `/mem timeline <id>` — Browse around an observation
|
|
14
|
+
- `/mem save <text>` — Save a note
|
|
15
|
+
- `/mem stats` — Memory statistics
|
|
16
|
+
- `/mem cleanup [Nd]` — Purge stale data
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
- `/mem recent` or `/mem recent 20` → call `mem_search` with no query, limit=N
|
|
30
|
-
- `/mem save <text>` → call `mem_save` with the text as content
|
|
31
|
-
- `/mem stats` → call `mem_stats`
|
|
32
|
-
- `/mem timeline <query>` → call `mem_timeline` with the query
|
|
33
|
-
- `/mem <query>` (no subcommand) → treat as search, call `mem_search`
|
|
18
|
+
## Efficient Workflow (saves 10x tokens)
|
|
34
19
|
|
|
35
|
-
|
|
20
|
+
1. `mem_search(query)` → compact ID index
|
|
21
|
+
2. `mem_timeline(anchor=ID)` → surrounding context
|
|
22
|
+
3. `mem_get(ids=[...])` → full content only when needed
|
package/synonyms.mjs
CHANGED
|
@@ -71,6 +71,43 @@ export const SYNONYM_PAIRS = [
|
|
|
71
71
|
['debug', 'troubleshoot'],
|
|
72
72
|
['error', 'failure'],
|
|
73
73
|
['migrate', 'migration'],
|
|
74
|
+
// ─── Concurrency & Async ───
|
|
75
|
+
['promise', 'async'],
|
|
76
|
+
['callback', 'handler'],
|
|
77
|
+
['deadlock', 'race condition'],
|
|
78
|
+
['mutex', 'lock'],
|
|
79
|
+
['semaphore', 'lock'],
|
|
80
|
+
['concurrent', 'parallel'],
|
|
81
|
+
['thread', 'worker'],
|
|
82
|
+
['await', 'async'],
|
|
83
|
+
// ─── Type System ───
|
|
84
|
+
['interface', 'type'],
|
|
85
|
+
['generic', 'generics'],
|
|
86
|
+
['typedef', 'type'],
|
|
87
|
+
['enum', 'enumeration'],
|
|
88
|
+
// ─── Testing ───
|
|
89
|
+
['mock', 'stub'],
|
|
90
|
+
['mock', 'fake'],
|
|
91
|
+
['fixture', 'testdata'],
|
|
92
|
+
['assert', 'assertion'],
|
|
93
|
+
['jest', 'vitest'],
|
|
94
|
+
['pytest', 'unittest'],
|
|
95
|
+
['cypress', 'playwright'],
|
|
96
|
+
// ─── Networking ───
|
|
97
|
+
['http', 'request'],
|
|
98
|
+
['rest', 'api'],
|
|
99
|
+
['grpc', 'rpc'],
|
|
100
|
+
['timeout', 'deadline'],
|
|
101
|
+
['retry', 'backoff'],
|
|
102
|
+
['cors', 'cross origin'],
|
|
103
|
+
['ssl', 'tls'],
|
|
104
|
+
['throttle', 'rate limit'],
|
|
105
|
+
// ─── Build Tools ───
|
|
106
|
+
['webpack', 'bundler'],
|
|
107
|
+
['vite', 'bundler'],
|
|
108
|
+
['rollup', 'bundler'],
|
|
109
|
+
['esbuild', 'bundler'],
|
|
110
|
+
['transpile', 'compile'],
|
|
74
111
|
// ─── CJK ↔ EN cross-language synonyms ───
|
|
75
112
|
// Authentication & Authorization
|
|
76
113
|
['认证', 'auth'], ['认证', 'authentication'], ['登录', 'login'], ['登录', 'auth'],
|
|
@@ -108,6 +145,16 @@ export const SYNONYM_PAIRS = [
|
|
|
108
145
|
['钩子', 'hook'], ['回调', 'callback'],
|
|
109
146
|
['异步', 'async'], ['同步', 'sync'],
|
|
110
147
|
['并发', 'concurrent'], ['线程', 'thread'],
|
|
148
|
+
['竞态', 'race condition'], ['死锁', 'deadlock'], ['互斥', 'mutex'],
|
|
149
|
+
['协程', 'coroutine'], ['事件循环', 'event loop'],
|
|
150
|
+
['泛型', 'generic'], ['枚举', 'enum'],
|
|
151
|
+
['断言', 'assert'], ['单元测试', 'unit test'], ['集成测试', 'integration test'],
|
|
152
|
+
['模拟测试', 'mock'], ['测试覆盖', 'coverage'],
|
|
153
|
+
['错误处理', 'error handling'], ['异常捕获', 'try catch'], ['堆栈跟踪', 'stack trace'],
|
|
154
|
+
['容错', 'fault tolerance'],
|
|
155
|
+
['跨域', 'cors'], ['限流', 'rate limit'], ['熔断', 'circuit breaker'],
|
|
156
|
+
['负载均衡', 'load balancing'], ['心跳', 'heartbeat'],
|
|
157
|
+
['请求', 'request'], ['失败', 'failure'], ['覆盖率', 'coverage'],
|
|
111
158
|
// Performance
|
|
112
159
|
['性能', 'performance'], ['性能', 'perf'],
|
|
113
160
|
['内存', 'memory'], ['泄漏', 'leak'],
|
|
@@ -140,6 +187,23 @@ export const SYNONYM_PAIRS = [
|
|
|
140
187
|
['安装', 'install'], ['导入', 'import'],
|
|
141
188
|
['导出', 'export'], ['状态', 'state'],
|
|
142
189
|
['系统', 'system'], ['算法', 'algorithm'],
|
|
190
|
+
// Common dev terms (mined from real usage data)
|
|
191
|
+
['文件', 'file'], ['代码', 'code'],
|
|
192
|
+
['执行', 'execute'], ['执行', 'run'],
|
|
193
|
+
['调用', 'call'], ['调用', 'invoke'],
|
|
194
|
+
['运行', 'run'], ['运行', 'execute'],
|
|
195
|
+
['检查', 'check'], ['检查', 'inspect'],
|
|
196
|
+
['分析', 'analyze'], ['分析', 'analysis'],
|
|
197
|
+
['项目', 'project'], ['流程', 'workflow'],
|
|
198
|
+
['更新', 'update'], ['提示', 'prompt'],
|
|
199
|
+
['查找', 'find'], ['记录', 'record'],
|
|
200
|
+
['记录', 'log'], ['校验', 'validate'],
|
|
201
|
+
['计算', 'compute'], ['计算', 'calculate'],
|
|
202
|
+
['单元', 'unit'], ['资源', 'resource'],
|
|
203
|
+
['问题', 'issue'], ['问题', 'problem'],
|
|
204
|
+
['历史', 'history'], ['描述', 'description'],
|
|
205
|
+
['推荐', 'recommend'], ['建议', 'suggestion'],
|
|
206
|
+
['智能', 'smart'], ['智能', 'intelligent'],
|
|
143
207
|
];
|
|
144
208
|
|
|
145
209
|
// ─── Bidirectional SYNONYM_MAP (case-insensitive) ──────────────────────────────
|
|
@@ -168,11 +232,25 @@ export const CJK_COMPOUNDS = new Set([
|
|
|
168
232
|
'认证', '授权', '加密', '解密', '序列', '并发', '异步', '同步', '线程', '进程',
|
|
169
233
|
'容器', '集群', '服务器', '中间件', '网关', '负载', '监控', '日志', '告警',
|
|
170
234
|
'前端', '后端', '全栈', '响应式', '路由', '状态', '渲染', '样式', '布局',
|
|
235
|
+
'代码', '文件', '项目', '资源', '单元', '智能',
|
|
236
|
+
// concurrency & async (extended)
|
|
237
|
+
'竞态', '死锁', '互斥', '协程', '回调', '事件循环',
|
|
238
|
+
// type system
|
|
239
|
+
'泛型', '枚举', '联合类型', '类型推断', '类型守卫', '接口定义',
|
|
240
|
+
// testing (extended)
|
|
241
|
+
'单元测试', '集成测试', '端到端', '测试覆盖', '覆盖率', '模拟测试', '断言',
|
|
242
|
+
// error handling (extended)
|
|
243
|
+
'错误处理', '异常捕获', '堆栈跟踪', '错误码', '容错', '失败', '请求',
|
|
244
|
+
// networking
|
|
245
|
+
'跨域', '限流', '熔断', '负载均衡', '心跳',
|
|
171
246
|
// actions
|
|
172
247
|
'修复', '重构', '优化', '升级', '安装', '卸载', '导入', '导出', '上传', '下载',
|
|
173
248
|
'提交', '推送', '合并', '发布', '上线', '回退', '审查', '审核', '评审',
|
|
249
|
+
'执行', '调用', '运行', '检查', '分析', '查找', '计算', '校验', '更新', '描述',
|
|
174
250
|
// errors/issues
|
|
175
|
-
'报错', '崩溃', '泄露', '溢出', '
|
|
251
|
+
'报错', '崩溃', '泄露', '溢出', '超时', '中断', '异常', '故障',
|
|
252
|
+
// general (tokenization only — keeps CJK segmentation clean)
|
|
253
|
+
'问题', '使用', '继续', '推荐', '建议', '历史', '记录', '中文', '提示', '流程',
|
|
176
254
|
// architecture
|
|
177
255
|
'架构', '设计', '方案', '规划', '文档', '注释', '版本', '分支', '依赖',
|
|
178
256
|
'性能', '安全', '漏洞', '补丁', '系统', '算法',
|
package/tool-schemas.mjs
CHANGED
|
@@ -74,6 +74,7 @@ export const memSaveSchema = {
|
|
|
74
74
|
project: z.string().optional().describe('Project name (default: inferred from CWD)'),
|
|
75
75
|
importance: coerceInt.pipe(z.number().int().min(1).max(3)).optional().describe('Importance level: 1=routine, 2=notable, 3=critical (default: 2 for explicit saves)'),
|
|
76
76
|
files: z.array(z.string()).optional().describe('File paths associated with this observation'),
|
|
77
|
+
lesson_learned: z.string().max(500).optional().describe('Key lesson or takeaway (for bugfix: root cause & fix; for decision: rationale)'),
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
export const memStatsSchema = {
|
|
@@ -87,6 +88,15 @@ export const memCompressSchema = {
|
|
|
87
88
|
project: z.string().optional().describe('Filter by project'),
|
|
88
89
|
};
|
|
89
90
|
|
|
91
|
+
export const memOptimizeSchema = {
|
|
92
|
+
action: z.enum(['preview', 'run', 'run_all']).optional().default('preview')
|
|
93
|
+
.describe('preview=scan candidates, run=execute with limits, run_all=bypass gates'),
|
|
94
|
+
tasks: z.array(z.enum(['re-enrich', 'normalize', 'cluster-merge', 'smart-compress'])).optional()
|
|
95
|
+
.describe('Which optimization tasks to run (default: all)'),
|
|
96
|
+
max_items: coerceInt.pipe(z.number().int().min(1).max(100)).optional().default(15)
|
|
97
|
+
.describe('Maximum LLM calls across all tasks (default: 15)'),
|
|
98
|
+
};
|
|
99
|
+
|
|
90
100
|
export const memMaintainSchema = {
|
|
91
101
|
action: z.enum(['scan', 'execute']).describe('scan=analyze candidates, execute=apply changes'),
|
|
92
102
|
operations: z.array(z.enum(['dedup', 'decay', 'cleanup', 'boost', 'purge_stale', 'rebuild_vectors'])).optional()
|
package/utils.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { execSync } from 'child_process';
|
|
|
9
9
|
// Backward compatibility: all consumers import from utils.mjs
|
|
10
10
|
|
|
11
11
|
export { DECAY_HALF_LIFE_BY_TYPE, DEFAULT_DECAY_HALF_LIFE_MS, OBS_BM25, SESS_BM25, TYPE_DECAY_CASE, TYPE_QUALITY_CASE, OBS_FTS_COLUMNS } from './scoring-sql.mjs';
|
|
12
|
-
export { cjkBigrams, extractCjkSynonymTokens, extractCjkKeywords, SYNONYM_MAP, expandToken, sanitizeFtsQuery, relaxFtsQueryToOr, FTS_STOP_WORDS, CJK_COMPOUNDS } from './nlp.mjs';
|
|
12
|
+
export { cjkBigrams, extractCjkSynonymTokens, extractCjkKeywords, extractCjkLikePatterns, SYNONYM_MAP, expandToken, sanitizeFtsQuery, relaxFtsQueryToOr, FTS_STOP_WORDS, CJK_COMPOUNDS } from './nlp.mjs';
|
|
13
13
|
export { resolveProject, _resetProjectCache } from './project-utils.mjs';
|
|
14
14
|
export { scrubSecrets, SECRET_PATTERNS } from './secret-scrub.mjs';
|
|
15
15
|
export { truncate, typeIcon, fmtDate, fmtTime, isoWeekKey } from './format-utils.mjs';
|
package/commands/recall.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Recall past observations for a file before editing. Use when: about to edit a file, investigating a file with past issues, or before refactoring to check for past lessons"
|
|
3
|
-
argument-hint: <file_path>
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## File Memory
|
|
7
|
-
!`claude-mem-lite recall $ARGUMENTS 2>/dev/null || echo "No history found"`
|
|
8
|
-
|
|
9
|
-
Consider these past observations before making changes.
|
package/commands/recent.md
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Show recent memory observations. Use when: checking what happened recently, reviewing session progress, or verifying recent changes were captured"
|
|
3
|
-
argument-hint: [count]
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## Recent Observations
|
|
7
|
-
!`claude-mem-lite recent $ARGUMENTS 2>/dev/null || echo "No observations"`
|
package/commands/search.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Search memory for past bugfixes, decisions, discoveries. Use when: encountering a familiar error, investigating a module before changes, or looking for prior solutions to a similar problem"
|
|
3
|
-
argument-hint: <query>
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## Memory Search
|
|
7
|
-
!`claude-mem-lite search $ARGUMENTS 2>/dev/null || echo "No results found"`
|
|
8
|
-
|
|
9
|
-
Use `claude-mem-lite get <id>` via Bash for full details on any result.
|
package/commands/timeline.md
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: "Browse memory timeline around an observation. Use when: exploring what happened before/after a specific event, understanding the sequence of changes that led to a bug, or reviewing chronological context"
|
|
3
|
-
argument-hint: <observation_id>
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## Timeline
|
|
7
|
-
!`claude-mem-lite timeline --anchor $ARGUMENTS 2>/dev/null || echo "Not found"`
|