@ulrichc1/sparn 1.2.2 → 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 +3853 -484
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +3810 -457
  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 +115 -266
  19. package/dist/hooks/post-tool-result.cjs.map +1 -1
  20. package/dist/hooks/post-tool-result.js +115 -266
  21. package/dist/hooks/post-tool-result.js.map +1 -1
  22. package/dist/hooks/pre-prompt.cjs +197 -268
  23. package/dist/hooks/pre-prompt.cjs.map +1 -1
  24. package/dist/hooks/pre-prompt.js +182 -268
  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 +1754 -337
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +539 -40
  35. package/dist/index.d.ts +539 -40
  36. package/dist/index.js +1737 -329
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp/index.cjs +304 -71
  39. package/dist/mcp/index.cjs.map +1 -1
  40. package/dist/mcp/index.js +308 -71
  41. package/dist/mcp/index.js.map +1 -1
  42. package/package.json +10 -3
package/dist/mcp/index.js CHANGED
@@ -1,11 +1,71 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // node_modules/tsup/assets/esm_shims.js
13
+ import path from "path";
14
+ import { fileURLToPath } from "url";
15
+ var init_esm_shims = __esm({
16
+ "node_modules/tsup/assets/esm_shims.js"() {
17
+ "use strict";
18
+ }
19
+ });
20
+
21
+ // src/utils/tokenizer.ts
22
+ var tokenizer_exports = {};
23
+ __export(tokenizer_exports, {
24
+ countTokensPrecise: () => countTokensPrecise,
25
+ estimateTokens: () => estimateTokens,
26
+ setPreciseTokenCounting: () => setPreciseTokenCounting
27
+ });
28
+ import { encode } from "gpt-tokenizer";
29
+ function setPreciseTokenCounting(enabled) {
30
+ usePrecise = enabled;
31
+ }
32
+ function countTokensPrecise(text) {
33
+ if (!text || text.length === 0) {
34
+ return 0;
35
+ }
36
+ return encode(text).length;
37
+ }
38
+ function estimateTokens(text) {
39
+ if (!text || text.length === 0) {
40
+ return 0;
41
+ }
42
+ if (usePrecise) {
43
+ return encode(text).length;
44
+ }
45
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
46
+ const wordCount = words.length;
47
+ const charCount = text.length;
48
+ const charEstimate = Math.ceil(charCount / 4);
49
+ const wordEstimate = Math.ceil(wordCount * 0.75);
50
+ return Math.max(wordEstimate, charEstimate);
51
+ }
52
+ var usePrecise;
53
+ var init_tokenizer = __esm({
54
+ "src/utils/tokenizer.ts"() {
55
+ "use strict";
56
+ init_esm_shims();
57
+ usePrecise = false;
58
+ }
59
+ });
2
60
 
3
61
  // src/mcp/index.ts
62
+ init_esm_shims();
4
63
  import { mkdirSync } from "fs";
5
64
  import { dirname, resolve } from "path";
6
65
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
66
 
8
67
  // src/core/kv-memory.ts
68
+ init_esm_shims();
9
69
  import { copyFileSync, existsSync } from "fs";
10
70
  import Database from "better-sqlite3";
11
71
  function createBackup(dbPath) {
@@ -27,6 +87,7 @@ async function createKVMemory(dbPath) {
27
87
  const integrityCheck = db.pragma("quick_check", { simple: true });
28
88
  if (integrityCheck !== "ok") {
29
89
  console.error("\u26A0 Database corruption detected!");
90
+ db.close();
30
91
  if (existsSync(dbPath)) {
31
92
  const backupPath = createBackup(dbPath);
32
93
  if (backupPath) {
@@ -34,7 +95,6 @@ async function createKVMemory(dbPath) {
34
95
  }
35
96
  }
36
97
  console.log("Attempting database recovery...");
37
- db.close();
38
98
  db = new Database(dbPath);
39
99
  }
40
100
  } catch (error) {
@@ -46,6 +106,7 @@ async function createKVMemory(dbPath) {
46
106
  db = new Database(dbPath);
47
107
  }
48
108
  db.pragma("journal_mode = WAL");
109
+ db.pragma("foreign_keys = ON");
49
110
  db.exec(`
50
111
  CREATE TABLE IF NOT EXISTS entries_index (
51
112
  id TEXT PRIMARY KEY NOT NULL,
@@ -85,6 +146,36 @@ async function createKVMemory(dbPath) {
85
146
  CREATE INDEX IF NOT EXISTS idx_entries_timestamp ON entries_index(timestamp DESC);
86
147
  CREATE INDEX IF NOT EXISTS idx_stats_timestamp ON optimization_stats(timestamp DESC);
87
148
  `);
149
+ db.exec(`
150
+ CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(id, content, tokenize='porter');
151
+ `);
152
+ db.exec(`
153
+ CREATE TRIGGER IF NOT EXISTS entries_fts_insert
154
+ AFTER INSERT ON entries_value
155
+ BEGIN
156
+ INSERT OR REPLACE INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
157
+ END;
158
+ `);
159
+ db.exec(`
160
+ CREATE TRIGGER IF NOT EXISTS entries_fts_delete
161
+ AFTER DELETE ON entries_value
162
+ BEGIN
163
+ DELETE FROM entries_fts WHERE id = OLD.id;
164
+ END;
165
+ `);
166
+ db.exec(`
167
+ CREATE TRIGGER IF NOT EXISTS entries_fts_update
168
+ AFTER UPDATE ON entries_value
169
+ BEGIN
170
+ DELETE FROM entries_fts WHERE id = OLD.id;
171
+ INSERT INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
172
+ END;
173
+ `);
174
+ db.exec(`
175
+ INSERT OR IGNORE INTO entries_fts(id, content)
176
+ SELECT id, content FROM entries_value
177
+ WHERE id NOT IN (SELECT id FROM entries_fts);
178
+ `);
88
179
  const putIndexStmt = db.prepare(`
89
180
  INSERT OR REPLACE INTO entries_index
90
181
  (id, hash, timestamp, score, ttl, state, accessCount, isBTSP)
@@ -173,14 +264,20 @@ async function createKVMemory(dbPath) {
173
264
  sql += " AND i.isBTSP = ?";
174
265
  params.push(filters.isBTSP ? 1 : 0);
175
266
  }
267
+ if (filters.tags && filters.tags.length > 0) {
268
+ for (const tag of filters.tags) {
269
+ sql += " AND v.tags LIKE ?";
270
+ params.push(`%"${tag}"%`);
271
+ }
272
+ }
176
273
  sql += " ORDER BY i.score DESC";
177
274
  if (filters.limit) {
178
275
  sql += " LIMIT ?";
179
276
  params.push(filters.limit);
180
- }
181
- if (filters.offset) {
182
- sql += " OFFSET ?";
183
- params.push(filters.offset);
277
+ if (filters.offset) {
278
+ sql += " OFFSET ?";
279
+ params.push(filters.offset);
280
+ }
184
281
  }
185
282
  const stmt = db.prepare(sql);
186
283
  const rows = stmt.all(...params);
@@ -215,7 +312,22 @@ async function createKVMemory(dbPath) {
215
312
  },
216
313
  async compact() {
217
314
  const before = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
218
- db.exec("DELETE FROM entries_index WHERE ttl <= 0");
315
+ const now = Date.now();
316
+ db.prepare("DELETE FROM entries_index WHERE isBTSP = 0 AND (timestamp + ttl * 1000) < ?").run(
317
+ now
318
+ );
319
+ db.exec("DELETE FROM entries_index WHERE isBTSP = 0 AND ttl <= 0");
320
+ const candidates = db.prepare("SELECT id, timestamp, ttl FROM entries_index WHERE isBTSP = 0").all();
321
+ for (const row of candidates) {
322
+ const ageSeconds = Math.max(0, (now - row.timestamp) / 1e3);
323
+ const ttlSeconds = row.ttl;
324
+ if (ttlSeconds <= 0) continue;
325
+ const decay = 1 - Math.exp(-ageSeconds / ttlSeconds);
326
+ if (decay >= 0.95) {
327
+ db.prepare("DELETE FROM entries_index WHERE id = ?").run(row.id);
328
+ }
329
+ }
330
+ db.exec("DELETE FROM entries_value WHERE id NOT IN (SELECT id FROM entries_index)");
219
331
  db.exec("VACUUM");
220
332
  const after = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
221
333
  return before.count - after.count;
@@ -235,6 +347,9 @@ async function createKVMemory(dbPath) {
235
347
  stats.entries_pruned,
236
348
  stats.duration_ms
237
349
  );
350
+ db.prepare(
351
+ "DELETE FROM optimization_stats WHERE id NOT IN (SELECT id FROM optimization_stats ORDER BY timestamp DESC LIMIT 1000)"
352
+ ).run();
238
353
  },
239
354
  async getOptimizationStats() {
240
355
  const stmt = db.prepare(`
@@ -247,28 +362,70 @@ async function createKVMemory(dbPath) {
247
362
  },
248
363
  async clearOptimizationStats() {
249
364
  db.exec("DELETE FROM optimization_stats");
365
+ },
366
+ async searchFTS(query, limit = 10) {
367
+ if (!query || query.trim().length === 0) return [];
368
+ const sanitized = query.replace(/[{}()[\]"':*^~]/g, " ").trim();
369
+ if (sanitized.length === 0) return [];
370
+ const stmt = db.prepare(`
371
+ SELECT
372
+ f.id, f.content, rank,
373
+ i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
374
+ v.tags, v.metadata
375
+ FROM entries_fts f
376
+ JOIN entries_index i ON f.id = i.id
377
+ JOIN entries_value v ON f.id = v.id
378
+ WHERE entries_fts MATCH ?
379
+ ORDER BY rank
380
+ LIMIT ?
381
+ `);
382
+ try {
383
+ const rows = stmt.all(sanitized, limit);
384
+ return rows.map((r) => ({
385
+ entry: {
386
+ id: r.id,
387
+ content: r.content,
388
+ hash: r.hash,
389
+ timestamp: r.timestamp,
390
+ score: r.score,
391
+ ttl: r.ttl,
392
+ state: r.state,
393
+ accessCount: r.accessCount,
394
+ tags: r.tags ? JSON.parse(r.tags) : [],
395
+ metadata: r.metadata ? JSON.parse(r.metadata) : {},
396
+ isBTSP: r.isBTSP === 1
397
+ },
398
+ rank: r.rank
399
+ }));
400
+ } catch {
401
+ return [];
402
+ }
250
403
  }
251
404
  };
252
405
  }
253
406
 
254
407
  // src/mcp/server.ts
408
+ init_esm_shims();
255
409
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
256
410
  import { z } from "zod";
257
411
 
258
412
  // src/adapters/generic.ts
413
+ init_esm_shims();
259
414
  import { randomUUID as randomUUID2 } from "crypto";
260
415
 
261
416
  // src/core/btsp-embedder.ts
417
+ init_esm_shims();
262
418
  import { randomUUID } from "crypto";
263
419
 
264
420
  // src/utils/hash.ts
421
+ init_esm_shims();
265
422
  import { createHash } from "crypto";
266
423
  function hashContent(content) {
267
424
  return createHash("sha256").update(content, "utf8").digest("hex");
268
425
  }
269
426
 
270
427
  // src/core/btsp-embedder.ts
271
- function createBTSPEmbedder() {
428
+ function createBTSPEmbedder(config) {
272
429
  const BTSP_PATTERNS = [
273
430
  // Error patterns
274
431
  /\b(error|exception|failure|fatal|critical|panic)\b/i,
@@ -287,6 +444,14 @@ function createBTSPEmbedder() {
287
444
  /^=======/m,
288
445
  /^>>>>>>> /m
289
446
  ];
447
+ if (config?.customPatterns) {
448
+ for (const pattern of config.customPatterns) {
449
+ try {
450
+ BTSP_PATTERNS.push(new RegExp(pattern));
451
+ } catch {
452
+ }
453
+ }
454
+ }
290
455
  function detectBTSP(content) {
291
456
  return BTSP_PATTERNS.some((pattern) => pattern.test(content));
292
457
  }
@@ -315,13 +480,14 @@ function createBTSPEmbedder() {
315
480
  }
316
481
 
317
482
  // src/core/confidence-states.ts
483
+ init_esm_shims();
318
484
  function createConfidenceStates(config) {
319
485
  const { activeThreshold, readyThreshold } = config;
320
486
  function calculateState(entry) {
321
487
  if (entry.isBTSP) {
322
488
  return "active";
323
489
  }
324
- if (entry.score > activeThreshold) {
490
+ if (entry.score >= activeThreshold) {
325
491
  return "active";
326
492
  }
327
493
  if (entry.score >= readyThreshold) {
@@ -357,8 +523,11 @@ function createConfidenceStates(config) {
357
523
  }
358
524
 
359
525
  // src/core/engram-scorer.ts
526
+ init_esm_shims();
360
527
  function createEngramScorer(config) {
361
528
  const { defaultTTL } = config;
529
+ const recencyWindowMs = (config.recencyBoostMinutes ?? 30) * 60 * 1e3;
530
+ const recencyMultiplier = config.recencyBoostMultiplier ?? 1.3;
362
531
  function calculateDecay(ageInSeconds, ttlInSeconds) {
363
532
  if (ttlInSeconds === 0) return 1;
364
533
  if (ageInSeconds <= 0) return 0;
@@ -378,6 +547,13 @@ function createEngramScorer(config) {
378
547
  if (entry.isBTSP) {
379
548
  score = Math.max(score, 0.9);
380
549
  }
550
+ if (!entry.isBTSP && recencyWindowMs > 0) {
551
+ const ageMs = currentTime - entry.timestamp;
552
+ if (ageMs >= 0 && ageMs < recencyWindowMs) {
553
+ const boostFactor = 1 + (recencyMultiplier - 1) * (1 - ageMs / recencyWindowMs);
554
+ score = score * boostFactor;
555
+ }
556
+ }
381
557
  return Math.max(0, Math.min(1, score));
382
558
  }
383
559
  function refreshTTL(entry) {
@@ -395,49 +571,53 @@ function createEngramScorer(config) {
395
571
  };
396
572
  }
397
573
 
398
- // src/utils/tokenizer.ts
399
- function estimateTokens(text) {
400
- if (!text || text.length === 0) {
401
- return 0;
574
+ // src/core/sparse-pruner.ts
575
+ init_esm_shims();
576
+
577
+ // src/utils/tfidf.ts
578
+ init_esm_shims();
579
+ function tokenize(text) {
580
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
581
+ }
582
+ function calculateTF(term, tokens) {
583
+ const count = tokens.filter((t) => t === term).length;
584
+ return Math.sqrt(count);
585
+ }
586
+ function createTFIDFIndex(entries) {
587
+ const documentFrequency = /* @__PURE__ */ new Map();
588
+ for (const entry of entries) {
589
+ const tokens = tokenize(entry.content);
590
+ const uniqueTerms = new Set(tokens);
591
+ for (const term of uniqueTerms) {
592
+ documentFrequency.set(term, (documentFrequency.get(term) || 0) + 1);
593
+ }
402
594
  }
403
- const words = text.split(/\s+/).filter((w) => w.length > 0);
404
- const wordCount = words.length;
405
- const charCount = text.length;
406
- const charEstimate = Math.ceil(charCount / 4);
407
- const wordEstimate = Math.ceil(wordCount * 0.75);
408
- return Math.max(wordEstimate, charEstimate);
595
+ return {
596
+ documentFrequency,
597
+ totalDocuments: entries.length
598
+ };
599
+ }
600
+ function scoreTFIDF(entry, index) {
601
+ const tokens = tokenize(entry.content);
602
+ if (tokens.length === 0) return 0;
603
+ const uniqueTerms = new Set(tokens);
604
+ let totalScore = 0;
605
+ for (const term of uniqueTerms) {
606
+ const tf = calculateTF(term, tokens);
607
+ const docsWithTerm = index.documentFrequency.get(term) || 0;
608
+ if (docsWithTerm === 0) continue;
609
+ const idf = Math.log(index.totalDocuments / docsWithTerm);
610
+ totalScore += tf * idf;
611
+ }
612
+ return totalScore / tokens.length;
409
613
  }
410
614
 
411
615
  // src/core/sparse-pruner.ts
616
+ init_tokenizer();
412
617
  function createSparsePruner(config) {
413
618
  const { threshold } = config;
414
- function tokenize(text) {
415
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
416
- }
417
- function calculateTF(term, tokens) {
418
- const count = tokens.filter((t) => t === term).length;
419
- return Math.sqrt(count);
420
- }
421
- function calculateIDF(term, allEntries) {
422
- const totalDocs = allEntries.length;
423
- const docsWithTerm = allEntries.filter((entry) => {
424
- const tokens = tokenize(entry.content);
425
- return tokens.includes(term);
426
- }).length;
427
- if (docsWithTerm === 0) return 0;
428
- return Math.log(totalDocs / docsWithTerm);
429
- }
430
619
  function scoreEntry(entry, allEntries) {
431
- const tokens = tokenize(entry.content);
432
- if (tokens.length === 0) return 0;
433
- const uniqueTerms = [...new Set(tokens)];
434
- let totalScore = 0;
435
- for (const term of uniqueTerms) {
436
- const tf = calculateTF(term, tokens);
437
- const idf = calculateIDF(term, allEntries);
438
- totalScore += tf * idf;
439
- }
440
- return totalScore / tokens.length;
620
+ return scoreTFIDF(entry, createTFIDFIndex(allEntries));
441
621
  }
442
622
  function prune(entries) {
443
623
  if (entries.length === 0) {
@@ -449,9 +629,10 @@ function createSparsePruner(config) {
449
629
  };
450
630
  }
451
631
  const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
632
+ const tfidfIndex = createTFIDFIndex(entries);
452
633
  const scored = entries.map((entry) => ({
453
634
  entry,
454
- score: scoreEntry(entry, entries)
635
+ score: scoreTFIDF(entry, tfidfIndex)
455
636
  }));
456
637
  scored.sort((a, b) => b.score - a.score);
457
638
  const keepCount = Math.max(1, Math.ceil(entries.length * (threshold / 100)));
@@ -472,29 +653,33 @@ function createSparsePruner(config) {
472
653
  }
473
654
 
474
655
  // src/adapters/generic.ts
656
+ init_tokenizer();
475
657
  function createGenericAdapter(memory, config) {
476
658
  const pruner = createSparsePruner(config.pruning);
477
659
  const scorer = createEngramScorer(config.decay);
478
660
  const states = createConfidenceStates(config.states);
479
- const btsp = createBTSPEmbedder();
661
+ const btsp = createBTSPEmbedder({ customPatterns: config.btspPatterns });
480
662
  async function optimize(context, options = {}) {
481
663
  const startTime = Date.now();
482
664
  const lines = context.split("\n").filter((line) => line.trim().length > 0);
483
- const entries = lines.map((content) => ({
484
- id: randomUUID2(),
485
- content,
486
- hash: hashContent(content),
487
- timestamp: Date.now(),
488
- score: btsp.detectBTSP(content) ? 1 : 0.5,
489
- // BTSP gets high initial score
490
- ttl: config.decay.defaultTTL * 3600,
491
- // Convert hours to seconds
492
- state: "ready",
493
- accessCount: 0,
494
- tags: [],
495
- metadata: {},
496
- isBTSP: btsp.detectBTSP(content)
497
- }));
665
+ const now = Date.now();
666
+ const entries = lines.map((content, index) => {
667
+ const isBTSP = btsp.detectBTSP(content);
668
+ return {
669
+ id: randomUUID2(),
670
+ content,
671
+ hash: hashContent(content),
672
+ timestamp: now + index,
673
+ // Unique timestamps preserve ordering
674
+ score: isBTSP ? 1 : 0.5,
675
+ ttl: config.decay.defaultTTL * 3600,
676
+ state: "ready",
677
+ accessCount: 0,
678
+ tags: [],
679
+ metadata: {},
680
+ isBTSP
681
+ };
682
+ });
498
683
  const tokensBefore = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
499
684
  const scoredEntries = entries.map((entry) => ({
500
685
  ...entry,
@@ -547,6 +732,7 @@ function createGenericAdapter(memory, config) {
547
732
  }
548
733
 
549
734
  // src/core/sleep-compressor.ts
735
+ init_esm_shims();
550
736
  function createSleepCompressor() {
551
737
  const scorer = createEngramScorer({ defaultTTL: 24, decayThreshold: 0.95 });
552
738
  function consolidate(entries) {
@@ -637,13 +823,15 @@ function createSleepCompressor() {
637
823
  function cosineSimilarity(text1, text2) {
638
824
  const words1 = tokenize(text1);
639
825
  const words2 = tokenize(text2);
640
- const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
641
826
  const vec1 = {};
642
827
  const vec2 = {};
643
- for (const word of vocab) {
644
- vec1[word] = words1.filter((w) => w === word).length;
645
- vec2[word] = words2.filter((w) => w === word).length;
828
+ for (const word of words1) {
829
+ vec1[word] = (vec1[word] ?? 0) + 1;
830
+ }
831
+ for (const word of words2) {
832
+ vec2[word] = (vec2[word] ?? 0) + 1;
646
833
  }
834
+ const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
647
835
  let dotProduct = 0;
648
836
  let mag1 = 0;
649
837
  let mag2 = 0;
@@ -659,9 +847,6 @@ function createSleepCompressor() {
659
847
  if (mag1 === 0 || mag2 === 0) return 0;
660
848
  return dotProduct / (mag1 * mag2);
661
849
  }
662
- function tokenize(text) {
663
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
664
- }
665
850
  return {
666
851
  consolidate,
667
852
  findDuplicates,
@@ -670,6 +855,7 @@ function createSleepCompressor() {
670
855
  }
671
856
 
672
857
  // src/types/config.ts
858
+ init_esm_shims();
673
859
  var DEFAULT_CONFIG = {
674
860
  pruning: {
675
861
  threshold: 5,
@@ -708,11 +894,12 @@ function createSparnMcpServer(options) {
708
894
  const { memory, config = DEFAULT_CONFIG } = options;
709
895
  const server = new McpServer({
710
896
  name: "sparn",
711
- version: "1.1.1"
897
+ version: "1.4.0"
712
898
  });
713
899
  registerOptimizeTool(server, memory, config);
714
900
  registerStatsTool(server, memory);
715
901
  registerConsolidateTool(server, memory);
902
+ registerSearchTool(server, memory);
716
903
  return server;
717
904
  }
718
905
  function registerOptimizeTool(server, memory, config) {
@@ -720,7 +907,7 @@ function registerOptimizeTool(server, memory, config) {
720
907
  "sparn_optimize",
721
908
  {
722
909
  title: "Sparn Optimize",
723
- 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.",
910
+ 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.",
724
911
  inputSchema: {
725
912
  context: z.string().describe("The context text to optimize"),
726
913
  dryRun: z.boolean().optional().default(false).describe("If true, do not persist changes to the memory store"),
@@ -850,12 +1037,58 @@ function registerStatsTool(server, memory) {
850
1037
  }
851
1038
  );
852
1039
  }
1040
+ function registerSearchTool(server, memory) {
1041
+ server.registerTool(
1042
+ "sparn_search",
1043
+ {
1044
+ title: "Sparn Search",
1045
+ description: "Search memory entries using full-text search. Returns matching entries with relevance ranking, score, and state information.",
1046
+ inputSchema: {
1047
+ query: z.string().describe("Search query text"),
1048
+ limit: z.number().int().min(1).max(100).optional().default(10).describe("Maximum number of results (1-100, default 10)")
1049
+ }
1050
+ },
1051
+ async ({ query, limit }) => {
1052
+ try {
1053
+ const results = await memory.searchFTS(query, limit);
1054
+ const response = results.map((r) => ({
1055
+ id: r.entry.id,
1056
+ content: r.entry.content.length > 500 ? `${r.entry.content.slice(0, 500)}...` : r.entry.content,
1057
+ score: r.entry.score,
1058
+ state: r.entry.state,
1059
+ rank: r.rank,
1060
+ tags: r.entry.tags,
1061
+ isBTSP: r.entry.isBTSP
1062
+ }));
1063
+ return {
1064
+ content: [
1065
+ {
1066
+ type: "text",
1067
+ text: JSON.stringify({ results: response, total: response.length }, null, 2)
1068
+ }
1069
+ ]
1070
+ };
1071
+ } catch (error) {
1072
+ const message = error instanceof Error ? error.message : String(error);
1073
+ return {
1074
+ content: [
1075
+ {
1076
+ type: "text",
1077
+ text: JSON.stringify({ error: message })
1078
+ }
1079
+ ],
1080
+ isError: true
1081
+ };
1082
+ }
1083
+ }
1084
+ );
1085
+ }
853
1086
  function registerConsolidateTool(server, memory) {
854
1087
  server.registerTool(
855
1088
  "sparn_consolidate",
856
1089
  {
857
1090
  title: "Sparn Consolidate",
858
- 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."
1091
+ description: "Run memory consolidation. Removes decayed entries and merges duplicates to reclaim space."
859
1092
  },
860
1093
  async () => {
861
1094
  try {
@@ -911,6 +1144,10 @@ function registerConsolidateTool(server, memory) {
911
1144
 
912
1145
  // src/mcp/index.ts
913
1146
  async function main() {
1147
+ if (process.env["SPARN_PRECISE_TOKENS"] === "true") {
1148
+ const { setPreciseTokenCounting: setPreciseTokenCounting2 } = await Promise.resolve().then(() => (init_tokenizer(), tokenizer_exports));
1149
+ setPreciseTokenCounting2(true);
1150
+ }
914
1151
  const dbPath = resolve(process.env["SPARN_DB_PATH"] ?? ".sparn/memory.db");
915
1152
  mkdirSync(dirname(dbPath), { recursive: true });
916
1153
  const memory = await createKVMemory(dbPath);