claude-mem-lite 2.0.4 → 2.0.12
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/.mcp.json +2 -8
- package/dispatch-feedback.mjs +3 -0
- package/dispatch-inject.mjs +18 -2
- package/dispatch.mjs +85 -24
- package/hook-llm.mjs +22 -2
- package/hook.mjs +29 -0
- package/install.mjs +6 -4
- package/package.json +1 -1
- package/registry-retriever.mjs +1 -0
- package/server.mjs +59 -7
- package/utils.mjs +29 -0
package/.mcp.json
CHANGED
package/dispatch-feedback.mjs
CHANGED
|
@@ -162,6 +162,9 @@ export async function collectFeedback(db, sessionId, sessionEvents = []) {
|
|
|
162
162
|
const outcome = detectOutcome(sessionEvents);
|
|
163
163
|
|
|
164
164
|
for (const inv of invocations) {
|
|
165
|
+
// Skip if already collected (prevents double-collection from stop + session-start)
|
|
166
|
+
if (inv.outcome) continue;
|
|
167
|
+
|
|
165
168
|
const adopted = detectAdoption(inv, sessionEvents);
|
|
166
169
|
const score = adopted ? (outcome === 'success' ? 1.0 : outcome === 'partial' ? 0.5 : 0.2) : 0;
|
|
167
170
|
|
package/dispatch-inject.mjs
CHANGED
|
@@ -33,9 +33,21 @@ function isNativeSkill(name) {
|
|
|
33
33
|
|
|
34
34
|
// ─── Injection Templates ─────────────────────────────────────────────────────
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Invocable skill template -- tells Claude to invoke via Skill tool.
|
|
38
|
+
* Used when the resource has an invocation_name (registered as a Claude Code skill/plugin).
|
|
39
|
+
* @param {object} resource Resource object from DB
|
|
40
|
+
* @returns {string} Injection text instructing Skill tool invocation
|
|
41
|
+
*/
|
|
42
|
+
function injectSkillInvocable(resource) {
|
|
43
|
+
return `[Auto-suggestion] A relevant skill is available for this task. ` +
|
|
44
|
+
`Invoke it now: use the Skill tool with skill="${resource.invocation_name}". ` +
|
|
45
|
+
`Capability: ${truncate(resource.capability_summary, 100)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
/**
|
|
37
49
|
* Native skill template -- tells Claude to use the skill command.
|
|
38
|
-
* Used when skill exists in ~/.claude/skills
|
|
50
|
+
* Used when skill exists in ~/.claude/skills/ but has no invocation_name.
|
|
39
51
|
* @param {object} resource Resource object from DB
|
|
40
52
|
* @returns {string} Injection text referencing the native skill command
|
|
41
53
|
*/
|
|
@@ -129,7 +141,11 @@ export function renderInjection(resource) {
|
|
|
129
141
|
let injection;
|
|
130
142
|
|
|
131
143
|
if (resource.type === 'skill') {
|
|
132
|
-
if
|
|
144
|
+
// Priority: if invocation_name is set, the skill is a registered Claude Code skill/plugin
|
|
145
|
+
// → instruct Claude to invoke via Skill tool (enables adoption tracking)
|
|
146
|
+
if (resource.invocation_name) {
|
|
147
|
+
injection = injectSkillInvocable(resource);
|
|
148
|
+
} else if (isNativeSkill(resource.name)) {
|
|
133
149
|
injection = injectSkillNative(resource);
|
|
134
150
|
} else {
|
|
135
151
|
injection = injectSkillManaged(resource);
|
package/dispatch.mjs
CHANGED
|
@@ -25,7 +25,8 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
25
25
|
'AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode',
|
|
26
26
|
]);
|
|
27
27
|
|
|
28
|
-
export const COOLDOWN_MINUTES =
|
|
28
|
+
export const COOLDOWN_MINUTES = 60;
|
|
29
|
+
export const SESSION_RECOMMEND_CAP = 3;
|
|
29
30
|
|
|
30
31
|
// ─── Haiku Circuit Breaker ──────────────────────────────────────────────────
|
|
31
32
|
// Prevents cascading latency when Haiku API is down or slow.
|
|
@@ -116,6 +117,16 @@ export function detectProjectDomains() {
|
|
|
116
117
|
['pubspec.yaml', ['dart', 'flutter']],
|
|
117
118
|
['Podfile', ['ios', 'swift']],
|
|
118
119
|
['CMakeLists.txt', ['cpp']],
|
|
120
|
+
// Web/browser context — enables domain filtering for browser-specific resources
|
|
121
|
+
['next.config.js', ['web', 'browser', 'react']],
|
|
122
|
+
['next.config.mjs', ['web', 'browser', 'react']],
|
|
123
|
+
['next.config.ts', ['web', 'browser', 'react']],
|
|
124
|
+
['nuxt.config.ts', ['web', 'browser', 'vue']],
|
|
125
|
+
['angular.json', ['web', 'browser', 'angular']],
|
|
126
|
+
['.browserslistrc', ['web', 'browser']],
|
|
127
|
+
['vite.config.ts', ['web', 'browser', 'frontend']],
|
|
128
|
+
['vite.config.js', ['web', 'browser', 'frontend']],
|
|
129
|
+
['webpack.config.js', ['web', 'browser', 'frontend']],
|
|
119
130
|
];
|
|
120
131
|
for (const [file, tags] of checks) {
|
|
121
132
|
if (existsSync(join(dir, file))) tags.forEach(t => techs.add(t));
|
|
@@ -187,6 +198,7 @@ export function extractContextSignals(event, sessionCtx = {}) {
|
|
|
187
198
|
const signals = {
|
|
188
199
|
intent: '', // comma-separated intent tags, primary first
|
|
189
200
|
primaryIntent: '', // first/strongest intent (for column-targeted queries)
|
|
201
|
+
suppressedIntents: [], // intents detected but actively suppressed (e.g. test-run)
|
|
190
202
|
techStack: '',
|
|
191
203
|
action: '',
|
|
192
204
|
errorDomain: '',
|
|
@@ -194,7 +206,9 @@ export function extractContextSignals(event, sessionCtx = {}) {
|
|
|
194
206
|
|
|
195
207
|
// Extract weighted intent from user prompt (primary intent is first element)
|
|
196
208
|
if (sessionCtx.userPrompt) {
|
|
197
|
-
|
|
209
|
+
const { intent, suppressed } = extractIntent(sessionCtx.userPrompt);
|
|
210
|
+
signals.intent = intent;
|
|
211
|
+
signals.suppressedIntents = suppressed;
|
|
198
212
|
signals.primaryIntent = signals.intent.split(',')[0] || '';
|
|
199
213
|
}
|
|
200
214
|
|
|
@@ -234,6 +248,12 @@ export function extractContextSignals(event, sessionCtx = {}) {
|
|
|
234
248
|
const NEGATION_EN = /\b(?:don'?t|do\s+not|no\s+need\s+to|skip|without|avoid|not|never|stop|cancel|ignore|hold\s+off)\s+/i;
|
|
235
249
|
const NEGATION_CJK = /(?:不要|别|不用|先别|暂时不|不需要|跳过|停止|取消|算了|不做|不搞)/;
|
|
236
250
|
|
|
251
|
+
// Test-run vs test-write disambiguation (module-scoped for performance)
|
|
252
|
+
const _RUN_TEST = /\b(run\w*\s+(?:the\s+)?tests?|npm\s+test|npx\s+(?:vitest|jest|mocha|pytest)|yarn\s+test|pnpm\s+test|make\s+test|cargo\s+test|go\s+test|check\s+(?:if\s+)?tests?\s+pass|execute\s+(?:the\s+)?tests?)\b/i;
|
|
253
|
+
const _RUN_TEST_CJK = /(?:运行测试|跑测试|跑一下测试|跑单测|执行测试|测试跑|看测试)/;
|
|
254
|
+
const _WRITE_TEST = /\b(write\s+tests?|add\s+tests?|create\s+tests?|need\s+tests?|missing\s+tests?|tdd|test.?driven|red.?green|increase\s+coverage|improve\s+coverage)\b/i;
|
|
255
|
+
const _WRITE_TEST_CJK = /(?:写测试|加测试|补测试|补单测|缺测试|测试覆盖)/;
|
|
256
|
+
|
|
237
257
|
/**
|
|
238
258
|
* Extract weighted intent keywords from user prompt.
|
|
239
259
|
* Returns primary (first match, strongest signal) and secondary intents.
|
|
@@ -247,41 +267,48 @@ function extractIntent(prompt) {
|
|
|
247
267
|
// English patterns — use trailing-optional boundaries for verb conjugations:
|
|
248
268
|
// \b prefix ensures word start, but many suffixed forms (debugging, refactoring, deployed)
|
|
249
269
|
// fail with trailing \b. Use \b...\w* for words that commonly have suffixes.
|
|
270
|
+
// Pattern ordering determines PRIMARY intent (first match).
|
|
271
|
+
// Priority: action verbs → domain-specific → quality/style → generic/overloaded.
|
|
272
|
+
// This ensures "review code before push" → review (not commit),
|
|
273
|
+
// "design database schema" → db (not design), "I have a spec" → plan (not test).
|
|
250
274
|
const intentPatterns = [
|
|
251
|
-
|
|
275
|
+
// ── Action verbs (what the user wants to DO) ──
|
|
276
|
+
[/\b(tests?|testing|tested|tdd|coverage|jest|vitest|pytest|mocha|cypress)\b/i, 'test'],
|
|
252
277
|
[/\b(debug\w*|fix\w*|bugs?|errors?|troubleshoot\w*|broken|crash\w*|issue|problem|fail\w*|not working|doesn'?t work)\b/i, 'fix'],
|
|
278
|
+
[/\b(reviews?|reviewing|reviewed|reviewer|code review|audit\w*|inspect\w*|look over|check over)\b/i, 'review'],
|
|
253
279
|
[/\b(commits?|committing|committed|push\w*|pr|pull request|merg\w*|rebas\w*|cherry.?pick|stash|tag)\b/i, 'commit'],
|
|
254
280
|
[/\b(deploy\w*|release\w*|publish\w*|ship\w*|rollout|staging|production)\b/i, 'deploy'],
|
|
255
|
-
[/\b(
|
|
281
|
+
[/\b(plan\w*|architect\w*|rfc|proposal|roadmap|blueprint|spec)\b/i, 'plan'],
|
|
256
282
|
[/\b(refactor\w*|clean\w*|simplif\w*|tidy|organiz\w*|restructur\w*|rewrit\w*|messy|ugly|smell|technical.?debt)\b/i, 'clean'],
|
|
257
|
-
[/\b(perf|performance|optimiz\w*|fast\w*|slow\w*|speed\w*|latency|bottleneck|laggy)\b/i, 'fast'],
|
|
258
|
-
[/\b(secur\w*|vulnerabilit\w*|xss|csrf|injection|encrypt\w*|ssl|tls|cors|oauth|jwt|cve|insecure|unsafe)\b/i, 'secure'],
|
|
259
|
-
[/\b(lint\w*|format\w*|style|prettier|eslint|biome|stylelint)\b/i, 'lint'],
|
|
260
|
-
[/\b(design\w*|ui|ux|frontend|css|tailwind|responsive|layout|theme|component)\b/i, 'design'],
|
|
261
|
-
[/\b(build\w*|compil\w*|bundl\w*|transpil\w*|esbuild|vite|rollup|webpack|parcel|babel|swc)\b/i, 'build'],
|
|
262
283
|
[/\b(docs?|documentation|readme|changelog|wiki|guide|tutorial|jsdoc|typedoc)\b/i, 'doc'],
|
|
263
|
-
|
|
284
|
+
// ── Domain-specific (what area the work is in) ──
|
|
264
285
|
[/\b(db|database|sql|migrat\w*|schema|orm|prisma|redis|mongo\w*|postgres\w*|mysql|sqlite)\b/i, 'db'],
|
|
265
286
|
[/\b(api|endpoints?|routes?|rest|graphql|grpc|websocket|middleware|swagger|openapi)\b/i, 'api'],
|
|
266
|
-
[/\b(
|
|
267
|
-
|
|
268
|
-
|
|
287
|
+
[/\b(secur\w*|vulnerabilit\w*|xss|csrf|injection|encrypt\w*|ssl|tls|cors|oauth|jwt|cve|insecure|unsafe)\b/i, 'secure'],
|
|
288
|
+
[/\b(infra\w*|docker\w*|k8s|kubernetes|terraform|ansible|helm|aws|gcp|azure|cloud|nginx|ci\b|cd\b|pipeline)\b/i, 'infra'],
|
|
289
|
+
[/\b(build\w*|compil\w*|bundl\w*|transpil\w*|esbuild|vite|rollup|webpack|parcel|babel|swc)\b/i, 'build'],
|
|
290
|
+
// ── Quality / style ──
|
|
291
|
+
[/\b(perf|performance|optimiz\w*|fast\w*|slow\w*|speed\w*|latency|bottleneck|laggy)\b/i, 'fast'],
|
|
292
|
+
[/\b(lint\w*|format\w*|style|prettier|eslint|biome|stylelint)\b/i, 'lint'],
|
|
293
|
+
// ── Generic / overloaded (easily confused with domain terms) ──
|
|
294
|
+
[/\b(ui|ux|frontend|css|tailwind|responsive|layout|theme|component)\b/i, 'design'],
|
|
295
|
+
// ── Chinese patterns ──
|
|
269
296
|
[/(测试|写测试|单测|单元测试|用例|覆盖率)/, 'test'],
|
|
270
297
|
[/(修复|修bug|改bug|找bug|有bug|调试|排错|报错|出错|有问题|不工作|跑不起来|不能用|挂了|崩溃)/, 'fix'],
|
|
298
|
+
[/(审查|审核|代码审查|评审|代码审核|看看代码|review)/, 'review'],
|
|
271
299
|
[/(提交|推送|上传)/, 'commit'],
|
|
272
300
|
[/(部署|上线|发布|回滚)/, 'deploy'],
|
|
273
|
-
[/(
|
|
301
|
+
[/(规划|架构|方案|设计方案)/, 'plan'],
|
|
274
302
|
[/(重构|清理|整理|简化|太烂|乱七八糟|看不懂)/, 'clean'],
|
|
275
|
-
[/(优化|性能|卡顿|耗时|太慢|慢死了|好慢|缓存)/, 'fast'],
|
|
276
|
-
[/(安全|漏洞|鉴权|认证|授权|权限|泄露|暴露|不安全)/, 'secure'],
|
|
277
|
-
[/(格式化|代码风格|代码规范|类型检查)/, 'lint'],
|
|
278
|
-
[/(界面|前端|样式|页面|组件|布局)/, 'design'],
|
|
279
|
-
[/(构建|编译|打包|依赖)/, 'build'],
|
|
280
303
|
[/(写文档|文档化|文档|注释)/, 'doc'],
|
|
281
|
-
[/(容器|服务器|运维|集群|监控|配置|日志)/, 'infra'],
|
|
282
304
|
[/(数据库|建表|索引|迁移|查询慢)/, 'db'],
|
|
283
305
|
[/(接口|路由)/, 'api'],
|
|
284
|
-
[/(
|
|
306
|
+
[/(安全|漏洞|鉴权|认证|授权|权限|泄露|暴露|不安全)/, 'secure'],
|
|
307
|
+
[/(容器|服务器|运维|集群|监控|配置|日志)/, 'infra'],
|
|
308
|
+
[/(构建|编译|打包|依赖)/, 'build'],
|
|
309
|
+
[/(优化|性能|卡顿|耗时|太慢|慢死了|好慢|缓存)/, 'fast'],
|
|
310
|
+
[/(格式化|代码风格|代码规范|类型检查)/, 'lint'],
|
|
311
|
+
[/(界面|前端|样式|页面|组件|布局)/, 'design'],
|
|
285
312
|
];
|
|
286
313
|
|
|
287
314
|
// Build per-tag negation/affirmation tracking.
|
|
@@ -319,7 +346,20 @@ function extractIntent(prompt) {
|
|
|
319
346
|
found.push(tag);
|
|
320
347
|
}
|
|
321
348
|
}
|
|
322
|
-
|
|
349
|
+
|
|
350
|
+
// Distinguish test-running from test-writing: "run tests" / "npm test" / "运行测试" should NOT
|
|
351
|
+
// trigger TDD recommendations. Only keep 'test' intent when the prompt implies *writing* tests.
|
|
352
|
+
const suppressed = [];
|
|
353
|
+
if (found.includes('test')) {
|
|
354
|
+
const isRunning = _RUN_TEST.test(prompt) || _RUN_TEST_CJK.test(prompt);
|
|
355
|
+
const isWriting = _WRITE_TEST.test(prompt) || _WRITE_TEST_CJK.test(prompt);
|
|
356
|
+
if (isRunning && !isWriting) {
|
|
357
|
+
found.splice(found.indexOf('test'), 1);
|
|
358
|
+
suppressed.push('test');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { intent: found.join(','), suppressed };
|
|
323
363
|
}
|
|
324
364
|
|
|
325
365
|
/** Exported for testing. */
|
|
@@ -538,13 +578,21 @@ JSON: {"query":"search keywords for finding the right skill or agent","type":"sk
|
|
|
538
578
|
// ─── Cooldown & Dedup (DB-persisted, survives process restarts) ─────────────
|
|
539
579
|
|
|
540
580
|
export function isRecentlyRecommended(db, resourceId, sessionId) {
|
|
541
|
-
// Check 1:
|
|
581
|
+
// Check 1: Per-session recommendation cap (avoid overwhelming user with suggestions)
|
|
582
|
+
if (sessionId) {
|
|
583
|
+
const sessionCount = db.prepare(
|
|
584
|
+
'SELECT COUNT(*) as cnt FROM invocations WHERE session_id = ? AND recommended = 1'
|
|
585
|
+
).get(sessionId);
|
|
586
|
+
if (sessionCount.cnt >= SESSION_RECOMMEND_CAP) return true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Check 2: Already recommended in this session (session dedup)
|
|
542
590
|
const sessionHit = db.prepare(
|
|
543
591
|
'SELECT 1 FROM invocations WHERE resource_id = ? AND session_id = ? LIMIT 1'
|
|
544
592
|
).get(resourceId, sessionId);
|
|
545
593
|
if (sessionHit) return true;
|
|
546
594
|
|
|
547
|
-
// Check
|
|
595
|
+
// Check 3: Recommended within cooldown window (cross-session cooldown)
|
|
548
596
|
const cooldownHit = db.prepare(
|
|
549
597
|
`SELECT 1 FROM invocations WHERE resource_id = ? AND created_at > datetime('now', ?) LIMIT 1`
|
|
550
598
|
).get(resourceId, `-${COOLDOWN_MINUTES} minutes`);
|
|
@@ -577,6 +625,13 @@ export async function dispatchOnSessionStart(db, userPrompt, sessionId) {
|
|
|
577
625
|
const textQuery = buildQueryFromText(userPrompt);
|
|
578
626
|
if (!textQuery) return null;
|
|
579
627
|
results = retrieveResources(db, textQuery, { limit: 3, projectDomains });
|
|
628
|
+
// Filter out resources matching suppressed intents (e.g. TDD for test-running prompts)
|
|
629
|
+
if (signals.suppressedIntents.length > 0) {
|
|
630
|
+
results = results.filter(r => {
|
|
631
|
+
const tags = (r.intent_tags || '').toLowerCase().split(/[\s,]+/);
|
|
632
|
+
return !signals.suppressedIntents.some(s => tags.includes(s));
|
|
633
|
+
});
|
|
634
|
+
}
|
|
580
635
|
}
|
|
581
636
|
|
|
582
637
|
let tier = 2;
|
|
@@ -651,6 +706,12 @@ export async function dispatchOnUserPrompt(db, userPrompt, sessionId) {
|
|
|
651
706
|
const textQuery = buildQueryFromText(userPrompt);
|
|
652
707
|
if (!textQuery) return null;
|
|
653
708
|
results = retrieveResources(db, textQuery, { limit: 3, projectDomains });
|
|
709
|
+
if (signals.suppressedIntents.length > 0) {
|
|
710
|
+
results = results.filter(r => {
|
|
711
|
+
const tags = (r.intent_tags || '').toLowerCase().split(/[\s,]+/);
|
|
712
|
+
return !signals.suppressedIntents.some(s => tags.includes(s));
|
|
713
|
+
});
|
|
714
|
+
}
|
|
654
715
|
}
|
|
655
716
|
|
|
656
717
|
if (results.length === 0) return null;
|
package/hook-llm.mjs
CHANGED
|
@@ -156,6 +156,27 @@ function linkRelatedObservations(db, savedId, obs, episode) {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// ─── Degraded Title Builder ──────────────────────────────────────────────────
|
|
160
|
+
// When LLM is unavailable, build a readable title from episode metadata
|
|
161
|
+
// instead of using raw makeEntryDesc output (which contains JSON stdout).
|
|
162
|
+
|
|
163
|
+
function buildDegradedTitle(episode) {
|
|
164
|
+
const files = (episode.files || []).filter(Boolean);
|
|
165
|
+
const hasError = episode.entries.some(e => e.isError);
|
|
166
|
+
const hasEdit = episode.entries.some(e => ['Edit', 'Write', 'NotebookEdit'].includes(e.tool));
|
|
167
|
+
|
|
168
|
+
if (files.length > 0) {
|
|
169
|
+
const names = files.map(f => basename(f)).slice(0, 3).join(', ');
|
|
170
|
+
const suffix = files.length > 3 ? ` +${files.length - 3} more` : '';
|
|
171
|
+
if (hasError) return `Error while working on ${names}${suffix}`;
|
|
172
|
+
if (hasEdit) return `Modified ${names}${suffix}`;
|
|
173
|
+
return `Worked on ${names}${suffix}`;
|
|
174
|
+
}
|
|
175
|
+
// No files: strip raw JSON output from Bash descriptions
|
|
176
|
+
const desc = episode.entries[0]?.desc || '(no description)';
|
|
177
|
+
return desc.replace(/ → (?:ERROR: )?\{.*$/, hasError ? ' (error)' : '');
|
|
178
|
+
}
|
|
179
|
+
|
|
159
180
|
// ─── Background: LLM Episode Extraction (Tier 2 F) ──────────────────────────
|
|
160
181
|
|
|
161
182
|
export async function handleLLMEpisode() {
|
|
@@ -252,10 +273,9 @@ importance: 1=routine, 2=notable (error fix, arch decision, config change), 3=cr
|
|
|
252
273
|
const hasError = episode.entries.some(e => e.isError);
|
|
253
274
|
const hasEdit = episode.entries.some(e => ['Edit', 'Write', 'NotebookEdit'].includes(e.tool));
|
|
254
275
|
const inferredType = hasError ? 'bugfix' : hasEdit ? 'change' : 'discovery';
|
|
255
|
-
const firstDesc = episode.entries[0]?.desc || '(no description)';
|
|
256
276
|
obs = {
|
|
257
277
|
type: inferredType,
|
|
258
|
-
title: truncate(
|
|
278
|
+
title: truncate(buildDegradedTitle(episode), 120),
|
|
259
279
|
subtitle: fileList,
|
|
260
280
|
narrative: episode.entries.map(e => e.desc).join('; '),
|
|
261
281
|
concepts: [],
|
package/hook.mjs
CHANGED
|
@@ -582,6 +582,35 @@ async function handleSessionStart() {
|
|
|
582
582
|
summaryLines.push('');
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
+
// Key context: top high-importance observations for CLAUDE.md persistence
|
|
586
|
+
const keyObs = db.prepare(`
|
|
587
|
+
SELECT id, type, title FROM observations
|
|
588
|
+
WHERE project = ? AND COALESCE(compressed_into, 0) = 0
|
|
589
|
+
AND COALESCE(importance, 1) >= 2
|
|
590
|
+
ORDER BY created_at_epoch DESC LIMIT 5
|
|
591
|
+
`).all(project);
|
|
592
|
+
if (keyObs.length > 0) {
|
|
593
|
+
summaryLines.push('### Key Context');
|
|
594
|
+
for (const o of keyObs) {
|
|
595
|
+
// Strip raw JSON output from degraded Bash-style titles
|
|
596
|
+
const clean = (o.title || '(untitled)')
|
|
597
|
+
.replace(/ → (?:ERROR: )?\{".*$/, '')
|
|
598
|
+
.replace(/ → (?:ERROR: )?\{[^}]*\.{3}$/, '');
|
|
599
|
+
summaryLines.push(`- [${o.type || 'discovery'}] ${truncate(clean, 80)} (#${o.id})`);
|
|
600
|
+
}
|
|
601
|
+
summaryLines.push('');
|
|
602
|
+
} else if (!latestSummary) {
|
|
603
|
+
// Fallback: no summary AND no key observations — show recent activity
|
|
604
|
+
const recentObs = (observations.length >= 3 ? observations : fallbackObs).slice(0, 3);
|
|
605
|
+
if (recentObs.length > 0) {
|
|
606
|
+
summaryLines.push('### Recent Activity');
|
|
607
|
+
for (const o of recentObs) {
|
|
608
|
+
summaryLines.push(`- ${truncate(o.title || '(untitled)', 80)}`);
|
|
609
|
+
}
|
|
610
|
+
summaryLines.push('');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
585
614
|
// Build observations table (stdout only — not persisted to CLAUDE.md)
|
|
586
615
|
const obsLines = [];
|
|
587
616
|
const obsToShow = observations.length >= 3 ? observations : fallbackObs;
|
package/install.mjs
CHANGED
|
@@ -511,8 +511,8 @@ const RESOURCE_METADATA = {
|
|
|
511
511
|
trigger_patterns: 'when user needs XML sitemap generation optimization or crawl management',
|
|
512
512
|
},
|
|
513
513
|
'skill:seo-technical': {
|
|
514
|
-
intent_tags: 'seo,technical,
|
|
515
|
-
domain_tags: 'seo,technical
|
|
514
|
+
intent_tags: 'seo,technical,core-web-vitals,crawl,indexing',
|
|
515
|
+
domain_tags: 'seo,technical',
|
|
516
516
|
capability_summary: 'Technical SEO audit covering core web vitals site speed crawlability and indexing',
|
|
517
517
|
trigger_patterns: 'when user needs technical SEO improvements core web vitals or site speed optimization',
|
|
518
518
|
},
|
|
@@ -997,8 +997,8 @@ const RESOURCE_METADATA = {
|
|
|
997
997
|
trigger_patterns: 'when user needs SEO-optimized content creation keyword articles or blog writing',
|
|
998
998
|
},
|
|
999
999
|
'agent:seo-technical-optimization': {
|
|
1000
|
-
intent_tags: 'seo,technical,optimization,
|
|
1001
|
-
domain_tags: 'seo,technical
|
|
1000
|
+
intent_tags: 'seo,technical,optimization,crawl,indexing',
|
|
1001
|
+
domain_tags: 'seo,technical',
|
|
1002
1002
|
capability_summary: 'Technical SEO optimization agent for site speed crawl efficiency and indexing',
|
|
1003
1003
|
trigger_patterns: 'when user needs technical SEO optimization site speed or crawl improvements',
|
|
1004
1004
|
},
|
|
@@ -1290,6 +1290,8 @@ async function install() {
|
|
|
1290
1290
|
// Remove existing first (ignore errors)
|
|
1291
1291
|
try { execFileSync('claude', ['mcp', 'remove', '-s', 'user', 'mem'], { stdio: 'pipe' }); } catch {}
|
|
1292
1292
|
execFileSync('claude', ['mcp', 'add', '-s', 'user', '-t', 'stdio', 'mem', '--', 'node', SERVER_PATH], { stdio: 'pipe' });
|
|
1293
|
+
// Remove project-level registration that shadows global (from .mcp.json)
|
|
1294
|
+
try { execFileSync('claude', ['mcp', 'remove', '-s', 'project', 'mem'], { stdio: 'pipe' }); } catch {}
|
|
1293
1295
|
ok('MCP server registered: mem');
|
|
1294
1296
|
} catch (e) {
|
|
1295
1297
|
fail('MCP registration failed: ' + e.message);
|
package/package.json
CHANGED
package/registry-retriever.mjs
CHANGED
package/server.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
-
import { jaccardSimilarity, truncate, typeIcon, sanitizeFtsQuery, inferProject, computeMinHash, scrubSecrets, fmtDate, isoWeekKey, debugLog, debugCatch } from './utils.mjs';
|
|
7
|
+
import { jaccardSimilarity, truncate, typeIcon, sanitizeFtsQuery, relaxFtsQueryToOr, inferProject, computeMinHash, scrubSecrets, fmtDate, isoWeekKey, debugLog, debugCatch } from './utils.mjs';
|
|
8
8
|
import { ensureDb, DB_PATH } from './schema.mjs';
|
|
9
9
|
import { reRankWithContext, markSuperseded, extractPRFTerms, expandQueryByConcepts } from './server-internals.mjs';
|
|
10
10
|
import { memSearchSchema, memTimelineSchema, memGetSchema, memDeleteSchema, memSaveSchema, memStatsSchema, memCompressSchema } from './tool-schemas.mjs';
|
|
@@ -64,13 +64,21 @@ const server = new McpServer(
|
|
|
64
64
|
{ name: 'claude-mem-lite', version: '2.0.0' },
|
|
65
65
|
{
|
|
66
66
|
instructions: [
|
|
67
|
-
'Proactively
|
|
68
|
-
'- Errors occur: search for related past fixes (obs_type="bugfix")',
|
|
69
|
-
'- Before significant file changes: search for file history',
|
|
70
|
-
'- Architecture decisions: check past decisions (obs_type="decision")',
|
|
71
|
-
'- Stuck/blocked: search for similar past work',
|
|
67
|
+
'Proactively search memory to leverage past experience. This is your long-term memory across sessions.',
|
|
72
68
|
'',
|
|
73
|
-
'
|
|
69
|
+
'WHEN TO SEARCH (mem_search):',
|
|
70
|
+
'- Error/bug encountered → mem_search(query="<error keyword>", obs_type="bugfix")',
|
|
71
|
+
'- Before modifying important files → mem_search(query="<filename>")',
|
|
72
|
+
'- Architecture/design decisions → mem_search(query="<topic>", obs_type="decision")',
|
|
73
|
+
'- Stuck or blocked → mem_search(query="<problem description>")',
|
|
74
|
+
'- Starting work on a feature → mem_search(query="<feature name>")',
|
|
75
|
+
'',
|
|
76
|
+
'WHEN TO SAVE (mem_save):',
|
|
77
|
+
'- Non-obvious debugging discovery → mem_save with type="bugfix"',
|
|
78
|
+
'- Key architecture decision → mem_save with type="decision"',
|
|
79
|
+
'- Important pattern or convention found → mem_save with type="discovery"',
|
|
80
|
+
'',
|
|
81
|
+
'WORKFLOW: mem_search → mem_timeline(anchor=ID) → mem_get(ids=[...]) for full context.',
|
|
74
82
|
].join('\n'),
|
|
75
83
|
},
|
|
76
84
|
);
|
|
@@ -129,6 +137,50 @@ function searchObservations(ctx) {
|
|
|
129
137
|
results.push({ source: 'obs', id: r.id, type: r.type, title: r.title, subtitle: r.subtitle, project: r.project, date: r.created_at, score: r.score, files_modified: r.files_modified, importance: r.importance, snippet: r.match_snippet || '' });
|
|
130
138
|
}
|
|
131
139
|
|
|
140
|
+
// OR fallback: when AND query returns 0 results, retry with OR semantics
|
|
141
|
+
if (rows.length === 0) {
|
|
142
|
+
const orQuery = relaxFtsQueryToOr(ftsQuery);
|
|
143
|
+
if (orQuery) {
|
|
144
|
+
try {
|
|
145
|
+
const orRows = db.prepare(`
|
|
146
|
+
SELECT o.id, o.type, o.title, o.subtitle, o.project, o.created_at, o.importance,
|
|
147
|
+
o.files_modified,
|
|
148
|
+
snippet(observations_fts, 2, '»', '«', '…', 10) as match_snippet,
|
|
149
|
+
${OBS_BM25}
|
|
150
|
+
* (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / ${RECENCY_HALF_LIFE_MS}.0))
|
|
151
|
+
* (CASE WHEN ? IS NOT NULL AND o.project = ? THEN 2.0 ELSE 1.0 END)
|
|
152
|
+
* (0.5 + 0.5 * COALESCE(o.importance, 1))
|
|
153
|
+
* (1.0 + 0.1 * LN(1 + COALESCE(o.access_count, 0)))
|
|
154
|
+
* 0.8 as score
|
|
155
|
+
FROM observations_fts
|
|
156
|
+
JOIN observations o ON observations_fts.rowid = o.id
|
|
157
|
+
WHERE observations_fts MATCH ?
|
|
158
|
+
AND COALESCE(o.compressed_into, 0) = 0
|
|
159
|
+
AND (? IS NULL OR o.project = ?)
|
|
160
|
+
AND (? IS NULL OR o.type = ?)
|
|
161
|
+
AND (? IS NULL OR o.created_at_epoch >= ?)
|
|
162
|
+
AND (? IS NULL OR o.created_at_epoch <= ?)
|
|
163
|
+
AND (? IS NULL OR COALESCE(o.importance, 1) >= ?)
|
|
164
|
+
ORDER BY score
|
|
165
|
+
LIMIT ? OFFSET ?
|
|
166
|
+
`).all(
|
|
167
|
+
now,
|
|
168
|
+
projectBoost, projectBoost,
|
|
169
|
+
orQuery,
|
|
170
|
+
args.project ?? null, args.project ?? null,
|
|
171
|
+
args.obs_type ?? null, args.obs_type ?? null,
|
|
172
|
+
epochFrom, epochFrom,
|
|
173
|
+
epochTo, epochTo,
|
|
174
|
+
args.importance ?? null, args.importance ?? null,
|
|
175
|
+
perSourceLimit, perSourceOffset
|
|
176
|
+
);
|
|
177
|
+
for (const r of orRows) {
|
|
178
|
+
results.push({ source: 'obs', id: r.id, type: r.type, title: r.title, subtitle: r.subtitle, project: r.project, date: r.created_at, score: r.score, files_modified: r.files_modified, importance: r.importance, snippet: r.match_snippet || '' });
|
|
179
|
+
}
|
|
180
|
+
} catch (e) { debugCatch(e, 'searchObservations-or-fallback'); }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
132
184
|
// Two-phase query expansion for sparse results
|
|
133
185
|
if (rows.length > 0 && results.length < limit) {
|
|
134
186
|
const existingIds = new Set(results.map(r => r.id));
|
package/utils.mjs
CHANGED
|
@@ -238,6 +238,15 @@ const SYNONYM_PAIRS = [
|
|
|
238
238
|
['api', 'endpoint'],
|
|
239
239
|
['api', 'route'],
|
|
240
240
|
['cache', 'caching'],
|
|
241
|
+
['cache', 'memoize'],
|
|
242
|
+
['optimize', 'optimization'],
|
|
243
|
+
['optimize', 'performance'],
|
|
244
|
+
['speed', 'performance'],
|
|
245
|
+
['fix', 'bugfix'],
|
|
246
|
+
['fix', 'patch'],
|
|
247
|
+
['debug', 'debugging'],
|
|
248
|
+
['debug', 'troubleshoot'],
|
|
249
|
+
['error', 'failure'],
|
|
241
250
|
['migrate', 'migration'],
|
|
242
251
|
];
|
|
243
252
|
// Build bidirectional lookup (case-insensitive)
|
|
@@ -289,6 +298,26 @@ export function sanitizeFtsQuery(query) {
|
|
|
289
298
|
return expanded.join(hasGroup ? ' AND ' : ' ');
|
|
290
299
|
}
|
|
291
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Relax an AND-joined FTS5 query to OR-joined for fallback search.
|
|
303
|
+
* Only useful when the original query has multiple tokens (single-token queries
|
|
304
|
+
* are already as relaxed as possible).
|
|
305
|
+
* @param {string} ftsQuery Original AND-joined FTS5 query from sanitizeFtsQuery
|
|
306
|
+
* @returns {string|null} OR-joined query, or null if relaxation wouldn't help
|
|
307
|
+
*/
|
|
308
|
+
export function relaxFtsQueryToOr(ftsQuery) {
|
|
309
|
+
if (!ftsQuery) return null;
|
|
310
|
+
// Replace AND joins with OR — handles both explicit " AND " and implicit space joins
|
|
311
|
+
const orQuery = ftsQuery.replace(/ AND /g, ' OR ');
|
|
312
|
+
// If no AND was present, tokens are space-joined (implicit AND); convert to OR
|
|
313
|
+
if (orQuery === ftsQuery && !ftsQuery.includes(' OR ')) {
|
|
314
|
+
const parts = ftsQuery.split(/\s+/);
|
|
315
|
+
if (parts.length < 2) return null; // single token — OR won't help
|
|
316
|
+
return parts.join(' OR ');
|
|
317
|
+
}
|
|
318
|
+
return orQuery !== ftsQuery ? orQuery : null;
|
|
319
|
+
}
|
|
320
|
+
|
|
292
321
|
// ─── Importance ──────────────────────────────────────────────────────────────
|
|
293
322
|
|
|
294
323
|
/**
|