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-09.
11
+ Auto-generated from 5 memories. Last updated: 2026-04-10.
12
12
 
13
13
  ## Rules
14
14
 
15
- - Session test preference 1775768726677
16
- - Test preference 1775768726629-2
17
- - Test preference 1775768726629-1
18
- - Test preference 1775768726629-0
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": "6deab3a85a84a13804c09fd95225d54f4d11e646d8109ec83cd968d1b7af771e",
3
+ "sourceHash": "9b2ae714e4bdd82091b4fcc5347c2b1a8a2a10d3be57ca57877e943e380a5457",
4
4
  "memoryCount": 5,
5
- "generatedAt": "2026-04-09T21:05:26.692Z",
5
+ "generatedAt": "2026-04-10T11:51:32.469Z",
6
6
  "memoryKeys": [
7
- "memory_1775768726678_aa8631cqc",
8
- "memory_1775768726653_3tkihcis0",
9
- "memory_1775768726640_ii27bz2av",
10
- "memory_1775768726630_p95bn61ba",
11
- "memory_1775768726593_fb0syx1bo"
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)) {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-recall",
3
- "version": "0.20.13",
3
+ "version": "0.20.14",
4
4
  "description": "Persistent memory for Claude Code and Pi with native Skills integration, automatic capture, failure learning, and project scoping",
5
5
  "main": "dist/index.js",
6
6
  "bin": {