claude-mem-lite 2.12.0 → 2.12.2

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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.12.0",
13
+ "version": "2.12.2",
14
14
  "source": "./",
15
15
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.12.0",
3
+ "version": "2.12.2",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
@@ -157,7 +157,7 @@ function detectAdoption(invocation, sessionEvents) {
157
157
 
158
158
  // Tier 2: Behavioral adoption — methodology patterns (10 min window)
159
159
  const resourceLower = resource_name.toLowerCase();
160
- const recTime = invocation.created_at ? new Date(invocation.created_at).getTime() : 0;
160
+ const recTime = invocation.created_at ? new Date(invocation.created_at).getTime() : null;
161
161
 
162
162
  // TDD pattern: Bash(test fail) → Edit → Bash(test pass)
163
163
  if (resourceLower.includes('tdd') || resourceLower.includes('test-driven')) {
@@ -365,7 +365,7 @@ function autodemoteZombies(db) {
365
365
  try {
366
366
  const demoted = db.prepare(`
367
367
  UPDATE resources SET recommendation_mode = 'on_request', updated_at = datetime('now')
368
- WHERE COALESCE(recommend_count, 0) > 5
368
+ WHERE COALESCE(recommend_count, 0) > 8
369
369
  AND (COALESCE(adopt_count, 0) + 1.0) / (COALESCE(recommend_count, 0) + 2.0) < 0.1
370
370
  AND COALESCE(recommendation_mode, 'proactive') = 'proactive'
371
371
  AND status = 'active'
@@ -138,14 +138,14 @@ export function inferCurrentStage(primaryIntent, activeSuite, suppressedIntents
138
138
  // ─── Explicit Request Detection ──────────────────────────────────────────────
139
139
 
140
140
  const EXPLICIT_REQUEST_PATTERNS = [
141
- // EN: "use the playwright skill", "try the ppt skill"
142
- /(?:use|try|invoke|run|activate|load)\s+(?:the\s+)?(\S+?)\s+(?:skill|agent|tool|plugin)\b/i,
143
- // CN: "用ppt的技能", "帮我用playwright的skill"
144
- /(?:用|使用|帮我用|试试|启用)\s*(\S+?)\s*(?:的|的技能|的skill|的agent|技能|skill|agent|工具|插件)/,
145
- // "有没有xxx的skill", "is there a xxx agent"
146
- /(?:有没有|有无|是否有|do you have|is there)\s*(?:一个|a|an)?\s*(\S+?)\s*(?:的|skill|agent|技能|工具)/i,
147
- // "推荐一个xxx", "recommend a xxx agent"
148
- /(?:推荐|suggest|recommend)\s*(?:一个|a|an)?\s*(\S+?)\s*(?:的|skill|agent|技能|工具)/i,
141
+ // EN: "use the playwright skill", "try the code review skill" (multi-word support)
142
+ /(?:use|try|invoke|run|activate|load)\s+(?:the\s+)?(.+?)\s+(?:skill|agent|tool|plugin)\b/i,
143
+ // CN: "用ppt的技能", "帮我用code review的skill" (multi-word support)
144
+ /(?:用|使用|帮我用|试试|启用)\s*(.+?)\s*(?:的技能|的skill|的agent|的工具|的插件|技能|skill|agent|工具|插件)/,
145
+ // "有没有code review的skill", "is there a code review agent"
146
+ /(?:有没有|有无|是否有|do you have|is there)\s*(?:一个|a|an)?\s*(.+?)\s*(?:的|skill|agent|技能|工具)/i,
147
+ // "推荐一个code review的", "recommend a testing agent"
148
+ /(?:推荐|suggest|recommend)\s*(?:一个|a|an)?\s*(.+?)\s*(?:的|skill|agent|技能|工具)/i,
149
149
  ];
150
150
 
151
151
  /**
package/dispatch.mjs CHANGED
@@ -1114,16 +1114,16 @@ export async function dispatchOnUserPrompt(db, userPrompt, sessionId) {
1114
1114
  const textQuery = buildQueryFromText(explicit.searchTerm);
1115
1115
  if (!textQuery) return null;
1116
1116
 
1117
- let results = retrieveResources(db, textQuery, { limit: 3, projectDomains: detectProjectDomains() });
1118
- results = filterAutoLoadedSkills(results);
1117
+ // Explicit requests bypass project domain filtering if the user asks
1118
+ // "use the playwright skill", don't filter by project tech stack
1119
+ let results = retrieveResources(db, textQuery, { limit: 3 });
1119
1120
  results = filterGarbageMetadata(results);
1120
1121
  // Community resources without invocation_name can't be easily invoked
1121
1122
  results = results.filter(r => r.quality_tier !== 'community' || r.invocation_name);
1122
- results = applyAdoptionDecay(results, db);
1123
+ // Explicit requests skip adoption decay and cooldown — user asked for this
1123
1124
  if (results.length === 0) return null;
1124
1125
 
1125
1126
  const best = results[0];
1126
- if (sessionId && isRecentlyRecommended(db, best.id, sessionId)) return null;
1127
1127
 
1128
1128
  recordInvocation(db, { resource_id: best.id, session_id: sessionId, trigger: 'user_prompt', tier: 1, recommended: 1 });
1129
1129
  updateResourceStats(db, best.id, 'recommend_count');
package/hook-llm.mjs CHANGED
@@ -275,6 +275,22 @@ export function buildImmediateObservation(episode) {
275
275
  importance = ruleImportance;
276
276
  }
277
277
 
278
+ // Separate files_modified (from Edit/Write tools) from files_read (everything else)
279
+ const modifiedFiles = new Set();
280
+ const searchedFiles = new Set();
281
+ for (const entry of episode.entries) {
282
+ if (!entry.files) continue;
283
+ if (EDIT_TOOLS.has(entry.tool)) {
284
+ for (const f of entry.files) modifiedFiles.add(f);
285
+ } else {
286
+ for (const f of entry.files) searchedFiles.add(f);
287
+ }
288
+ }
289
+ // Merge bash-tracked reads and search tool files into filesRead
290
+ const allReads = new Set([...(episode.filesRead || []), ...searchedFiles]);
291
+ // Remove files that were both searched AND modified — they're modified
292
+ for (const f of modifiedFiles) allReads.delete(f);
293
+
278
294
  return {
279
295
  type: inferredType,
280
296
  title,
@@ -282,8 +298,8 @@ export function buildImmediateObservation(episode) {
282
298
  narrative: episode.entries.map(e => e.desc).join('; '),
283
299
  concepts: [],
284
300
  facts: [],
285
- files: episode.files,
286
- filesRead: episode.filesRead || [],
301
+ files: [...modifiedFiles],
302
+ filesRead: [...allReads],
287
303
  importance,
288
304
  };
289
305
  }
package/hook-shared.mjs CHANGED
@@ -143,24 +143,6 @@ export function hasInjectionBudget() { return _injectionCount < MAX_INJECTIONS_P
143
143
  // ─── Previous Session Context (for user-prompt dispatch enrichment) ──────────
144
144
  // Session-start caches next_steps; first user-prompt reads+clears for richer dispatch.
145
145
 
146
- export function prevContextFile() {
147
- return join(RUNTIME_DIR, `prev-context-${inferProject()}`);
148
- }
149
-
150
- export function cachePrevContext(nextSteps) {
151
- try { writeFileSync(prevContextFile(), JSON.stringify({ nextSteps, ts: Date.now() })); } catch {}
152
- }
153
-
154
- export function readAndClearPrevContext() {
155
- const file = prevContextFile();
156
- try {
157
- const data = JSON.parse(readFileSync(file, 'utf8'));
158
- try { unlinkSync(file); } catch {}
159
- if (Date.now() - data.ts > 12 * 3600000) return null; // 12h expiry
160
- return data.nextSteps || null;
161
- } catch { return null; }
162
- }
163
-
164
146
  // ─── Tool Event Tracking (for dispatch feedback) ────────────────────────────
165
147
  // PostToolUse appends feedback-relevant tool events (Skill, Task, Edit, Write, Bash errors).
166
148
  // Stop handler reads them and passes to collectFeedback for adoption/outcome detection.
package/hook.mjs CHANGED
@@ -759,7 +759,6 @@ async function handleSessionStart() {
759
759
  // CLAUDE.md: slim (summary + handoff state — observations already in stdout)
760
760
  updateClaudeMd([...summaryLines, ...handoffLines].join('\n'));
761
761
 
762
- // Cache previous session context for user-prompt dispatch enrichment.
763
762
  // Background rescan: detect changed/new managed resources since last scan.
764
763
  // TTL-based (1h) — avoids redundant filesystem scans on every session.
765
764
  // Non-blocking: spawns detached worker, results available before first user prompt.
@@ -842,6 +841,9 @@ async function handleUserPrompt() {
842
841
  const promptText = hookData.prompt || hookData.user_prompt;
843
842
  if (!promptText || typeof promptText !== 'string') return;
844
843
 
844
+ // Skip internal Claude Code protocol messages — not real user input
845
+ if (promptText.startsWith('<task-notification>')) return;
846
+
845
847
  const sessionId = getSessionId();
846
848
  const db = openDb();
847
849
  if (!db) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.12.0",
3
+ "version": "2.12.2",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
@@ -83,7 +83,7 @@ function fallbackExtract(resource) {
83
83
 
84
84
  return {
85
85
  intent_tags: intentTags || resource.type,
86
- domain_tags: domainTags || 'general',
86
+ domain_tags: domainTags || '',
87
87
  action_type: resource.type === 'agent' ? 'analyze' : 'generate',
88
88
  trigger_patterns: `when user needs ${name.replace(/-/g, ' ')} functionality`,
89
89
  capability_summary: truncate(`${resource.type}: ${name.replace(/-/g, ' ')}`, 100),
@@ -10,6 +10,8 @@ export const DISPATCH_SYNONYMS = {
10
10
  'clean': ['refactor', 'lint', 'format', 'organize', 'tidy', 'simplify', 'restructure', 'rewrite', 'smell', 'debt'],
11
11
  'test': ['testing', 'unittest', 'e2e', 'coverage', 'tdd', 'qa', 'spec', 'jest', 'vitest', 'pytest', 'mocha', 'cypress', 'playwright'],
12
12
  'fix': ['debug', 'bugfix', 'troubleshoot', 'diagnose', 'repair', 'error', 'crash', 'broken', 'issue', 'problem'],
13
+ 'debug': ['debugging', 'fix', 'bugfix', 'troubleshoot', 'diagnose', 'error', 'crash', 'bug', 'breakpoint'],
14
+ 'debugging':['debug', 'fix', 'bugfix', 'troubleshoot', 'diagnose', 'error', 'crash', 'bug', 'systematic'],
13
15
  'fast': ['performance', 'optimize', 'profile', 'benchmark', 'speed', 'latency', 'bottleneck', 'slow', 'cache'],
14
16
  'deploy': ['release', 'publish', 'ci', 'cd', 'ship', 'rollout', 'staging', 'production'],
15
17
  'commit': ['git', 'push', 'merge', 'pr', 'branch', 'version', 'rebase', 'stash', 'tag'],
@@ -244,7 +246,7 @@ const TEXT_QUERY_STOP_WORDS = new Set([
244
246
  export function buildQueryFromText(text) {
245
247
  if (!text || typeof text !== 'string') return null;
246
248
 
247
- const cleaned = text.replace(/[{}()[\]^~*:@#$%&]/g, ' ').trim();
249
+ const cleaned = text.replace(/[{}()[\]^~*:@#$%&"\\]/g, ' ').trim();
248
250
 
249
251
  // Extract CJK compound words before whitespace split (Chinese has no spaces)
250
252
  const cjkTokens = extractCJKTokens(cleaned);
@@ -220,7 +220,7 @@ export function scanAllResources(config = {}) {
220
220
  */
221
221
  export function diffResources(db, scanned) {
222
222
  const existing = new Map();
223
- const rows = db.prepare('SELECT id, type, name, file_hash, status FROM resources').all();
223
+ const rows = db.prepare('SELECT id, type, name, file_hash, local_path, status FROM resources').all();
224
224
  for (const r of rows) existing.set(`${r.type}:${r.name}`, r);
225
225
 
226
226
  const toIndex = [];
@@ -242,9 +242,12 @@ export function diffResources(db, scanned) {
242
242
  }
243
243
 
244
244
  // Resources in DB but not on filesystem → disable
245
+ // Only disable resources that have a local_path (filesystem-backed).
246
+ // Resources without local_path were imported via metadata/registry and
247
+ // cannot be validated by filesystem scan.
245
248
  const toDisable = [];
246
249
  for (const [key, row] of existing) {
247
- if (!scannedKeys.has(key) && row.status === 'active') {
250
+ if (!scannedKeys.has(key) && row.status === 'active' && row.local_path) {
248
251
  toDisable.push(row);
249
252
  }
250
253
  }
package/server.mjs CHANGED
@@ -1272,11 +1272,18 @@ server.registerTool(
1272
1272
  }
1273
1273
  let results = searchResources(rdb, args.query, {
1274
1274
  type: args.type || undefined,
1275
- limit: args.category || args.quality ? 20 : 5, // fetch more when filtering
1275
+ limit: args.category || args.quality ? 20 : 10, // fetch more for post-filtering
1276
1276
  });
1277
1277
  // Apply category/quality filters if provided
1278
1278
  if (args.category) results = results.filter(r => r.category === args.category);
1279
1279
  if (args.quality) results = results.filter(r => r.quality_tier === args.quality);
1280
+ // Prioritize directly invocable resources (with invocation_name) over community resources
1281
+ results.sort((a, b) => {
1282
+ const aInvocable = a.invocation_name ? 1 : 0;
1283
+ const bInvocable = b.invocation_name ? 1 : 0;
1284
+ if (aInvocable !== bInvocable) return bInvocable - aInvocable;
1285
+ return 0; // preserve FTS5 ranking within same tier
1286
+ });
1280
1287
  results = results.slice(0, 5);
1281
1288
  if (results.length === 0) {
1282
1289
  return { content: [{ type: 'text', text: `No matching resources for: "${args.query}"` }] };