@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.
- package/PRIVACY.md +1 -1
- package/README.md +136 -642
- package/SECURITY.md +1 -1
- package/dist/cli/dashboard.cjs +3977 -0
- package/dist/cli/dashboard.cjs.map +1 -0
- package/dist/cli/dashboard.d.cts +17 -0
- package/dist/cli/dashboard.d.ts +17 -0
- package/dist/cli/dashboard.js +3932 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/index.cjs +3853 -484
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +3810 -457
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/index.cjs +411 -99
- package/dist/daemon/index.cjs.map +1 -1
- package/dist/daemon/index.js +423 -103
- package/dist/daemon/index.js.map +1 -1
- package/dist/hooks/post-tool-result.cjs +115 -266
- package/dist/hooks/post-tool-result.cjs.map +1 -1
- package/dist/hooks/post-tool-result.js +115 -266
- package/dist/hooks/post-tool-result.js.map +1 -1
- package/dist/hooks/pre-prompt.cjs +197 -268
- package/dist/hooks/pre-prompt.cjs.map +1 -1
- package/dist/hooks/pre-prompt.js +182 -268
- package/dist/hooks/pre-prompt.js.map +1 -1
- package/dist/hooks/stop-docs-refresh.cjs +123 -0
- package/dist/hooks/stop-docs-refresh.cjs.map +1 -0
- package/dist/hooks/stop-docs-refresh.d.cts +1 -0
- package/dist/hooks/stop-docs-refresh.d.ts +1 -0
- package/dist/hooks/stop-docs-refresh.js +126 -0
- package/dist/hooks/stop-docs-refresh.js.map +1 -0
- package/dist/index.cjs +1754 -337
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +539 -40
- package/dist/index.d.ts +539 -40
- package/dist/index.js +1737 -329
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.cjs +304 -71
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +308 -71
- package/dist/mcp/index.js.map +1 -1
- 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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
484
|
-
|
|
485
|
-
content
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
|
644
|
-
vec1[word] =
|
|
645
|
-
|
|
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.
|
|
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
|
|
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
|
|
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);
|