claude-recall 0.20.13 → 0.20.14
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.
|
@@ -8,14 +8,14 @@ source: claude-recall
|
|
|
8
8
|
|
|
9
9
|
# Preferences
|
|
10
10
|
|
|
11
|
-
Auto-generated from 5 memories. Last updated: 2026-04-
|
|
11
|
+
Auto-generated from 5 memories. Last updated: 2026-04-10.
|
|
12
12
|
|
|
13
13
|
## Rules
|
|
14
14
|
|
|
15
|
-
- Session test preference
|
|
16
|
-
- Test preference
|
|
17
|
-
- Test preference
|
|
18
|
-
- Test preference
|
|
15
|
+
- Session test preference 1775821892450
|
|
16
|
+
- Test preference 1775821892400-2
|
|
17
|
+
- Test preference 1775821892400-1
|
|
18
|
+
- Test preference 1775821892400-0
|
|
19
19
|
- Test memory content
|
|
20
20
|
|
|
21
21
|
---
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"topicId": "preferences",
|
|
3
|
-
"sourceHash": "
|
|
3
|
+
"sourceHash": "9b2ae714e4bdd82091b4fcc5347c2b1a8a2a10d3be57ca57877e943e380a5457",
|
|
4
4
|
"memoryCount": 5,
|
|
5
|
-
"generatedAt": "2026-04-
|
|
5
|
+
"generatedAt": "2026-04-10T11:51:32.469Z",
|
|
6
6
|
"memoryKeys": [
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
7
|
+
"memory_1775821892451_dzjdxx6ef",
|
|
8
|
+
"memory_1775821892427_462vxb2gq",
|
|
9
|
+
"memory_1775821892416_8taocju9a",
|
|
10
|
+
"memory_1775821892402_3x8ksma5p",
|
|
11
|
+
"memory_1775821892372_4khhv0wc2"
|
|
12
12
|
]
|
|
13
13
|
}
|
|
@@ -49,6 +49,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
49
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
50
|
exports.handleBashFailureWatcher = void 0;
|
|
51
51
|
exports.handleToolOutcomeWatcher = handleToolOutcomeWatcher;
|
|
52
|
+
exports.shouldCaptureFailure = shouldCaptureFailure;
|
|
52
53
|
exports.handleToolFailure = handleToolFailure;
|
|
53
54
|
const fs = __importStar(require("fs"));
|
|
54
55
|
const path = __importStar(require("path"));
|
|
@@ -195,6 +196,37 @@ async function handleToolOutcomeWatcher(input) {
|
|
|
195
196
|
}
|
|
196
197
|
// Backward compatibility alias
|
|
197
198
|
exports.handleBashFailureWatcher = handleToolOutcomeWatcher;
|
|
199
|
+
/**
|
|
200
|
+
* Quality filter: should this bash failure be captured as a memory?
|
|
201
|
+
* Returns false for exploratory probes, command-not-found, and other low-signal failures.
|
|
202
|
+
*/
|
|
203
|
+
function shouldCaptureFailure(command, exitCode, output) {
|
|
204
|
+
// Very short commands (ls, cd, etc.) — not worth capturing
|
|
205
|
+
if (command.trim().length < 5)
|
|
206
|
+
return false;
|
|
207
|
+
// Exploratory probes: commands ending in 2>/dev/null are intentionally suppressing errors
|
|
208
|
+
if (/2>\s*\/dev\/null\s*$/.test(command))
|
|
209
|
+
return false;
|
|
210
|
+
// Command-existence checks: which, type, command -v
|
|
211
|
+
const trimmed = command.trim();
|
|
212
|
+
if (/^(which|type)\s+/i.test(trimmed))
|
|
213
|
+
return false;
|
|
214
|
+
if (/^command\s+-v\s+/i.test(trimmed))
|
|
215
|
+
return false;
|
|
216
|
+
// Command not found (exit code 127 or output contains "command not found")
|
|
217
|
+
if (exitCode === '127')
|
|
218
|
+
return false;
|
|
219
|
+
if (/command not found/i.test(output))
|
|
220
|
+
return false;
|
|
221
|
+
// No useful output — nothing to learn from
|
|
222
|
+
const cleanOutput = output.replace(/\(no output\)/gi, '').trim();
|
|
223
|
+
if (cleanOutput.length < 10 && !/^(npm|npx|node|python|pip|cargo|go|make|docker|git)\s/.test(trimmed))
|
|
224
|
+
return false;
|
|
225
|
+
// EISDIR / "Is a directory" — file read probes
|
|
226
|
+
if (/is a directory/i.test(output) || /EISDIR/i.test(output))
|
|
227
|
+
return false;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
198
230
|
// --- Bash handler (original bash-failure-watcher logic) ---
|
|
199
231
|
async function handleBashOutcome(input) {
|
|
200
232
|
const command = input.tool_input?.command;
|
|
@@ -215,6 +247,11 @@ async function handleBashOutcome(input) {
|
|
|
215
247
|
}
|
|
216
248
|
}
|
|
217
249
|
async function handleBashFailure(command, exitCode, output, sessionId) {
|
|
250
|
+
// Quality filter — skip low-signal failures
|
|
251
|
+
if (!shouldCaptureFailure(command, exitCode, output)) {
|
|
252
|
+
(0, shared_1.hookLog)(HOOK_NAME, `Skipped low-quality failure: ${truncate(command, 60)} (exit ${exitCode})`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
218
255
|
// Dedup check
|
|
219
256
|
const existing = (0, shared_1.searchExisting)(command);
|
|
220
257
|
if ((0, shared_1.isDuplicate)(command, existing, 0.7)) {
|
package/dist/memory/storage.js
CHANGED
|
@@ -230,6 +230,77 @@ class MemoryStorage {
|
|
|
230
230
|
});
|
|
231
231
|
return crypto.createHash('sha256').update(canonical).digest('hex');
|
|
232
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Find a same-type memory whose text content is a near-duplicate (Jaccard >= 0.85).
|
|
235
|
+
* Returns the key of the matching memory, or null if none found.
|
|
236
|
+
*/
|
|
237
|
+
findFuzzyDuplicate(memory) {
|
|
238
|
+
const newText = this.extractText(memory.value).toLowerCase();
|
|
239
|
+
if (newText.length < 40)
|
|
240
|
+
return null; // Too short to fuzzy-match reliably
|
|
241
|
+
// Scope fuzzy dedup to same type AND same project (or both null/universal)
|
|
242
|
+
const projectFilter = memory.project_id
|
|
243
|
+
? 'AND project_id = ?'
|
|
244
|
+
: 'AND (project_id IS NULL OR project_id = \'\')';
|
|
245
|
+
const params = [memory.type, memory.key];
|
|
246
|
+
if (memory.project_id)
|
|
247
|
+
params.push(memory.project_id);
|
|
248
|
+
const candidates = this.db.prepare(`SELECT key, value FROM memories WHERE type = ? AND key != ? ${projectFilter}`).all(...params);
|
|
249
|
+
for (const candidate of candidates) {
|
|
250
|
+
let candidateText;
|
|
251
|
+
try {
|
|
252
|
+
const parsed = JSON.parse(candidate.value);
|
|
253
|
+
candidateText = this.extractText(parsed).toLowerCase();
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
candidateText = candidate.value.toLowerCase();
|
|
257
|
+
}
|
|
258
|
+
if (this.jaccardSimilarity(newText, candidateText) >= 0.65) {
|
|
259
|
+
return candidate.key;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
extractText(value) {
|
|
265
|
+
if (typeof value === 'string')
|
|
266
|
+
return value;
|
|
267
|
+
if (value && typeof value === 'object') {
|
|
268
|
+
// Collect all leaf string values, ignoring JSON keys
|
|
269
|
+
const leaves = [];
|
|
270
|
+
const collect = (obj) => {
|
|
271
|
+
if (typeof obj === 'string') {
|
|
272
|
+
leaves.push(obj);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (obj && typeof obj === 'object') {
|
|
276
|
+
for (const v of Object.values(obj))
|
|
277
|
+
collect(v);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
collect(value);
|
|
281
|
+
return leaves.join(' ');
|
|
282
|
+
}
|
|
283
|
+
return String(value);
|
|
284
|
+
}
|
|
285
|
+
jaccardSimilarity(a, b) {
|
|
286
|
+
const tokenize = (s) => {
|
|
287
|
+
const words = s.replace(/[^a-z0-9\s]/g, ' ').split(/\s+/).filter(Boolean);
|
|
288
|
+
return new Set(words.filter(w => !MemoryStorage.STOP_WORDS.has(w)));
|
|
289
|
+
};
|
|
290
|
+
const wordsA = tokenize(a);
|
|
291
|
+
const wordsB = tokenize(b);
|
|
292
|
+
if (wordsA.size === 0 && wordsB.size === 0)
|
|
293
|
+
return 1;
|
|
294
|
+
if (wordsA.size === 0 || wordsB.size === 0)
|
|
295
|
+
return 0;
|
|
296
|
+
let intersection = 0;
|
|
297
|
+
for (const w of wordsA) {
|
|
298
|
+
if (wordsB.has(w))
|
|
299
|
+
intersection++;
|
|
300
|
+
}
|
|
301
|
+
const union = wordsA.size + wordsB.size - intersection;
|
|
302
|
+
return union === 0 ? 0 : intersection / union;
|
|
303
|
+
}
|
|
233
304
|
save(memory) {
|
|
234
305
|
const contentHash = this.computeContentHash(memory.value, memory.type);
|
|
235
306
|
// Write-time dedup: check if identical content already exists under a different key
|
|
@@ -240,6 +311,13 @@ class MemoryStorage {
|
|
|
240
311
|
this.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
241
312
|
return;
|
|
242
313
|
}
|
|
314
|
+
// Fuzzy dedup: check if a same-type memory with very similar content exists
|
|
315
|
+
const fuzzyMatch = this.findFuzzyDuplicate(memory);
|
|
316
|
+
if (fuzzyMatch) {
|
|
317
|
+
this.db.prepare('UPDATE memories SET timestamp = ?, access_count = access_count + 1 WHERE key = ?').run(Date.now(), fuzzyMatch);
|
|
318
|
+
this.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
243
321
|
const stmt = this.db.prepare(`
|
|
244
322
|
INSERT OR REPLACE INTO memories
|
|
245
323
|
(key, value, type, project_id, file_path, timestamp, relevance_score, access_count,
|
|
@@ -541,3 +619,9 @@ class MemoryStorage {
|
|
|
541
619
|
}
|
|
542
620
|
}
|
|
543
621
|
exports.MemoryStorage = MemoryStorage;
|
|
622
|
+
MemoryStorage.STOP_WORDS = new Set([
|
|
623
|
+
'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
624
|
+
'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as',
|
|
625
|
+
'and', 'or', 'but', 'not', 'no', 'do', 'does', 'did', 'this', 'that',
|
|
626
|
+
'it', 'its', 'via', 'can', 'should', 'will', 'would', 'may', 'might',
|
|
627
|
+
]);
|
package/package.json
CHANGED