@ulrichc1/sparn 1.1.0 → 1.2.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.
@@ -0,0 +1,934 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp/index.ts
4
+ import { mkdirSync } from "fs";
5
+ import { dirname, resolve } from "path";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+
8
+ // src/core/kv-memory.ts
9
+ import { copyFileSync, existsSync } from "fs";
10
+ import Database from "better-sqlite3";
11
+ function createBackup(dbPath) {
12
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13
+ const backupPath = `${dbPath}.backup-${timestamp}`;
14
+ try {
15
+ copyFileSync(dbPath, backupPath);
16
+ console.log(`\u2713 Database backed up to: ${backupPath}`);
17
+ return backupPath;
18
+ } catch (error) {
19
+ console.error(`Warning: Could not create backup: ${error}`);
20
+ return "";
21
+ }
22
+ }
23
+ async function createKVMemory(dbPath) {
24
+ let db;
25
+ try {
26
+ db = new Database(dbPath);
27
+ const integrityCheck = db.pragma("quick_check", { simple: true });
28
+ if (integrityCheck !== "ok") {
29
+ console.error("\u26A0 Database corruption detected!");
30
+ if (existsSync(dbPath)) {
31
+ const backupPath = createBackup(dbPath);
32
+ if (backupPath) {
33
+ console.log(`Backup created at: ${backupPath}`);
34
+ }
35
+ }
36
+ console.log("Attempting database recovery...");
37
+ db.close();
38
+ db = new Database(dbPath);
39
+ }
40
+ } catch (error) {
41
+ console.error("\u26A0 Database error detected:", error);
42
+ if (existsSync(dbPath)) {
43
+ createBackup(dbPath);
44
+ console.log("Creating new database...");
45
+ }
46
+ db = new Database(dbPath);
47
+ }
48
+ db.pragma("journal_mode = WAL");
49
+ db.exec(`
50
+ CREATE TABLE IF NOT EXISTS entries_index (
51
+ id TEXT PRIMARY KEY NOT NULL,
52
+ hash TEXT UNIQUE NOT NULL,
53
+ timestamp INTEGER NOT NULL,
54
+ score REAL NOT NULL DEFAULT 0.0 CHECK(score >= 0.0 AND score <= 1.0),
55
+ ttl INTEGER NOT NULL CHECK(ttl >= 0),
56
+ state TEXT NOT NULL CHECK(state IN ('silent', 'ready', 'active')),
57
+ accessCount INTEGER NOT NULL DEFAULT 0 CHECK(accessCount >= 0),
58
+ isBTSP INTEGER NOT NULL DEFAULT 0 CHECK(isBTSP IN (0, 1)),
59
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
60
+ );
61
+ `);
62
+ db.exec(`
63
+ CREATE TABLE IF NOT EXISTS entries_value (
64
+ id TEXT PRIMARY KEY NOT NULL,
65
+ content TEXT NOT NULL,
66
+ tags TEXT,
67
+ metadata TEXT,
68
+ FOREIGN KEY (id) REFERENCES entries_index(id) ON DELETE CASCADE
69
+ );
70
+ `);
71
+ db.exec(`
72
+ CREATE TABLE IF NOT EXISTS optimization_stats (
73
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
74
+ timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
75
+ tokens_before INTEGER NOT NULL,
76
+ tokens_after INTEGER NOT NULL,
77
+ entries_pruned INTEGER NOT NULL,
78
+ duration_ms INTEGER NOT NULL
79
+ );
80
+ `);
81
+ db.exec(`
82
+ CREATE INDEX IF NOT EXISTS idx_entries_state ON entries_index(state);
83
+ CREATE INDEX IF NOT EXISTS idx_entries_score ON entries_index(score DESC);
84
+ CREATE INDEX IF NOT EXISTS idx_entries_hash ON entries_index(hash);
85
+ CREATE INDEX IF NOT EXISTS idx_entries_timestamp ON entries_index(timestamp DESC);
86
+ CREATE INDEX IF NOT EXISTS idx_stats_timestamp ON optimization_stats(timestamp DESC);
87
+ `);
88
+ const putIndexStmt = db.prepare(`
89
+ INSERT OR REPLACE INTO entries_index
90
+ (id, hash, timestamp, score, ttl, state, accessCount, isBTSP)
91
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
92
+ `);
93
+ const putValueStmt = db.prepare(`
94
+ INSERT OR REPLACE INTO entries_value
95
+ (id, content, tags, metadata)
96
+ VALUES (?, ?, ?, ?)
97
+ `);
98
+ const getStmt = db.prepare(`
99
+ SELECT
100
+ i.id, i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
101
+ v.content, v.tags, v.metadata
102
+ FROM entries_index i
103
+ JOIN entries_value v ON i.id = v.id
104
+ WHERE i.id = ?
105
+ `);
106
+ const deleteIndexStmt = db.prepare("DELETE FROM entries_index WHERE id = ?");
107
+ const deleteValueStmt = db.prepare("DELETE FROM entries_value WHERE id = ?");
108
+ return {
109
+ async put(entry) {
110
+ const transaction = db.transaction(() => {
111
+ putIndexStmt.run(
112
+ entry.id,
113
+ entry.hash,
114
+ entry.timestamp,
115
+ entry.score,
116
+ entry.ttl,
117
+ entry.state,
118
+ entry.accessCount,
119
+ entry.isBTSP ? 1 : 0
120
+ );
121
+ putValueStmt.run(
122
+ entry.id,
123
+ entry.content,
124
+ JSON.stringify(entry.tags),
125
+ JSON.stringify(entry.metadata)
126
+ );
127
+ });
128
+ transaction();
129
+ },
130
+ async get(id) {
131
+ const row = getStmt.get(id);
132
+ if (!row) {
133
+ return null;
134
+ }
135
+ const r = row;
136
+ return {
137
+ id: r.id,
138
+ content: r.content,
139
+ hash: r.hash,
140
+ timestamp: r.timestamp,
141
+ score: r.score,
142
+ ttl: r.ttl,
143
+ state: r.state,
144
+ accessCount: r.accessCount,
145
+ tags: r.tags ? JSON.parse(r.tags) : [],
146
+ metadata: r.metadata ? JSON.parse(r.metadata) : {},
147
+ isBTSP: r.isBTSP === 1
148
+ };
149
+ },
150
+ async query(filters) {
151
+ let sql = `
152
+ SELECT
153
+ i.id, i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
154
+ v.content, v.tags, v.metadata
155
+ FROM entries_index i
156
+ JOIN entries_value v ON i.id = v.id
157
+ WHERE 1=1
158
+ `;
159
+ const params = [];
160
+ if (filters.state) {
161
+ sql += " AND i.state = ?";
162
+ params.push(filters.state);
163
+ }
164
+ if (filters.minScore !== void 0) {
165
+ sql += " AND i.score >= ?";
166
+ params.push(filters.minScore);
167
+ }
168
+ if (filters.maxScore !== void 0) {
169
+ sql += " AND i.score <= ?";
170
+ params.push(filters.maxScore);
171
+ }
172
+ if (filters.isBTSP !== void 0) {
173
+ sql += " AND i.isBTSP = ?";
174
+ params.push(filters.isBTSP ? 1 : 0);
175
+ }
176
+ sql += " ORDER BY i.score DESC";
177
+ if (filters.limit) {
178
+ sql += " LIMIT ?";
179
+ params.push(filters.limit);
180
+ }
181
+ if (filters.offset) {
182
+ sql += " OFFSET ?";
183
+ params.push(filters.offset);
184
+ }
185
+ const stmt = db.prepare(sql);
186
+ const rows = stmt.all(...params);
187
+ return rows.map((row) => {
188
+ const r = row;
189
+ return {
190
+ id: r.id,
191
+ content: r.content,
192
+ hash: r.hash,
193
+ timestamp: r.timestamp,
194
+ score: r.score,
195
+ ttl: r.ttl,
196
+ state: r.state,
197
+ accessCount: r.accessCount,
198
+ tags: r.tags ? JSON.parse(r.tags) : [],
199
+ metadata: r.metadata ? JSON.parse(r.metadata) : {},
200
+ isBTSP: r.isBTSP === 1
201
+ };
202
+ });
203
+ },
204
+ async delete(id) {
205
+ const transaction = db.transaction(() => {
206
+ deleteIndexStmt.run(id);
207
+ deleteValueStmt.run(id);
208
+ });
209
+ transaction();
210
+ },
211
+ async list() {
212
+ const stmt = db.prepare("SELECT id FROM entries_index");
213
+ const rows = stmt.all();
214
+ return rows.map((r) => r.id);
215
+ },
216
+ async compact() {
217
+ const before = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
218
+ db.exec("DELETE FROM entries_index WHERE ttl <= 0");
219
+ db.exec("VACUUM");
220
+ const after = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
221
+ return before.count - after.count;
222
+ },
223
+ async close() {
224
+ db.close();
225
+ },
226
+ async recordOptimization(stats) {
227
+ const stmt = db.prepare(`
228
+ INSERT INTO optimization_stats (timestamp, tokens_before, tokens_after, entries_pruned, duration_ms)
229
+ VALUES (?, ?, ?, ?, ?)
230
+ `);
231
+ stmt.run(
232
+ stats.timestamp,
233
+ stats.tokens_before,
234
+ stats.tokens_after,
235
+ stats.entries_pruned,
236
+ stats.duration_ms
237
+ );
238
+ },
239
+ async getOptimizationStats() {
240
+ const stmt = db.prepare(`
241
+ SELECT id, timestamp, tokens_before, tokens_after, entries_pruned, duration_ms
242
+ FROM optimization_stats
243
+ ORDER BY timestamp DESC
244
+ `);
245
+ const rows = stmt.all();
246
+ return rows;
247
+ },
248
+ async clearOptimizationStats() {
249
+ db.exec("DELETE FROM optimization_stats");
250
+ }
251
+ };
252
+ }
253
+
254
+ // src/mcp/server.ts
255
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
256
+ import { z } from "zod";
257
+
258
+ // src/adapters/generic.ts
259
+ import { randomUUID as randomUUID2 } from "crypto";
260
+
261
+ // src/core/btsp-embedder.ts
262
+ import { randomUUID } from "crypto";
263
+
264
+ // src/utils/hash.ts
265
+ import { createHash } from "crypto";
266
+ function hashContent(content) {
267
+ return createHash("sha256").update(content, "utf8").digest("hex");
268
+ }
269
+
270
+ // src/core/btsp-embedder.ts
271
+ function createBTSPEmbedder() {
272
+ const BTSP_PATTERNS = [
273
+ // Error patterns
274
+ /\b(error|exception|failure|fatal|critical|panic)\b/i,
275
+ /\b(TypeError|ReferenceError|SyntaxError|RangeError|URIError)\b/,
276
+ /\bENOENT|EACCES|ECONNREFUSED|ETIMEDOUT\b/,
277
+ // Stack trace patterns
278
+ /^\s+at\s+.*\(.*:\d+:\d+\)/m,
279
+ // JavaScript stack trace
280
+ /^\s+at\s+.*\.[a-zA-Z]+:\d+/m,
281
+ // Python/Ruby stack trace
282
+ // Git diff new files
283
+ /^new file mode \d+$/m,
284
+ /^--- \/dev\/null$/m,
285
+ // Merge conflict markers
286
+ /^<<<<<<< /m,
287
+ /^=======/m,
288
+ /^>>>>>>> /m
289
+ ];
290
+ function detectBTSP(content) {
291
+ return BTSP_PATTERNS.some((pattern) => pattern.test(content));
292
+ }
293
+ function createBTSPEntry(content, tags = [], metadata = {}) {
294
+ return {
295
+ id: randomUUID(),
296
+ content,
297
+ hash: hashContent(content),
298
+ timestamp: Date.now(),
299
+ score: 1,
300
+ // Maximum initial score
301
+ ttl: 365 * 24 * 3600,
302
+ // 1 year in seconds (long retention)
303
+ state: "active",
304
+ // Always active
305
+ accessCount: 0,
306
+ tags: [...tags, "btsp"],
307
+ metadata,
308
+ isBTSP: true
309
+ };
310
+ }
311
+ return {
312
+ detectBTSP,
313
+ createBTSPEntry
314
+ };
315
+ }
316
+
317
+ // src/core/confidence-states.ts
318
+ function createConfidenceStates(config) {
319
+ const { activeThreshold, readyThreshold } = config;
320
+ function calculateState(entry) {
321
+ if (entry.isBTSP) {
322
+ return "active";
323
+ }
324
+ if (entry.score > activeThreshold) {
325
+ return "active";
326
+ }
327
+ if (entry.score >= readyThreshold) {
328
+ return "ready";
329
+ }
330
+ return "silent";
331
+ }
332
+ function transition(entry) {
333
+ const newState = calculateState(entry);
334
+ return {
335
+ ...entry,
336
+ state: newState
337
+ };
338
+ }
339
+ function getDistribution(entries) {
340
+ const distribution = {
341
+ silent: 0,
342
+ ready: 0,
343
+ active: 0,
344
+ total: entries.length
345
+ };
346
+ for (const entry of entries) {
347
+ const state = calculateState(entry);
348
+ distribution[state]++;
349
+ }
350
+ return distribution;
351
+ }
352
+ return {
353
+ calculateState,
354
+ transition,
355
+ getDistribution
356
+ };
357
+ }
358
+
359
+ // src/core/engram-scorer.ts
360
+ function createEngramScorer(config) {
361
+ const { defaultTTL } = config;
362
+ function calculateDecay(ageInSeconds, ttlInSeconds) {
363
+ if (ttlInSeconds === 0) return 1;
364
+ if (ageInSeconds <= 0) return 0;
365
+ const ratio = ageInSeconds / ttlInSeconds;
366
+ const decay = 1 - Math.exp(-ratio);
367
+ return Math.max(0, Math.min(1, decay));
368
+ }
369
+ function calculateScore(entry, currentTime = Date.now()) {
370
+ const ageInMilliseconds = currentTime - entry.timestamp;
371
+ const ageInSeconds = Math.max(0, ageInMilliseconds / 1e3);
372
+ const decay = calculateDecay(ageInSeconds, entry.ttl);
373
+ let score = entry.score * (1 - decay);
374
+ if (entry.accessCount > 0) {
375
+ const accessBonus = Math.log(entry.accessCount + 1) * 0.1;
376
+ score = Math.min(1, score + accessBonus);
377
+ }
378
+ if (entry.isBTSP) {
379
+ score = Math.max(score, 0.9);
380
+ }
381
+ return Math.max(0, Math.min(1, score));
382
+ }
383
+ function refreshTTL(entry) {
384
+ return {
385
+ ...entry,
386
+ ttl: defaultTTL * 3600,
387
+ // Convert hours to seconds
388
+ timestamp: Date.now()
389
+ };
390
+ }
391
+ return {
392
+ calculateScore,
393
+ refreshTTL,
394
+ calculateDecay
395
+ };
396
+ }
397
+
398
+ // src/utils/tokenizer.ts
399
+ function estimateTokens(text) {
400
+ if (!text || text.length === 0) {
401
+ return 0;
402
+ }
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);
409
+ }
410
+
411
+ // src/core/sparse-pruner.ts
412
+ function createSparsePruner(config) {
413
+ 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
+ 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;
441
+ }
442
+ function prune(entries) {
443
+ if (entries.length === 0) {
444
+ return {
445
+ kept: [],
446
+ removed: [],
447
+ originalTokens: 0,
448
+ prunedTokens: 0
449
+ };
450
+ }
451
+ const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
452
+ const scored = entries.map((entry) => ({
453
+ entry,
454
+ score: scoreEntry(entry, entries)
455
+ }));
456
+ scored.sort((a, b) => b.score - a.score);
457
+ const keepCount = Math.max(1, Math.ceil(entries.length * (threshold / 100)));
458
+ const kept = scored.slice(0, keepCount).map((s) => s.entry);
459
+ const removed = scored.slice(keepCount).map((s) => s.entry);
460
+ const prunedTokens = kept.reduce((sum, e) => sum + estimateTokens(e.content), 0);
461
+ return {
462
+ kept,
463
+ removed,
464
+ originalTokens,
465
+ prunedTokens
466
+ };
467
+ }
468
+ return {
469
+ prune,
470
+ scoreEntry
471
+ };
472
+ }
473
+
474
+ // src/adapters/generic.ts
475
+ function createGenericAdapter(memory, config) {
476
+ const pruner = createSparsePruner(config.pruning);
477
+ const scorer = createEngramScorer(config.decay);
478
+ const states = createConfidenceStates(config.states);
479
+ const btsp = createBTSPEmbedder();
480
+ async function optimize(context, options = {}) {
481
+ const startTime = Date.now();
482
+ 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
+ }));
498
+ const tokensBefore = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
499
+ const scoredEntries = entries.map((entry) => ({
500
+ ...entry,
501
+ score: scorer.calculateScore(entry)
502
+ }));
503
+ const statedEntries = scoredEntries.map((entry) => states.transition(entry));
504
+ const pruneResult = pruner.prune(statedEntries);
505
+ const optimizedEntries = pruneResult.kept.filter(
506
+ (e) => e.state === "active" || e.state === "ready"
507
+ );
508
+ const tokensAfter = optimizedEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
509
+ const optimizedContext = optimizedEntries.map((e) => e.content).join("\n");
510
+ if (!options.dryRun) {
511
+ for (const entry of optimizedEntries) {
512
+ await memory.put(entry);
513
+ }
514
+ await memory.recordOptimization({
515
+ timestamp: Date.now(),
516
+ tokens_before: tokensBefore,
517
+ tokens_after: tokensAfter,
518
+ entries_pruned: entries.length - optimizedEntries.length,
519
+ duration_ms: Date.now() - startTime
520
+ });
521
+ }
522
+ const distribution = states.getDistribution(optimizedEntries);
523
+ const result = {
524
+ optimizedContext,
525
+ tokensBefore,
526
+ tokensAfter,
527
+ reduction: tokensBefore > 0 ? (tokensBefore - tokensAfter) / tokensBefore : 0,
528
+ entriesProcessed: entries.length,
529
+ entriesKept: optimizedEntries.length,
530
+ stateDistribution: distribution,
531
+ durationMs: Date.now() - startTime
532
+ };
533
+ if (options.verbose) {
534
+ result.details = optimizedEntries.map((e) => ({
535
+ id: e.id,
536
+ score: e.score,
537
+ state: e.state,
538
+ isBTSP: e.isBTSP,
539
+ tokens: estimateTokens(e.content)
540
+ }));
541
+ }
542
+ return result;
543
+ }
544
+ return {
545
+ optimize
546
+ };
547
+ }
548
+
549
+ // src/core/sleep-compressor.ts
550
+ function createSleepCompressor() {
551
+ const scorer = createEngramScorer({ defaultTTL: 24, decayThreshold: 0.95 });
552
+ function consolidate(entries) {
553
+ const startTime = Date.now();
554
+ const originalCount = entries.length;
555
+ const now = Date.now();
556
+ const nonDecayed = entries.filter((entry) => {
557
+ const ageInSeconds = (now - entry.timestamp) / 1e3;
558
+ const decay = scorer.calculateDecay(ageInSeconds, entry.ttl);
559
+ return decay < 0.95;
560
+ });
561
+ const decayedRemoved = originalCount - nonDecayed.length;
562
+ const duplicateGroups = findDuplicates(nonDecayed);
563
+ const merged = mergeDuplicates(duplicateGroups);
564
+ const duplicateIds = new Set(duplicateGroups.flatMap((g) => g.entries.map((e) => e.id)));
565
+ const nonDuplicates = nonDecayed.filter((e) => !duplicateIds.has(e.id));
566
+ const kept = [...merged, ...nonDuplicates];
567
+ const removed = entries.filter((e) => !kept.some((k) => k.id === e.id));
568
+ const duplicatesRemoved = duplicateGroups.reduce((sum, g) => sum + (g.entries.length - 1), 0);
569
+ return {
570
+ kept,
571
+ removed,
572
+ entriesBefore: originalCount,
573
+ entriesAfter: kept.length,
574
+ decayedRemoved,
575
+ duplicatesRemoved,
576
+ compressionRatio: originalCount > 0 ? kept.length / originalCount : 0,
577
+ durationMs: Date.now() - startTime
578
+ };
579
+ }
580
+ function findDuplicates(entries) {
581
+ const groups = [];
582
+ const processed = /* @__PURE__ */ new Set();
583
+ for (let i = 0; i < entries.length; i++) {
584
+ const entry = entries[i];
585
+ if (!entry || processed.has(entry.id)) continue;
586
+ const duplicates = entries.filter((e, idx) => idx !== i && e.hash === entry.hash);
587
+ if (duplicates.length > 0) {
588
+ const group = {
589
+ entries: [entry, ...duplicates],
590
+ similarity: 1
591
+ // Exact match
592
+ };
593
+ groups.push(group);
594
+ processed.add(entry.id);
595
+ for (const dup of duplicates) {
596
+ processed.add(dup.id);
597
+ }
598
+ }
599
+ }
600
+ for (let i = 0; i < entries.length; i++) {
601
+ const entryI = entries[i];
602
+ if (!entryI || processed.has(entryI.id)) continue;
603
+ for (let j = i + 1; j < entries.length; j++) {
604
+ const entryJ = entries[j];
605
+ if (!entryJ || processed.has(entryJ.id)) continue;
606
+ const similarity = cosineSimilarity(entryI.content, entryJ.content);
607
+ if (similarity >= 0.85) {
608
+ const group = {
609
+ entries: [entryI, entryJ],
610
+ similarity
611
+ };
612
+ groups.push(group);
613
+ processed.add(entryI.id);
614
+ processed.add(entryJ.id);
615
+ break;
616
+ }
617
+ }
618
+ }
619
+ return groups;
620
+ }
621
+ function mergeDuplicates(groups) {
622
+ const merged = [];
623
+ for (const group of groups) {
624
+ const sorted = [...group.entries].sort((a, b) => b.score - a.score);
625
+ const best = sorted[0];
626
+ if (!best) continue;
627
+ const totalAccessCount = group.entries.reduce((sum, e) => sum + e.accessCount, 0);
628
+ const allTags = new Set(group.entries.flatMap((e) => e.tags));
629
+ merged.push({
630
+ ...best,
631
+ accessCount: totalAccessCount,
632
+ tags: Array.from(allTags)
633
+ });
634
+ }
635
+ return merged;
636
+ }
637
+ function cosineSimilarity(text1, text2) {
638
+ const words1 = tokenize(text1);
639
+ const words2 = tokenize(text2);
640
+ const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
641
+ const vec1 = {};
642
+ 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;
646
+ }
647
+ let dotProduct = 0;
648
+ let mag1 = 0;
649
+ let mag2 = 0;
650
+ for (const word of vocab) {
651
+ const count1 = vec1[word] ?? 0;
652
+ const count2 = vec2[word] ?? 0;
653
+ dotProduct += count1 * count2;
654
+ mag1 += count1 * count1;
655
+ mag2 += count2 * count2;
656
+ }
657
+ mag1 = Math.sqrt(mag1);
658
+ mag2 = Math.sqrt(mag2);
659
+ if (mag1 === 0 || mag2 === 0) return 0;
660
+ return dotProduct / (mag1 * mag2);
661
+ }
662
+ function tokenize(text) {
663
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
664
+ }
665
+ return {
666
+ consolidate,
667
+ findDuplicates,
668
+ mergeDuplicates
669
+ };
670
+ }
671
+
672
+ // src/types/config.ts
673
+ var DEFAULT_CONFIG = {
674
+ pruning: {
675
+ threshold: 5,
676
+ aggressiveness: 50
677
+ },
678
+ decay: {
679
+ defaultTTL: 24,
680
+ decayThreshold: 0.95
681
+ },
682
+ states: {
683
+ activeThreshold: 0.7,
684
+ readyThreshold: 0.3
685
+ },
686
+ agent: "generic",
687
+ ui: {
688
+ colors: true,
689
+ sounds: false,
690
+ verbose: false
691
+ },
692
+ autoConsolidate: null,
693
+ realtime: {
694
+ tokenBudget: 5e4,
695
+ autoOptimizeThreshold: 8e4,
696
+ watchPatterns: ["**/*.jsonl"],
697
+ pidFile: ".sparn/daemon.pid",
698
+ logFile: ".sparn/daemon.log",
699
+ debounceMs: 5e3,
700
+ incremental: true,
701
+ windowSize: 500,
702
+ consolidationInterval: null
703
+ }
704
+ };
705
+
706
+ // src/mcp/server.ts
707
+ function createSparnMcpServer(options) {
708
+ const { memory, config = DEFAULT_CONFIG } = options;
709
+ const server = new McpServer({
710
+ name: "sparn",
711
+ version: "1.1.1"
712
+ });
713
+ registerOptimizeTool(server, memory, config);
714
+ registerStatsTool(server, memory);
715
+ registerConsolidateTool(server, memory);
716
+ return server;
717
+ }
718
+ function registerOptimizeTool(server, memory, config) {
719
+ server.registerTool(
720
+ "sparn_optimize",
721
+ {
722
+ 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.",
724
+ inputSchema: {
725
+ context: z.string().describe("The context text to optimize"),
726
+ dryRun: z.boolean().optional().default(false).describe("If true, do not persist changes to the memory store"),
727
+ verbose: z.boolean().optional().default(false).describe("If true, include per-entry details in the response"),
728
+ threshold: z.number().min(0).max(100).optional().describe("Custom pruning threshold (1-100, overrides config)")
729
+ }
730
+ },
731
+ async ({ context, dryRun, verbose, threshold }) => {
732
+ try {
733
+ const effectiveConfig = threshold ? { ...config, pruning: { ...config.pruning, threshold } } : config;
734
+ const adapter = createGenericAdapter(memory, effectiveConfig);
735
+ const result = await adapter.optimize(context, {
736
+ dryRun,
737
+ verbose,
738
+ threshold
739
+ });
740
+ const response = {
741
+ optimizedContext: result.optimizedContext,
742
+ tokensBefore: result.tokensBefore,
743
+ tokensAfter: result.tokensAfter,
744
+ reduction: `${(result.reduction * 100).toFixed(1)}%`,
745
+ entriesProcessed: result.entriesProcessed,
746
+ entriesKept: result.entriesKept,
747
+ durationMs: result.durationMs,
748
+ stateDistribution: result.stateDistribution,
749
+ ...verbose && result.details ? { details: result.details } : {}
750
+ };
751
+ return {
752
+ content: [
753
+ {
754
+ type: "text",
755
+ text: JSON.stringify(response, null, 2)
756
+ }
757
+ ]
758
+ };
759
+ } catch (error) {
760
+ const message = error instanceof Error ? error.message : String(error);
761
+ return {
762
+ content: [
763
+ {
764
+ type: "text",
765
+ text: JSON.stringify({ error: message })
766
+ }
767
+ ],
768
+ isError: true
769
+ };
770
+ }
771
+ }
772
+ );
773
+ }
774
+ function registerStatsTool(server, memory) {
775
+ server.registerTool(
776
+ "sparn_stats",
777
+ {
778
+ title: "Sparn Stats",
779
+ description: "Get optimization statistics including total commands run, tokens saved, and average reduction percentage.",
780
+ inputSchema: {
781
+ reset: z.boolean().optional().default(false).describe("If true, reset all optimization statistics")
782
+ }
783
+ },
784
+ async ({ reset }) => {
785
+ try {
786
+ if (reset) {
787
+ await memory.clearOptimizationStats();
788
+ return {
789
+ content: [
790
+ {
791
+ type: "text",
792
+ text: JSON.stringify(
793
+ {
794
+ message: "Optimization statistics have been reset.",
795
+ totalCommands: 0,
796
+ totalTokensSaved: 0,
797
+ averageReduction: "0.0%"
798
+ },
799
+ null,
800
+ 2
801
+ )
802
+ }
803
+ ]
804
+ };
805
+ }
806
+ const stats = await memory.getOptimizationStats();
807
+ const totalCommands = stats.length;
808
+ const totalTokensSaved = stats.reduce(
809
+ (sum, s) => sum + (s.tokens_before - s.tokens_after),
810
+ 0
811
+ );
812
+ const averageReduction = totalCommands > 0 ? stats.reduce((sum, s) => {
813
+ const reduction = s.tokens_before > 0 ? (s.tokens_before - s.tokens_after) / s.tokens_before : 0;
814
+ return sum + reduction;
815
+ }, 0) / totalCommands : 0;
816
+ const recentOptimizations = stats.slice(0, 10).map((s) => ({
817
+ timestamp: new Date(s.timestamp).toISOString(),
818
+ tokensBefore: s.tokens_before,
819
+ tokensAfter: s.tokens_after,
820
+ entriesPruned: s.entries_pruned,
821
+ durationMs: s.duration_ms,
822
+ reduction: `${((s.tokens_before - s.tokens_after) / Math.max(s.tokens_before, 1) * 100).toFixed(1)}%`
823
+ }));
824
+ const response = {
825
+ totalCommands,
826
+ totalTokensSaved,
827
+ averageReduction: `${(averageReduction * 100).toFixed(1)}%`,
828
+ recentOptimizations
829
+ };
830
+ return {
831
+ content: [
832
+ {
833
+ type: "text",
834
+ text: JSON.stringify(response, null, 2)
835
+ }
836
+ ]
837
+ };
838
+ } catch (error) {
839
+ const message = error instanceof Error ? error.message : String(error);
840
+ return {
841
+ content: [
842
+ {
843
+ type: "text",
844
+ text: JSON.stringify({ error: message })
845
+ }
846
+ ],
847
+ isError: true
848
+ };
849
+ }
850
+ }
851
+ );
852
+ }
853
+ function registerConsolidateTool(server, memory) {
854
+ server.registerTool(
855
+ "sparn_consolidate",
856
+ {
857
+ 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."
859
+ },
860
+ async () => {
861
+ try {
862
+ const allIds = await memory.list();
863
+ const allEntries = await Promise.all(
864
+ allIds.map(async (id) => {
865
+ const entry = await memory.get(id);
866
+ return entry;
867
+ })
868
+ );
869
+ const entries = allEntries.filter((e) => e !== null);
870
+ const compressor = createSleepCompressor();
871
+ const result = compressor.consolidate(entries);
872
+ for (const removed of result.removed) {
873
+ await memory.delete(removed.id);
874
+ }
875
+ for (const kept of result.kept) {
876
+ await memory.put(kept);
877
+ }
878
+ await memory.compact();
879
+ const response = {
880
+ entriesBefore: result.entriesBefore,
881
+ entriesAfter: result.entriesAfter,
882
+ decayedRemoved: result.decayedRemoved,
883
+ duplicatesRemoved: result.duplicatesRemoved,
884
+ compressionRatio: `${(result.compressionRatio * 100).toFixed(1)}%`,
885
+ durationMs: result.durationMs,
886
+ vacuumCompleted: true
887
+ };
888
+ return {
889
+ content: [
890
+ {
891
+ type: "text",
892
+ text: JSON.stringify(response, null, 2)
893
+ }
894
+ ]
895
+ };
896
+ } catch (error) {
897
+ const message = error instanceof Error ? error.message : String(error);
898
+ return {
899
+ content: [
900
+ {
901
+ type: "text",
902
+ text: JSON.stringify({ error: message })
903
+ }
904
+ ],
905
+ isError: true
906
+ };
907
+ }
908
+ }
909
+ );
910
+ }
911
+
912
+ // src/mcp/index.ts
913
+ async function main() {
914
+ const dbPath = resolve(process.env["SPARN_DB_PATH"] ?? ".sparn/memory.db");
915
+ mkdirSync(dirname(dbPath), { recursive: true });
916
+ const memory = await createKVMemory(dbPath);
917
+ const server = createSparnMcpServer({ memory });
918
+ const transport = new StdioServerTransport();
919
+ await server.connect(transport);
920
+ console.error("Sparn MCP server running on stdio");
921
+ const shutdown = async () => {
922
+ console.error("Shutting down Sparn MCP server...");
923
+ await server.close();
924
+ await memory.close();
925
+ process.exit(0);
926
+ };
927
+ process.on("SIGINT", shutdown);
928
+ process.on("SIGTERM", shutdown);
929
+ }
930
+ main().catch((error) => {
931
+ console.error("Fatal error in Sparn MCP server:", error);
932
+ process.exit(1);
933
+ });
934
+ //# sourceMappingURL=index.js.map