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.
- package/.claude-plugin/plugin.json +1 -1
- package/commands/mem.md +0 -1
- package/commands/memory.md +0 -1
- package/commands/tools.md +0 -1
- package/commands/update.md +0 -1
- package/dispatch.mjs +25 -24
- package/haiku-client.mjs +1 -0
- package/hook-context.mjs +3 -2
- package/hook-shared.mjs +1 -0
- package/package.json +1 -1
package/commands/mem.md
CHANGED
package/commands/memory.md
CHANGED
package/commands/tools.md
CHANGED
package/commands/update.md
CHANGED
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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 [
|
|
343
|
-
//
|
|
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) && !
|
|
356
|
-
const hasCjkNeg = NEGATION_CJK.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',
|
|
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
|
|
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) {
|