claude-mem-lite 2.5.1 → 2.5.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/commands/mem.md CHANGED
@@ -1,5 +1,4 @@
1
1
  ---
2
- name: mem
3
2
  description: Search and manage project memory (observations, sessions, prompts)
4
3
  ---
5
4
 
@@ -1,5 +1,4 @@
1
1
  ---
2
- name: memory
3
2
  description: Save content to memory — with explicit content, instructions, or auto-summarize current session
4
3
  ---
5
4
 
package/commands/tools.md CHANGED
@@ -1,5 +1,4 @@
1
1
  ---
2
- name: tools
3
2
  description: Import skills and agents from GitHub repositories into the tool resource registry
4
3
  ---
5
4
 
@@ -1,5 +1,4 @@
1
1
  ---
2
- name: update
3
2
  description: Auto-maintain memory and resource registry — deduplicate, merge, decay, cleanup, reindex
4
3
  ---
5
4
 
package/dispatch.mjs CHANGED
@@ -279,16 +279,11 @@ const _WRITE_TEST_CJK = /(?:写测试|加测试|补测试|补单测|缺测试|
279
279
  * @param {string} prompt User's natural language prompt
280
280
  * @returns {string} Comma-separated intent tags, primary intent listed first (e.g. "test,fix")
281
281
  */
282
- function extractIntent(prompt) {
283
- if (!prompt) return { intent: '', suppressed: [] };
284
- // English patterns use trailing-optional boundaries for verb conjugations:
285
- // \b prefix ensures word start, but many suffixed forms (debugging, refactoring, deployed)
286
- // fail with trailing \b. Use \b...\w* for words that commonly have suffixes.
287
- // Pattern ordering determines PRIMARY intent (first match).
288
- // Priority: action verbs → domain-specific → quality/style → generic/overloaded.
289
- // This ensures "review code before push" → review (not commit),
290
- // "design database schema" → db (not design), "I have a spec" → plan (not test).
291
- const intentPatterns = [
282
+ // Intent patterns — pre-compiled at module scope to avoid re-creating RegExp on every call.
283
+ // Each entry: [pattern, globalPattern, tag]. Pattern ordering determines PRIMARY intent (first match).
284
+ // Priority: action verbs domain-specific quality/style generic/overloaded.
285
+ const _INTENT_PATTERNS = (() => {
286
+ const raw = [
292
287
  // ── Action verbs (what the user wants to DO) ──
293
288
  [/\b(tests?|testing|tested|tdd|coverage|jest|vitest|pytest|mocha|cypress)\b/i, 'test'],
294
289
  [/\b(debug\w*|fix\w*|bugs?|errors?|troubleshoot\w*|broken|crash\w*|issue|problem|fail\w*|not working|doesn'?t work)\b/i, 'fix'],
@@ -330,20 +325,26 @@ function extractIntent(prompt) {
330
325
  [/(格式化|代码风格|代码规范|类型检查)/, 'lint'],
331
326
  [/(界面|前端|样式|页面|组件|布局)/, 'design'],
332
327
  ];
328
+ // Pre-compile global variants for matchAll — avoids creating new RegExp on every extractIntent call
329
+ return raw.map(([p, tag]) => [p, new RegExp(p.source, p.flags.includes('g') ? p.flags : p.flags + 'g'), tag]);
330
+ })();
331
+
332
+ const _CLAUSE_BOUNDARY = /[,,。;;、.!?!?]/;
333
+
334
+ function extractIntent(prompt) {
335
+ if (!prompt) return { intent: '', suppressed: [] };
333
336
 
334
337
  // Build per-tag negation/affirmation tracking.
335
338
  // A tag is only excluded if ALL its matching instances are negated.
336
339
  // This handles mixed-language inputs like "不要测试了,但 write the tests for auth"
337
340
  // where the Chinese variant is negated but the English variant is not.
338
- const CLAUSE_BOUNDARY = /[,,。;;、.!?!?]/;
339
341
  const tagHasAffirmative = new Map(); // tag → true if any non-negated match exists
340
342
  const tagMatched = new Set(); // tags that matched at least once
341
343
 
342
- for (const [pattern, tag] of intentPatterns) {
343
- // Use global flag + matchAll to find ALL matches (not just the first).
344
+ for (const [, globalPattern, tag] of _INTENT_PATTERNS) {
345
+ // matchAll finds ALL matches (not just the first).
344
346
  // This handles "don't test auth, but test UI" where the first match is negated
345
347
  // but the second is affirmative — the tag should still be included.
346
- const globalPattern = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g');
347
348
  const matches = prompt.matchAll(globalPattern);
348
349
  for (const match of matches) {
349
350
  tagMatched.add(tag);
@@ -352,8 +353,8 @@ function extractIntent(prompt) {
352
353
  const enPrefix = prompt.slice(Math.max(0, matchStart - 20), matchStart);
353
354
  const cjkPrefix = prompt.slice(Math.max(0, matchStart - 8), matchStart);
354
355
  // Clause boundary check: if a comma/period separates negation from keyword, skip
355
- const hasEnNeg = NEGATION_EN.test(enPrefix) && !CLAUSE_BOUNDARY.test(enPrefix);
356
- const hasCjkNeg = NEGATION_CJK.test(cjkPrefix) && !CLAUSE_BOUNDARY.test(cjkPrefix);
356
+ const hasEnNeg = NEGATION_EN.test(enPrefix) && !_CLAUSE_BOUNDARY.test(enPrefix);
357
+ const hasCjkNeg = NEGATION_CJK.test(cjkPrefix) && !_CLAUSE_BOUNDARY.test(cjkPrefix);
357
358
  if (!hasEnNeg && !hasCjkNeg) {
358
359
  tagHasAffirmative.set(tag, true);
359
360
  }
@@ -699,19 +700,19 @@ function isConsecutivelyRejected(db, resourceId) {
699
700
  const recent = db.prepare(`
700
701
  SELECT adopted FROM invocations
701
702
  WHERE resource_id = ? AND recommended = 1 AND outcome IS NOT NULL
702
- AND created_at > datetime('now', '-${CONSECUTIVE_REJECT_WINDOW_DAYS} days')
703
+ AND created_at > datetime('now', ?)
703
704
  ORDER BY created_at DESC
704
705
  LIMIT ?
705
- `).all(resourceId, CONSECUTIVE_REJECT_THRESHOLD);
706
+ `).all(resourceId, `-${CONSECUTIVE_REJECT_WINDOW_DAYS} days`, CONSECUTIVE_REJECT_THRESHOLD);
706
707
 
707
708
  if (recent.length < CONSECUTIVE_REJECT_THRESHOLD) return false;
708
709
  return recent.every(r => r.adopted === 0);
709
710
  } catch { return false; }
710
711
  }
711
712
 
712
- export function isRecentlyRecommended(db, resourceId, sessionId) {
713
- // Check 1: Session cap (loop-invariant — callers should prefer isSessionCapped for filter loops)
714
- if (sessionId) {
713
+ export function isRecentlyRecommended(db, resourceId, sessionId, { skipCapCheck = false } = {}) {
714
+ // Check 1: Session cap (loop-invariant — callers should hoist isSessionCapped and pass skipCapCheck: true)
715
+ if (sessionId && !skipCapCheck) {
715
716
  if (isSessionCapped(db, sessionId)) return true;
716
717
 
717
718
  // Check 2: Already recommended in this session (session dedup)
@@ -964,7 +965,7 @@ export async function dispatchOnSessionStart(db, userPrompt, sessionId, { hasHan
964
965
  // Filter by DB-persisted cooldown + session dedup (hoisted cap check avoids N queries)
965
966
  if (sessionId && isSessionCapped(db, sessionId)) return null;
966
967
  const viable = sessionId
967
- ? results.filter(r => !isRecentlyRecommended(db, r.id, sessionId))
968
+ ? results.filter(r => !isRecentlyRecommended(db, r.id, sessionId, { skipCapCheck: true }))
968
969
  : results;
969
970
  if (viable.length === 0) return null;
970
971
 
@@ -1069,7 +1070,7 @@ export async function dispatchOnUserPrompt(db, userPrompt, sessionId, { sessionE
1069
1070
  // Filter by cooldown + session dedup (hoisted cap check avoids N queries)
1070
1071
  if (sessionId && isSessionCapped(db, sessionId)) return null;
1071
1072
  const viable = sessionId
1072
- ? results.filter(r => !isRecentlyRecommended(db, r.id, sessionId))
1073
+ ? results.filter(r => !isRecentlyRecommended(db, r.id, sessionId, { skipCapCheck: true }))
1073
1074
  : results;
1074
1075
  if (viable.length === 0) return null;
1075
1076
 
@@ -1143,7 +1144,7 @@ export async function dispatchOnPreToolUse(db, event, sessionCtx = {}) {
1143
1144
  const sid = sessionCtx.sessionId || null;
1144
1145
  if (sid && isSessionCapped(db, sid)) return null;
1145
1146
  const viable = sid
1146
- ? results.filter(r => !isRecentlyRecommended(db, r.id, sid))
1147
+ ? results.filter(r => !isRecentlyRecommended(db, r.id, sid, { skipCapCheck: true }))
1147
1148
  : results;
1148
1149
  if (viable.length === 0) return null;
1149
1150
  const best = viable[0];
package/haiku-client.mjs CHANGED
@@ -150,6 +150,7 @@ function callHaikuCLI(prompt, { timeout }) {
150
150
  encoding: 'utf8',
151
151
  env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1' },
152
152
  stdio: ['pipe', 'pipe', 'pipe'],
153
+ cwd: '/tmp', // Prevent ghost sessions in user's /resume list
153
154
  });
154
155
  const text = result.trim();
155
156
  return text ? { text } : null;
package/hook-context.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  // Handles adaptive time windows, token-budgeted selection, and CLAUDE.md persistence
3
3
 
4
4
  import { join } from 'path';
5
- import { readFileSync, writeFileSync, renameSync } from 'fs';
5
+ import { readFileSync, writeFileSync, renameSync, unlinkSync } from 'fs';
6
6
  import { estimateTokens, truncate, debugLog, debugCatch } from './utils.mjs';
7
7
 
8
8
  /**
@@ -165,11 +165,12 @@ export function updateClaudeMd(contextBlock) {
165
165
  content = hintComment + '\n' + newSection + '\n';
166
166
  }
167
167
 
168
+ const tmp = claudeMdPath + '.mem-tmp';
168
169
  try {
169
- const tmp = claudeMdPath + '.mem-tmp';
170
170
  writeFileSync(tmp, content);
171
171
  renameSync(tmp, claudeMdPath);
172
172
  } catch (e) {
173
+ try { unlinkSync(tmp); } catch {}
173
174
  debugLog('ERROR', 'updateClaudeMd', `CLAUDE.md write failed: ${e.message}`);
174
175
  }
175
176
  }
package/hook-shared.mjs CHANGED
@@ -94,6 +94,7 @@ export function callLLM(prompt, timeoutMs = 15000) {
94
94
  encoding: 'utf8',
95
95
  env: { ...process.env, CLAUDE_MEM_HOOK_RUNNING: '1' },
96
96
  stdio: ['pipe', 'pipe', 'pipe'],
97
+ cwd: '/tmp', // Prevent ghost sessions in user's /resume list
97
98
  });
98
99
  return result.trim();
99
100
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {