@ulrichc1/sparn 1.2.1 → 1.4.0

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.
Files changed (42) hide show
  1. package/PRIVACY.md +1 -1
  2. package/README.md +136 -642
  3. package/SECURITY.md +1 -1
  4. package/dist/cli/dashboard.cjs +3977 -0
  5. package/dist/cli/dashboard.cjs.map +1 -0
  6. package/dist/cli/dashboard.d.cts +17 -0
  7. package/dist/cli/dashboard.d.ts +17 -0
  8. package/dist/cli/dashboard.js +3932 -0
  9. package/dist/cli/dashboard.js.map +1 -0
  10. package/dist/cli/index.cjs +3855 -486
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +3812 -459
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/daemon/index.cjs +411 -99
  15. package/dist/daemon/index.cjs.map +1 -1
  16. package/dist/daemon/index.js +423 -103
  17. package/dist/daemon/index.js.map +1 -1
  18. package/dist/hooks/post-tool-result.cjs +129 -225
  19. package/dist/hooks/post-tool-result.cjs.map +1 -1
  20. package/dist/hooks/post-tool-result.js +129 -225
  21. package/dist/hooks/post-tool-result.js.map +1 -1
  22. package/dist/hooks/pre-prompt.cjs +206 -242
  23. package/dist/hooks/pre-prompt.cjs.map +1 -1
  24. package/dist/hooks/pre-prompt.js +192 -243
  25. package/dist/hooks/pre-prompt.js.map +1 -1
  26. package/dist/hooks/stop-docs-refresh.cjs +123 -0
  27. package/dist/hooks/stop-docs-refresh.cjs.map +1 -0
  28. package/dist/hooks/stop-docs-refresh.d.cts +1 -0
  29. package/dist/hooks/stop-docs-refresh.d.ts +1 -0
  30. package/dist/hooks/stop-docs-refresh.js +126 -0
  31. package/dist/hooks/stop-docs-refresh.js.map +1 -0
  32. package/dist/index.cjs +1756 -339
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +540 -41
  35. package/dist/index.d.ts +540 -41
  36. package/dist/index.js +1739 -331
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp/index.cjs +306 -73
  39. package/dist/mcp/index.cjs.map +1 -1
  40. package/dist/mcp/index.js +310 -73
  41. package/dist/mcp/index.js.map +1 -1
  42. package/package.json +10 -3
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
9
16
  var __copyProps = (to, from, except, desc) => {
10
17
  if (from && typeof from === "object" || typeof from === "function") {
11
18
  for (let key of __getOwnPropNames(from))
@@ -23,12 +30,61 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
30
  mod
24
31
  ));
25
32
 
33
+ // node_modules/tsup/assets/cjs_shims.js
34
+ var init_cjs_shims = __esm({
35
+ "node_modules/tsup/assets/cjs_shims.js"() {
36
+ "use strict";
37
+ }
38
+ });
39
+
40
+ // src/utils/tokenizer.ts
41
+ var tokenizer_exports = {};
42
+ __export(tokenizer_exports, {
43
+ countTokensPrecise: () => countTokensPrecise,
44
+ estimateTokens: () => estimateTokens,
45
+ setPreciseTokenCounting: () => setPreciseTokenCounting
46
+ });
47
+ function setPreciseTokenCounting(enabled) {
48
+ usePrecise = enabled;
49
+ }
50
+ function countTokensPrecise(text) {
51
+ if (!text || text.length === 0) {
52
+ return 0;
53
+ }
54
+ return (0, import_gpt_tokenizer.encode)(text).length;
55
+ }
56
+ function estimateTokens(text) {
57
+ if (!text || text.length === 0) {
58
+ return 0;
59
+ }
60
+ if (usePrecise) {
61
+ return (0, import_gpt_tokenizer.encode)(text).length;
62
+ }
63
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
64
+ const wordCount = words.length;
65
+ const charCount = text.length;
66
+ const charEstimate = Math.ceil(charCount / 4);
67
+ const wordEstimate = Math.ceil(wordCount * 0.75);
68
+ return Math.max(wordEstimate, charEstimate);
69
+ }
70
+ var import_gpt_tokenizer, usePrecise;
71
+ var init_tokenizer = __esm({
72
+ "src/utils/tokenizer.ts"() {
73
+ "use strict";
74
+ init_cjs_shims();
75
+ import_gpt_tokenizer = require("gpt-tokenizer");
76
+ usePrecise = false;
77
+ }
78
+ });
79
+
26
80
  // src/mcp/index.ts
81
+ init_cjs_shims();
27
82
  var import_node_fs2 = require("fs");
28
83
  var import_node_path = require("path");
29
84
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
30
85
 
31
86
  // src/core/kv-memory.ts
87
+ init_cjs_shims();
32
88
  var import_node_fs = require("fs");
33
89
  var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
34
90
  function createBackup(dbPath) {
@@ -50,6 +106,7 @@ async function createKVMemory(dbPath) {
50
106
  const integrityCheck = db.pragma("quick_check", { simple: true });
51
107
  if (integrityCheck !== "ok") {
52
108
  console.error("\u26A0 Database corruption detected!");
109
+ db.close();
53
110
  if ((0, import_node_fs.existsSync)(dbPath)) {
54
111
  const backupPath = createBackup(dbPath);
55
112
  if (backupPath) {
@@ -57,7 +114,6 @@ async function createKVMemory(dbPath) {
57
114
  }
58
115
  }
59
116
  console.log("Attempting database recovery...");
60
- db.close();
61
117
  db = new import_better_sqlite3.default(dbPath);
62
118
  }
63
119
  } catch (error) {
@@ -69,6 +125,7 @@ async function createKVMemory(dbPath) {
69
125
  db = new import_better_sqlite3.default(dbPath);
70
126
  }
71
127
  db.pragma("journal_mode = WAL");
128
+ db.pragma("foreign_keys = ON");
72
129
  db.exec(`
73
130
  CREATE TABLE IF NOT EXISTS entries_index (
74
131
  id TEXT PRIMARY KEY NOT NULL,
@@ -108,6 +165,36 @@ async function createKVMemory(dbPath) {
108
165
  CREATE INDEX IF NOT EXISTS idx_entries_timestamp ON entries_index(timestamp DESC);
109
166
  CREATE INDEX IF NOT EXISTS idx_stats_timestamp ON optimization_stats(timestamp DESC);
110
167
  `);
168
+ db.exec(`
169
+ CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(id, content, tokenize='porter');
170
+ `);
171
+ db.exec(`
172
+ CREATE TRIGGER IF NOT EXISTS entries_fts_insert
173
+ AFTER INSERT ON entries_value
174
+ BEGIN
175
+ INSERT OR REPLACE INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
176
+ END;
177
+ `);
178
+ db.exec(`
179
+ CREATE TRIGGER IF NOT EXISTS entries_fts_delete
180
+ AFTER DELETE ON entries_value
181
+ BEGIN
182
+ DELETE FROM entries_fts WHERE id = OLD.id;
183
+ END;
184
+ `);
185
+ db.exec(`
186
+ CREATE TRIGGER IF NOT EXISTS entries_fts_update
187
+ AFTER UPDATE ON entries_value
188
+ BEGIN
189
+ DELETE FROM entries_fts WHERE id = OLD.id;
190
+ INSERT INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
191
+ END;
192
+ `);
193
+ db.exec(`
194
+ INSERT OR IGNORE INTO entries_fts(id, content)
195
+ SELECT id, content FROM entries_value
196
+ WHERE id NOT IN (SELECT id FROM entries_fts);
197
+ `);
111
198
  const putIndexStmt = db.prepare(`
112
199
  INSERT OR REPLACE INTO entries_index
113
200
  (id, hash, timestamp, score, ttl, state, accessCount, isBTSP)
@@ -196,14 +283,20 @@ async function createKVMemory(dbPath) {
196
283
  sql += " AND i.isBTSP = ?";
197
284
  params.push(filters.isBTSP ? 1 : 0);
198
285
  }
286
+ if (filters.tags && filters.tags.length > 0) {
287
+ for (const tag of filters.tags) {
288
+ sql += " AND v.tags LIKE ?";
289
+ params.push(`%"${tag}"%`);
290
+ }
291
+ }
199
292
  sql += " ORDER BY i.score DESC";
200
293
  if (filters.limit) {
201
294
  sql += " LIMIT ?";
202
295
  params.push(filters.limit);
203
- }
204
- if (filters.offset) {
205
- sql += " OFFSET ?";
206
- params.push(filters.offset);
296
+ if (filters.offset) {
297
+ sql += " OFFSET ?";
298
+ params.push(filters.offset);
299
+ }
207
300
  }
208
301
  const stmt = db.prepare(sql);
209
302
  const rows = stmt.all(...params);
@@ -238,7 +331,22 @@ async function createKVMemory(dbPath) {
238
331
  },
239
332
  async compact() {
240
333
  const before = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
241
- db.exec("DELETE FROM entries_index WHERE ttl <= 0");
334
+ const now = Date.now();
335
+ db.prepare("DELETE FROM entries_index WHERE isBTSP = 0 AND (timestamp + ttl * 1000) < ?").run(
336
+ now
337
+ );
338
+ db.exec("DELETE FROM entries_index WHERE isBTSP = 0 AND ttl <= 0");
339
+ const candidates = db.prepare("SELECT id, timestamp, ttl FROM entries_index WHERE isBTSP = 0").all();
340
+ for (const row of candidates) {
341
+ const ageSeconds = Math.max(0, (now - row.timestamp) / 1e3);
342
+ const ttlSeconds = row.ttl;
343
+ if (ttlSeconds <= 0) continue;
344
+ const decay = 1 - Math.exp(-ageSeconds / ttlSeconds);
345
+ if (decay >= 0.95) {
346
+ db.prepare("DELETE FROM entries_index WHERE id = ?").run(row.id);
347
+ }
348
+ }
349
+ db.exec("DELETE FROM entries_value WHERE id NOT IN (SELECT id FROM entries_index)");
242
350
  db.exec("VACUUM");
243
351
  const after = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
244
352
  return before.count - after.count;
@@ -258,6 +366,9 @@ async function createKVMemory(dbPath) {
258
366
  stats.entries_pruned,
259
367
  stats.duration_ms
260
368
  );
369
+ db.prepare(
370
+ "DELETE FROM optimization_stats WHERE id NOT IN (SELECT id FROM optimization_stats ORDER BY timestamp DESC LIMIT 1000)"
371
+ ).run();
261
372
  },
262
373
  async getOptimizationStats() {
263
374
  const stmt = db.prepare(`
@@ -270,28 +381,70 @@ async function createKVMemory(dbPath) {
270
381
  },
271
382
  async clearOptimizationStats() {
272
383
  db.exec("DELETE FROM optimization_stats");
384
+ },
385
+ async searchFTS(query, limit = 10) {
386
+ if (!query || query.trim().length === 0) return [];
387
+ const sanitized = query.replace(/[{}()[\]"':*^~]/g, " ").trim();
388
+ if (sanitized.length === 0) return [];
389
+ const stmt = db.prepare(`
390
+ SELECT
391
+ f.id, f.content, rank,
392
+ i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
393
+ v.tags, v.metadata
394
+ FROM entries_fts f
395
+ JOIN entries_index i ON f.id = i.id
396
+ JOIN entries_value v ON f.id = v.id
397
+ WHERE entries_fts MATCH ?
398
+ ORDER BY rank
399
+ LIMIT ?
400
+ `);
401
+ try {
402
+ const rows = stmt.all(sanitized, limit);
403
+ return rows.map((r) => ({
404
+ entry: {
405
+ id: r.id,
406
+ content: r.content,
407
+ hash: r.hash,
408
+ timestamp: r.timestamp,
409
+ score: r.score,
410
+ ttl: r.ttl,
411
+ state: r.state,
412
+ accessCount: r.accessCount,
413
+ tags: r.tags ? JSON.parse(r.tags) : [],
414
+ metadata: r.metadata ? JSON.parse(r.metadata) : {},
415
+ isBTSP: r.isBTSP === 1
416
+ },
417
+ rank: r.rank
418
+ }));
419
+ } catch {
420
+ return [];
421
+ }
273
422
  }
274
423
  };
275
424
  }
276
425
 
277
426
  // src/mcp/server.ts
427
+ init_cjs_shims();
278
428
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
279
429
  var import_zod = require("zod");
280
430
 
281
431
  // src/adapters/generic.ts
432
+ init_cjs_shims();
282
433
  var import_node_crypto3 = require("crypto");
283
434
 
284
435
  // src/core/btsp-embedder.ts
436
+ init_cjs_shims();
285
437
  var import_node_crypto2 = require("crypto");
286
438
 
287
439
  // src/utils/hash.ts
440
+ init_cjs_shims();
288
441
  var import_node_crypto = require("crypto");
289
442
  function hashContent(content) {
290
443
  return (0, import_node_crypto.createHash)("sha256").update(content, "utf8").digest("hex");
291
444
  }
292
445
 
293
446
  // src/core/btsp-embedder.ts
294
- function createBTSPEmbedder() {
447
+ function createBTSPEmbedder(config) {
295
448
  const BTSP_PATTERNS = [
296
449
  // Error patterns
297
450
  /\b(error|exception|failure|fatal|critical|panic)\b/i,
@@ -310,6 +463,14 @@ function createBTSPEmbedder() {
310
463
  /^=======/m,
311
464
  /^>>>>>>> /m
312
465
  ];
466
+ if (config?.customPatterns) {
467
+ for (const pattern of config.customPatterns) {
468
+ try {
469
+ BTSP_PATTERNS.push(new RegExp(pattern));
470
+ } catch {
471
+ }
472
+ }
473
+ }
313
474
  function detectBTSP(content) {
314
475
  return BTSP_PATTERNS.some((pattern) => pattern.test(content));
315
476
  }
@@ -338,13 +499,14 @@ function createBTSPEmbedder() {
338
499
  }
339
500
 
340
501
  // src/core/confidence-states.ts
502
+ init_cjs_shims();
341
503
  function createConfidenceStates(config) {
342
504
  const { activeThreshold, readyThreshold } = config;
343
505
  function calculateState(entry) {
344
506
  if (entry.isBTSP) {
345
507
  return "active";
346
508
  }
347
- if (entry.score > activeThreshold) {
509
+ if (entry.score >= activeThreshold) {
348
510
  return "active";
349
511
  }
350
512
  if (entry.score >= readyThreshold) {
@@ -380,8 +542,11 @@ function createConfidenceStates(config) {
380
542
  }
381
543
 
382
544
  // src/core/engram-scorer.ts
545
+ init_cjs_shims();
383
546
  function createEngramScorer(config) {
384
547
  const { defaultTTL } = config;
548
+ const recencyWindowMs = (config.recencyBoostMinutes ?? 30) * 60 * 1e3;
549
+ const recencyMultiplier = config.recencyBoostMultiplier ?? 1.3;
385
550
  function calculateDecay(ageInSeconds, ttlInSeconds) {
386
551
  if (ttlInSeconds === 0) return 1;
387
552
  if (ageInSeconds <= 0) return 0;
@@ -401,6 +566,13 @@ function createEngramScorer(config) {
401
566
  if (entry.isBTSP) {
402
567
  score = Math.max(score, 0.9);
403
568
  }
569
+ if (!entry.isBTSP && recencyWindowMs > 0) {
570
+ const ageMs = currentTime - entry.timestamp;
571
+ if (ageMs >= 0 && ageMs < recencyWindowMs) {
572
+ const boostFactor = 1 + (recencyMultiplier - 1) * (1 - ageMs / recencyWindowMs);
573
+ score = score * boostFactor;
574
+ }
575
+ }
404
576
  return Math.max(0, Math.min(1, score));
405
577
  }
406
578
  function refreshTTL(entry) {
@@ -418,49 +590,53 @@ function createEngramScorer(config) {
418
590
  };
419
591
  }
420
592
 
421
- // src/utils/tokenizer.ts
422
- function estimateTokens(text) {
423
- if (!text || text.length === 0) {
424
- return 0;
593
+ // src/core/sparse-pruner.ts
594
+ init_cjs_shims();
595
+
596
+ // src/utils/tfidf.ts
597
+ init_cjs_shims();
598
+ function tokenize(text) {
599
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
600
+ }
601
+ function calculateTF(term, tokens) {
602
+ const count = tokens.filter((t) => t === term).length;
603
+ return Math.sqrt(count);
604
+ }
605
+ function createTFIDFIndex(entries) {
606
+ const documentFrequency = /* @__PURE__ */ new Map();
607
+ for (const entry of entries) {
608
+ const tokens = tokenize(entry.content);
609
+ const uniqueTerms = new Set(tokens);
610
+ for (const term of uniqueTerms) {
611
+ documentFrequency.set(term, (documentFrequency.get(term) || 0) + 1);
612
+ }
425
613
  }
426
- const words = text.split(/\s+/).filter((w) => w.length > 0);
427
- const wordCount = words.length;
428
- const charCount = text.length;
429
- const charEstimate = Math.ceil(charCount / 4);
430
- const wordEstimate = Math.ceil(wordCount * 0.75);
431
- return Math.max(wordEstimate, charEstimate);
614
+ return {
615
+ documentFrequency,
616
+ totalDocuments: entries.length
617
+ };
618
+ }
619
+ function scoreTFIDF(entry, index) {
620
+ const tokens = tokenize(entry.content);
621
+ if (tokens.length === 0) return 0;
622
+ const uniqueTerms = new Set(tokens);
623
+ let totalScore = 0;
624
+ for (const term of uniqueTerms) {
625
+ const tf = calculateTF(term, tokens);
626
+ const docsWithTerm = index.documentFrequency.get(term) || 0;
627
+ if (docsWithTerm === 0) continue;
628
+ const idf = Math.log(index.totalDocuments / docsWithTerm);
629
+ totalScore += tf * idf;
630
+ }
631
+ return totalScore / tokens.length;
432
632
  }
433
633
 
434
634
  // src/core/sparse-pruner.ts
635
+ init_tokenizer();
435
636
  function createSparsePruner(config) {
436
637
  const { threshold } = config;
437
- function tokenize(text) {
438
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
439
- }
440
- function calculateTF(term, tokens) {
441
- const count = tokens.filter((t) => t === term).length;
442
- return Math.sqrt(count);
443
- }
444
- function calculateIDF(term, allEntries) {
445
- const totalDocs = allEntries.length;
446
- const docsWithTerm = allEntries.filter((entry) => {
447
- const tokens = tokenize(entry.content);
448
- return tokens.includes(term);
449
- }).length;
450
- if (docsWithTerm === 0) return 0;
451
- return Math.log(totalDocs / docsWithTerm);
452
- }
453
638
  function scoreEntry(entry, allEntries) {
454
- const tokens = tokenize(entry.content);
455
- if (tokens.length === 0) return 0;
456
- const uniqueTerms = [...new Set(tokens)];
457
- let totalScore = 0;
458
- for (const term of uniqueTerms) {
459
- const tf = calculateTF(term, tokens);
460
- const idf = calculateIDF(term, allEntries);
461
- totalScore += tf * idf;
462
- }
463
- return totalScore / tokens.length;
639
+ return scoreTFIDF(entry, createTFIDFIndex(allEntries));
464
640
  }
465
641
  function prune(entries) {
466
642
  if (entries.length === 0) {
@@ -472,9 +648,10 @@ function createSparsePruner(config) {
472
648
  };
473
649
  }
474
650
  const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
651
+ const tfidfIndex = createTFIDFIndex(entries);
475
652
  const scored = entries.map((entry) => ({
476
653
  entry,
477
- score: scoreEntry(entry, entries)
654
+ score: scoreTFIDF(entry, tfidfIndex)
478
655
  }));
479
656
  scored.sort((a, b) => b.score - a.score);
480
657
  const keepCount = Math.max(1, Math.ceil(entries.length * (threshold / 100)));
@@ -495,29 +672,33 @@ function createSparsePruner(config) {
495
672
  }
496
673
 
497
674
  // src/adapters/generic.ts
675
+ init_tokenizer();
498
676
  function createGenericAdapter(memory, config) {
499
677
  const pruner = createSparsePruner(config.pruning);
500
678
  const scorer = createEngramScorer(config.decay);
501
679
  const states = createConfidenceStates(config.states);
502
- const btsp = createBTSPEmbedder();
680
+ const btsp = createBTSPEmbedder({ customPatterns: config.btspPatterns });
503
681
  async function optimize(context, options = {}) {
504
682
  const startTime = Date.now();
505
683
  const lines = context.split("\n").filter((line) => line.trim().length > 0);
506
- const entries = lines.map((content) => ({
507
- id: (0, import_node_crypto3.randomUUID)(),
508
- content,
509
- hash: hashContent(content),
510
- timestamp: Date.now(),
511
- score: btsp.detectBTSP(content) ? 1 : 0.5,
512
- // BTSP gets high initial score
513
- ttl: config.decay.defaultTTL * 3600,
514
- // Convert hours to seconds
515
- state: "ready",
516
- accessCount: 0,
517
- tags: [],
518
- metadata: {},
519
- isBTSP: btsp.detectBTSP(content)
520
- }));
684
+ const now = Date.now();
685
+ const entries = lines.map((content, index) => {
686
+ const isBTSP = btsp.detectBTSP(content);
687
+ return {
688
+ id: (0, import_node_crypto3.randomUUID)(),
689
+ content,
690
+ hash: hashContent(content),
691
+ timestamp: now + index,
692
+ // Unique timestamps preserve ordering
693
+ score: isBTSP ? 1 : 0.5,
694
+ ttl: config.decay.defaultTTL * 3600,
695
+ state: "ready",
696
+ accessCount: 0,
697
+ tags: [],
698
+ metadata: {},
699
+ isBTSP
700
+ };
701
+ });
521
702
  const tokensBefore = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
522
703
  const scoredEntries = entries.map((entry) => ({
523
704
  ...entry,
@@ -570,6 +751,7 @@ function createGenericAdapter(memory, config) {
570
751
  }
571
752
 
572
753
  // src/core/sleep-compressor.ts
754
+ init_cjs_shims();
573
755
  function createSleepCompressor() {
574
756
  const scorer = createEngramScorer({ defaultTTL: 24, decayThreshold: 0.95 });
575
757
  function consolidate(entries) {
@@ -660,13 +842,15 @@ function createSleepCompressor() {
660
842
  function cosineSimilarity(text1, text2) {
661
843
  const words1 = tokenize(text1);
662
844
  const words2 = tokenize(text2);
663
- const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
664
845
  const vec1 = {};
665
846
  const vec2 = {};
666
- for (const word of vocab) {
667
- vec1[word] = words1.filter((w) => w === word).length;
668
- vec2[word] = words2.filter((w) => w === word).length;
847
+ for (const word of words1) {
848
+ vec1[word] = (vec1[word] ?? 0) + 1;
849
+ }
850
+ for (const word of words2) {
851
+ vec2[word] = (vec2[word] ?? 0) + 1;
669
852
  }
853
+ const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
670
854
  let dotProduct = 0;
671
855
  let mag1 = 0;
672
856
  let mag2 = 0;
@@ -682,9 +866,6 @@ function createSleepCompressor() {
682
866
  if (mag1 === 0 || mag2 === 0) return 0;
683
867
  return dotProduct / (mag1 * mag2);
684
868
  }
685
- function tokenize(text) {
686
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
687
- }
688
869
  return {
689
870
  consolidate,
690
871
  findDuplicates,
@@ -693,6 +874,7 @@ function createSleepCompressor() {
693
874
  }
694
875
 
695
876
  // src/types/config.ts
877
+ init_cjs_shims();
696
878
  var DEFAULT_CONFIG = {
697
879
  pruning: {
698
880
  threshold: 5,
@@ -714,8 +896,8 @@ var DEFAULT_CONFIG = {
714
896
  },
715
897
  autoConsolidate: null,
716
898
  realtime: {
717
- tokenBudget: 5e4,
718
- autoOptimizeThreshold: 8e4,
899
+ tokenBudget: 4e4,
900
+ autoOptimizeThreshold: 6e4,
719
901
  watchPatterns: ["**/*.jsonl"],
720
902
  pidFile: ".sparn/daemon.pid",
721
903
  logFile: ".sparn/daemon.log",
@@ -731,11 +913,12 @@ function createSparnMcpServer(options) {
731
913
  const { memory, config = DEFAULT_CONFIG } = options;
732
914
  const server = new import_mcp.McpServer({
733
915
  name: "sparn",
734
- version: "1.1.1"
916
+ version: "1.4.0"
735
917
  });
736
918
  registerOptimizeTool(server, memory, config);
737
919
  registerStatsTool(server, memory);
738
920
  registerConsolidateTool(server, memory);
921
+ registerSearchTool(server, memory);
739
922
  return server;
740
923
  }
741
924
  function registerOptimizeTool(server, memory, config) {
@@ -743,7 +926,7 @@ function registerOptimizeTool(server, memory, config) {
743
926
  "sparn_optimize",
744
927
  {
745
928
  title: "Sparn Optimize",
746
- description: "Optimize context using neuroscience-inspired pruning. Applies BTSP detection, engram scoring, confidence states, and sparse pruning to reduce token usage while preserving important information.",
929
+ description: "Optimize context using multi-stage pruning. Applies critical event detection, relevance scoring, entry classification, and sparse pruning to reduce token usage while preserving important information.",
747
930
  inputSchema: {
748
931
  context: import_zod.z.string().describe("The context text to optimize"),
749
932
  dryRun: import_zod.z.boolean().optional().default(false).describe("If true, do not persist changes to the memory store"),
@@ -873,12 +1056,58 @@ function registerStatsTool(server, memory) {
873
1056
  }
874
1057
  );
875
1058
  }
1059
+ function registerSearchTool(server, memory) {
1060
+ server.registerTool(
1061
+ "sparn_search",
1062
+ {
1063
+ title: "Sparn Search",
1064
+ description: "Search memory entries using full-text search. Returns matching entries with relevance ranking, score, and state information.",
1065
+ inputSchema: {
1066
+ query: import_zod.z.string().describe("Search query text"),
1067
+ limit: import_zod.z.number().int().min(1).max(100).optional().default(10).describe("Maximum number of results (1-100, default 10)")
1068
+ }
1069
+ },
1070
+ async ({ query, limit }) => {
1071
+ try {
1072
+ const results = await memory.searchFTS(query, limit);
1073
+ const response = results.map((r) => ({
1074
+ id: r.entry.id,
1075
+ content: r.entry.content.length > 500 ? `${r.entry.content.slice(0, 500)}...` : r.entry.content,
1076
+ score: r.entry.score,
1077
+ state: r.entry.state,
1078
+ rank: r.rank,
1079
+ tags: r.entry.tags,
1080
+ isBTSP: r.entry.isBTSP
1081
+ }));
1082
+ return {
1083
+ content: [
1084
+ {
1085
+ type: "text",
1086
+ text: JSON.stringify({ results: response, total: response.length }, null, 2)
1087
+ }
1088
+ ]
1089
+ };
1090
+ } catch (error) {
1091
+ const message = error instanceof Error ? error.message : String(error);
1092
+ return {
1093
+ content: [
1094
+ {
1095
+ type: "text",
1096
+ text: JSON.stringify({ error: message })
1097
+ }
1098
+ ],
1099
+ isError: true
1100
+ };
1101
+ }
1102
+ }
1103
+ );
1104
+ }
876
1105
  function registerConsolidateTool(server, memory) {
877
1106
  server.registerTool(
878
1107
  "sparn_consolidate",
879
1108
  {
880
1109
  title: "Sparn Consolidate",
881
- description: "Run memory consolidation (sleep replay). Removes decayed entries and merges duplicates to reclaim space. Inspired by the neuroscience principle of sleep-based memory consolidation."
1110
+ description: "Run memory consolidation. Removes decayed entries and merges duplicates to reclaim space."
882
1111
  },
883
1112
  async () => {
884
1113
  try {
@@ -934,6 +1163,10 @@ function registerConsolidateTool(server, memory) {
934
1163
 
935
1164
  // src/mcp/index.ts
936
1165
  async function main() {
1166
+ if (process.env["SPARN_PRECISE_TOKENS"] === "true") {
1167
+ const { setPreciseTokenCounting: setPreciseTokenCounting2 } = await Promise.resolve().then(() => (init_tokenizer(), tokenizer_exports));
1168
+ setPreciseTokenCounting2(true);
1169
+ }
937
1170
  const dbPath = (0, import_node_path.resolve)(process.env["SPARN_DB_PATH"] ?? ".sparn/memory.db");
938
1171
  (0, import_node_fs2.mkdirSync)((0, import_node_path.dirname)(dbPath), { recursive: true });
939
1172
  const memory = await createKVMemory(dbPath);