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 CHANGED
@@ -1,9 +1,3 @@
1
1
  {
2
- "mcpServers": {
3
- "mem": {
4
- "type": "stdio",
5
- "command": "node",
6
- "args": ["./server.mjs"]
7
- }
8
- }
9
- }
2
+ "mcpServers": {}
3
+ }
@@ -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
 
@@ -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 (isNativeSkill(resource.name)) {
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 = 5;
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
- signals.intent = extractIntent(sessionCtx.userPrompt);
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
- [/\b(tests?|testing|tested|tdd|spec|coverage|jest|vitest|pytest|mocha|cypress)\b/i, 'test'],
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(reviews?|reviewing|reviewed|reviewer|code review|audit\w*|inspect\w*|look over|check over)\b/i, 'review'],
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
- [/\b(infra\w*|docker\w*|k8s|kubernetes|terraform|ansible|helm|aws|gcp|azure|cloud|nginx|ci\b|cd\b|pipeline)\b/i, 'infra'],
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(plan\w*|architect\w*|rfc|proposal|roadmap|blueprint|spec\b)\b/i, 'plan'],
267
- // Chinese patterns — \b doesn't work with CJK characters, so match without boundaries.
268
- // Use 2+ char compounds to avoid false positives from polysemous single chars.
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
- [/(审查|审核|代码审查|评审|代码审核|看看代码|review)/, 'review'],
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
- [/(规划|架构|方案|设计方案)/, 'plan'],
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
- return found.join(',');
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: Already recommended in this session (session dedup)
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 2: Recommended within cooldown window (cross-session cooldown)
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(firstDesc, 120),
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,performance,core-web-vitals,speed,crawl,indexing',
515
- domain_tags: 'seo,technical,performance',
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,speed,crawl,indexing,performance',
1001
- domain_tags: 'seo,technical,optimization',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.0.4",
3
+ "version": "2.0.12",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
@@ -260,6 +260,7 @@ const TECHNOLOGY_TAGS = new Set([
260
260
  'cpp', 'c', 'csharp', 'dotnet', 'aspnet',
261
261
  'elixir', 'erlang', 'lua', 'zig', 'solidity',
262
262
  'html', 'css', 'frontend', 'backend',
263
+ 'browser', 'web', 'playwright',
263
264
  ]);
264
265
 
265
266
  /**
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 use mem_search when:',
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
- 'Workflow: mem_search mem_timeline(anchor=ID) → mem_get(ids=[...]) for full context.',
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
  /**