@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/index.js CHANGED
@@ -8,7 +8,7 @@ function hashContent(content) {
8
8
  }
9
9
 
10
10
  // src/core/btsp-embedder.ts
11
- function createBTSPEmbedder() {
11
+ function createBTSPEmbedder(config) {
12
12
  const BTSP_PATTERNS = [
13
13
  // Error patterns
14
14
  /\b(error|exception|failure|fatal|critical|panic)\b/i,
@@ -27,6 +27,14 @@ function createBTSPEmbedder() {
27
27
  /^=======/m,
28
28
  /^>>>>>>> /m
29
29
  ];
30
+ if (config?.customPatterns) {
31
+ for (const pattern of config.customPatterns) {
32
+ try {
33
+ BTSP_PATTERNS.push(new RegExp(pattern));
34
+ } catch {
35
+ }
36
+ }
37
+ }
30
38
  function detectBTSP(content) {
31
39
  return BTSP_PATTERNS.some((pattern) => pattern.test(content));
32
40
  }
@@ -54,51 +62,96 @@ function createBTSPEmbedder() {
54
62
  };
55
63
  }
56
64
 
57
- // src/core/confidence-states.ts
58
- function createConfidenceStates(config) {
59
- const { activeThreshold, readyThreshold } = config;
60
- function calculateState(entry) {
61
- if (entry.isBTSP) {
62
- return "active";
63
- }
64
- if (entry.score > activeThreshold) {
65
- return "active";
66
- }
67
- if (entry.score >= readyThreshold) {
68
- return "ready";
69
- }
70
- return "silent";
71
- }
72
- function transition(entry) {
73
- const newState = calculateState(entry);
74
- return {
75
- ...entry,
76
- state: newState
77
- };
78
- }
79
- function getDistribution(entries) {
80
- const distribution = {
81
- silent: 0,
82
- ready: 0,
83
- active: 0,
84
- total: entries.length
85
- };
86
- for (const entry of entries) {
87
- const state = calculateState(entry);
88
- distribution[state]++;
65
+ // src/utils/tfidf.ts
66
+ function tokenize(text) {
67
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
68
+ }
69
+ function calculateTF(term, tokens) {
70
+ const count = tokens.filter((t) => t === term).length;
71
+ return Math.sqrt(count);
72
+ }
73
+ function calculateIDF(term, allEntries) {
74
+ const totalDocs = allEntries.length;
75
+ const docsWithTerm = allEntries.filter((entry) => {
76
+ const tokens = tokenize(entry.content);
77
+ return tokens.includes(term);
78
+ }).length;
79
+ if (docsWithTerm === 0) return 0;
80
+ return Math.log(totalDocs / docsWithTerm);
81
+ }
82
+ function calculateTFIDF(entry, allEntries) {
83
+ const tokens = tokenize(entry.content);
84
+ if (tokens.length === 0) return 0;
85
+ const uniqueTerms = [...new Set(tokens)];
86
+ let totalScore = 0;
87
+ for (const term of uniqueTerms) {
88
+ const tf = calculateTF(term, tokens);
89
+ const idf = calculateIDF(term, allEntries);
90
+ totalScore += tf * idf;
91
+ }
92
+ return totalScore / tokens.length;
93
+ }
94
+ function createTFIDFIndex(entries) {
95
+ const documentFrequency = /* @__PURE__ */ new Map();
96
+ for (const entry of entries) {
97
+ const tokens = tokenize(entry.content);
98
+ const uniqueTerms = new Set(tokens);
99
+ for (const term of uniqueTerms) {
100
+ documentFrequency.set(term, (documentFrequency.get(term) || 0) + 1);
89
101
  }
90
- return distribution;
91
102
  }
92
103
  return {
93
- calculateState,
94
- transition,
95
- getDistribution
104
+ documentFrequency,
105
+ totalDocuments: entries.length
96
106
  };
97
107
  }
108
+ function scoreTFIDF(entry, index) {
109
+ const tokens = tokenize(entry.content);
110
+ if (tokens.length === 0) return 0;
111
+ const uniqueTerms = new Set(tokens);
112
+ let totalScore = 0;
113
+ for (const term of uniqueTerms) {
114
+ const tf = calculateTF(term, tokens);
115
+ const docsWithTerm = index.documentFrequency.get(term) || 0;
116
+ if (docsWithTerm === 0) continue;
117
+ const idf = Math.log(index.totalDocuments / docsWithTerm);
118
+ totalScore += tf * idf;
119
+ }
120
+ return totalScore / tokens.length;
121
+ }
122
+
123
+ // src/utils/tokenizer.ts
124
+ import { encode } from "gpt-tokenizer";
125
+ var usePrecise = false;
126
+ function setPreciseTokenCounting(enabled) {
127
+ usePrecise = enabled;
128
+ }
129
+ function countTokensPrecise(text) {
130
+ if (!text || text.length === 0) {
131
+ return 0;
132
+ }
133
+ return encode(text).length;
134
+ }
135
+ function estimateTokens(text) {
136
+ if (!text || text.length === 0) {
137
+ return 0;
138
+ }
139
+ if (usePrecise) {
140
+ return encode(text).length;
141
+ }
142
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
143
+ const wordCount = words.length;
144
+ const charCount = text.length;
145
+ const charEstimate = Math.ceil(charCount / 4);
146
+ const wordEstimate = Math.ceil(wordCount * 0.75);
147
+ return Math.max(wordEstimate, charEstimate);
148
+ }
98
149
 
99
150
  // src/core/engram-scorer.ts
100
151
  function createEngramScorer(config) {
101
152
  const { defaultTTL } = config;
153
+ const recencyWindowMs = (config.recencyBoostMinutes ?? 30) * 60 * 1e3;
154
+ const recencyMultiplier = config.recencyBoostMultiplier ?? 1.3;
102
155
  function calculateDecay(ageInSeconds, ttlInSeconds) {
103
156
  if (ttlInSeconds === 0) return 1;
104
157
  if (ageInSeconds <= 0) return 0;
@@ -118,6 +171,13 @@ function createEngramScorer(config) {
118
171
  if (entry.isBTSP) {
119
172
  score = Math.max(score, 0.9);
120
173
  }
174
+ if (!entry.isBTSP && recencyWindowMs > 0) {
175
+ const ageMs = currentTime - entry.timestamp;
176
+ if (ageMs >= 0 && ageMs < recencyWindowMs) {
177
+ const boostFactor = 1 + (recencyMultiplier - 1) * (1 - ageMs / recencyWindowMs);
178
+ score = score * boostFactor;
179
+ }
180
+ }
121
181
  return Math.max(0, Math.min(1, score));
122
182
  }
123
183
  function refreshTTL(entry) {
@@ -135,85 +195,151 @@ function createEngramScorer(config) {
135
195
  };
136
196
  }
137
197
 
138
- // src/utils/tokenizer.ts
139
- function estimateTokens(text) {
140
- if (!text || text.length === 0) {
141
- return 0;
142
- }
143
- const words = text.split(/\s+/).filter((w) => w.length > 0);
144
- const wordCount = words.length;
145
- const charCount = text.length;
146
- const charEstimate = Math.ceil(charCount / 4);
147
- const wordEstimate = Math.ceil(wordCount * 0.75);
148
- return Math.max(wordEstimate, charEstimate);
149
- }
150
-
151
- // src/core/sparse-pruner.ts
152
- function createSparsePruner(config) {
153
- const { threshold } = config;
154
- function tokenize(text) {
155
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
156
- }
157
- function calculateTF(term, tokens) {
158
- const count = tokens.filter((t) => t === term).length;
159
- return Math.sqrt(count);
160
- }
161
- function calculateIDF(term, allEntries) {
162
- const totalDocs = allEntries.length;
163
- const docsWithTerm = allEntries.filter((entry) => {
164
- const tokens = tokenize(entry.content);
165
- return tokens.includes(term);
166
- }).length;
167
- if (docsWithTerm === 0) return 0;
168
- return Math.log(totalDocs / docsWithTerm);
169
- }
170
- function scoreEntry(entry, allEntries) {
171
- const tokens = tokenize(entry.content);
172
- if (tokens.length === 0) return 0;
173
- const uniqueTerms = [...new Set(tokens)];
174
- let totalScore = 0;
175
- for (const term of uniqueTerms) {
176
- const tf = calculateTF(term, tokens);
177
- const idf = calculateIDF(term, allEntries);
178
- totalScore += tf * idf;
198
+ // src/core/budget-pruner.ts
199
+ function createBudgetPruner(config) {
200
+ const { tokenBudget, decay } = config;
201
+ const engramScorer = createEngramScorer(decay);
202
+ function getStateMultiplier(entry) {
203
+ if (entry.isBTSP) return 2;
204
+ switch (entry.state) {
205
+ case "active":
206
+ return 2;
207
+ case "ready":
208
+ return 1;
209
+ case "silent":
210
+ return 0.5;
211
+ default:
212
+ return 1;
179
213
  }
180
- return totalScore / tokens.length;
181
214
  }
182
- function prune(entries) {
215
+ function priorityScore(entry, allEntries, index) {
216
+ const tfidf = index ? scoreTFIDF(entry, index) : scoreTFIDF(entry, createTFIDFIndex(allEntries));
217
+ const currentScore = engramScorer.calculateScore(entry);
218
+ const engramDecay = 1 - currentScore;
219
+ const stateMultiplier = getStateMultiplier(entry);
220
+ return tfidf * (1 - engramDecay) * stateMultiplier;
221
+ }
222
+ function pruneToFit(entries, budget = tokenBudget) {
183
223
  if (entries.length === 0) {
184
224
  return {
185
225
  kept: [],
186
226
  removed: [],
187
227
  originalTokens: 0,
188
- prunedTokens: 0
228
+ prunedTokens: 0,
229
+ budgetUtilization: 0
189
230
  };
190
231
  }
191
232
  const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
192
- const scored = entries.map((entry) => ({
233
+ const btspEntries = entries.filter((e) => e.isBTSP);
234
+ const regularEntries = entries.filter((e) => !e.isBTSP);
235
+ let includedBtsp = [];
236
+ let btspTokens = 0;
237
+ const sortedBtsp = [...btspEntries].sort((a, b) => b.timestamp - a.timestamp);
238
+ for (const entry of sortedBtsp) {
239
+ const tokens = estimateTokens(entry.content);
240
+ if (btspTokens + tokens <= budget * 0.8) {
241
+ includedBtsp.push(entry);
242
+ btspTokens += tokens;
243
+ }
244
+ }
245
+ if (includedBtsp.length === 0 && sortedBtsp.length > 0) {
246
+ const firstBtsp = sortedBtsp[0];
247
+ if (firstBtsp) {
248
+ includedBtsp = [firstBtsp];
249
+ btspTokens = estimateTokens(firstBtsp.content);
250
+ }
251
+ }
252
+ const excludedBtsp = btspEntries.filter((e) => !includedBtsp.includes(e));
253
+ const tfidfIndex = createTFIDFIndex(entries);
254
+ const scored = regularEntries.map((entry) => ({
193
255
  entry,
194
- score: scoreEntry(entry, entries)
256
+ score: priorityScore(entry, entries, tfidfIndex),
257
+ tokens: estimateTokens(entry.content)
195
258
  }));
196
259
  scored.sort((a, b) => b.score - a.score);
197
- const keepCount = Math.max(1, Math.ceil(entries.length * (threshold / 100)));
198
- const kept = scored.slice(0, keepCount).map((s) => s.entry);
199
- const removed = scored.slice(keepCount).map((s) => s.entry);
200
- const prunedTokens = kept.reduce((sum, e) => sum + estimateTokens(e.content), 0);
260
+ const kept = [...includedBtsp];
261
+ const removed = [...excludedBtsp];
262
+ let currentTokens = btspTokens;
263
+ for (const item of scored) {
264
+ if (currentTokens + item.tokens <= budget) {
265
+ kept.push(item.entry);
266
+ currentTokens += item.tokens;
267
+ } else {
268
+ removed.push(item.entry);
269
+ }
270
+ }
271
+ const budgetUtilization = budget > 0 ? currentTokens / budget : 0;
201
272
  return {
202
273
  kept,
203
274
  removed,
204
275
  originalTokens,
205
- prunedTokens
276
+ prunedTokens: currentTokens,
277
+ budgetUtilization
206
278
  };
207
279
  }
208
280
  return {
209
- prune,
210
- scoreEntry
281
+ pruneToFit,
282
+ priorityScore
283
+ };
284
+ }
285
+ function createBudgetPrunerFromConfig(realtimeConfig, decayConfig, statesConfig) {
286
+ return createBudgetPruner({
287
+ tokenBudget: realtimeConfig.tokenBudget,
288
+ decay: decayConfig,
289
+ states: statesConfig
290
+ });
291
+ }
292
+
293
+ // src/core/confidence-states.ts
294
+ function createConfidenceStates(config) {
295
+ const { activeThreshold, readyThreshold } = config;
296
+ function calculateState(entry) {
297
+ if (entry.isBTSP) {
298
+ return "active";
299
+ }
300
+ if (entry.score >= activeThreshold) {
301
+ return "active";
302
+ }
303
+ if (entry.score >= readyThreshold) {
304
+ return "ready";
305
+ }
306
+ return "silent";
307
+ }
308
+ function transition(entry) {
309
+ const newState = calculateState(entry);
310
+ return {
311
+ ...entry,
312
+ state: newState
313
+ };
314
+ }
315
+ function getDistribution(entries) {
316
+ const distribution = {
317
+ silent: 0,
318
+ ready: 0,
319
+ active: 0,
320
+ total: entries.length
321
+ };
322
+ for (const entry of entries) {
323
+ const state = calculateState(entry);
324
+ distribution[state]++;
325
+ }
326
+ return distribution;
327
+ }
328
+ return {
329
+ calculateState,
330
+ transition,
331
+ getDistribution
211
332
  };
212
333
  }
213
334
 
214
335
  // src/utils/context-parser.ts
215
336
  import { randomUUID as randomUUID2 } from "crypto";
216
337
  function parseClaudeCodeContext(context) {
338
+ const firstNonEmpty = context.split("\n").find((line) => line.trim().length > 0);
339
+ if (firstNonEmpty?.trim().startsWith("{")) {
340
+ const jsonlEntries = parseJSONLContext(context);
341
+ if (jsonlEntries.length > 0) return jsonlEntries;
342
+ }
217
343
  const entries = [];
218
344
  const now = Date.now();
219
345
  const lines = context.split("\n");
@@ -266,7 +392,7 @@ function createEntry(content, type, baseTime) {
266
392
  hash: hashContent(content),
267
393
  timestamp: baseTime,
268
394
  score: initialScore,
269
- state: initialScore > 0.7 ? "active" : initialScore > 0.3 ? "ready" : "silent",
395
+ state: initialScore >= 0.7 ? "active" : initialScore >= 0.3 ? "ready" : "silent",
270
396
  ttl: 24 * 3600,
271
397
  // 24 hours default
272
398
  accessCount: 0,
@@ -275,6 +401,58 @@ function createEntry(content, type, baseTime) {
275
401
  isBTSP: false
276
402
  };
277
403
  }
404
+ function parseJSONLLine(line) {
405
+ const trimmed = line.trim();
406
+ if (trimmed.length === 0) return null;
407
+ try {
408
+ return JSON.parse(trimmed);
409
+ } catch {
410
+ return null;
411
+ }
412
+ }
413
+ function extractContent(content) {
414
+ if (typeof content === "string") return content;
415
+ if (Array.isArray(content)) {
416
+ return content.map((block) => {
417
+ if (block.type === "text" && block.text) return block.text;
418
+ if (block.type === "tool_use" && block.name) return `[tool_use: ${block.name}]`;
419
+ if (block.type === "tool_result") {
420
+ if (typeof block.content === "string") return block.content;
421
+ if (Array.isArray(block.content)) {
422
+ return block.content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n");
423
+ }
424
+ }
425
+ return "";
426
+ }).filter((s) => s.length > 0).join("\n");
427
+ }
428
+ return "";
429
+ }
430
+ function classifyJSONLMessage(msg) {
431
+ if (Array.isArray(msg.content)) {
432
+ const hasToolUse = msg.content.some((b) => b.type === "tool_use");
433
+ const hasToolResult = msg.content.some((b) => b.type === "tool_result");
434
+ if (hasToolUse) return "tool";
435
+ if (hasToolResult) return "result";
436
+ }
437
+ if (msg.type === "tool_use" || msg.tool_use) return "tool";
438
+ if (msg.type === "tool_result" || msg.tool_result) return "result";
439
+ if (msg.role === "user" || msg.role === "assistant") return "conversation";
440
+ return "other";
441
+ }
442
+ function parseJSONLContext(context) {
443
+ const entries = [];
444
+ const now = Date.now();
445
+ const lines = context.split("\n");
446
+ for (const line of lines) {
447
+ const msg = parseJSONLLine(line);
448
+ if (!msg) continue;
449
+ const content = extractContent(msg.content);
450
+ if (!content || content.trim().length === 0) continue;
451
+ const blockType = classifyJSONLMessage(msg);
452
+ entries.push(createEntry(content, blockType, now));
453
+ }
454
+ return entries;
455
+ }
278
456
  function parseGenericContext(context) {
279
457
  const entries = [];
280
458
  const now = Date.now();
@@ -289,15 +467,9 @@ function parseGenericContext(context) {
289
467
 
290
468
  // src/adapters/claude-code.ts
291
469
  var CLAUDE_CODE_PROFILE = {
292
- // More aggressive pruning for tool results (they can be verbose)
293
- toolResultThreshold: 3,
294
- // Keep top 3% of tool results
295
470
  // Preserve conversation turns more aggressively
296
471
  conversationBoost: 1.5,
297
472
  // 50% boost for User/Assistant exchanges
298
- // Prioritize recent context (Claude Code sessions are typically focused)
299
- recentContextWindow: 10 * 60,
300
- // Last 10 minutes gets priority
301
473
  // BTSP patterns specific to Claude Code
302
474
  btspPatterns: [
303
475
  // Error patterns
@@ -318,12 +490,14 @@ var CLAUDE_CODE_PROFILE = {
318
490
  ]
319
491
  };
320
492
  function createClaudeCodeAdapter(memory, config) {
321
- const pruner = createSparsePruner({
322
- threshold: config.pruning.threshold
493
+ const pruner = createBudgetPruner({
494
+ tokenBudget: config.realtime.tokenBudget,
495
+ decay: config.decay,
496
+ states: config.states
323
497
  });
324
498
  const scorer = createEngramScorer(config.decay);
325
499
  const states = createConfidenceStates(config.states);
326
- const btsp = createBTSPEmbedder();
500
+ const btsp = createBTSPEmbedder({ customPatterns: config.btspPatterns });
327
501
  async function optimize(context, options = {}) {
328
502
  const startTime = Date.now();
329
503
  const entries = parseClaudeCodeContext(context);
@@ -354,9 +528,11 @@ function createClaudeCodeAdapter(memory, config) {
354
528
  });
355
529
  const scoredEntries = boostedEntries.map((entry) => {
356
530
  const decayScore = scorer.calculateScore(entry);
531
+ const isConversationTurn = entry.content.trim().startsWith("User:") || entry.content.trim().startsWith("Assistant:");
532
+ const finalScore = isConversationTurn ? Math.min(1, decayScore * CLAUDE_CODE_PROFILE.conversationBoost) : decayScore;
357
533
  return {
358
534
  ...entry,
359
- score: decayScore
535
+ score: finalScore
360
536
  };
361
537
  });
362
538
  const entriesWithStates = scoredEntries.map((entry) => {
@@ -366,7 +542,7 @@ function createClaudeCodeAdapter(memory, config) {
366
542
  state
367
543
  };
368
544
  });
369
- const pruneResult = pruner.prune(entriesWithStates);
545
+ const pruneResult = pruner.pruneToFit(entriesWithStates);
370
546
  if (!options.dryRun) {
371
547
  for (const entry of pruneResult.kept) {
372
548
  await memory.put(entry);
@@ -409,29 +585,73 @@ function createClaudeCodeAdapter(memory, config) {
409
585
 
410
586
  // src/adapters/generic.ts
411
587
  import { randomUUID as randomUUID3 } from "crypto";
588
+
589
+ // src/core/sparse-pruner.ts
590
+ function createSparsePruner(config) {
591
+ const { threshold } = config;
592
+ function scoreEntry(entry, allEntries) {
593
+ return scoreTFIDF(entry, createTFIDFIndex(allEntries));
594
+ }
595
+ function prune(entries) {
596
+ if (entries.length === 0) {
597
+ return {
598
+ kept: [],
599
+ removed: [],
600
+ originalTokens: 0,
601
+ prunedTokens: 0
602
+ };
603
+ }
604
+ const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
605
+ const tfidfIndex = createTFIDFIndex(entries);
606
+ const scored = entries.map((entry) => ({
607
+ entry,
608
+ score: scoreTFIDF(entry, tfidfIndex)
609
+ }));
610
+ scored.sort((a, b) => b.score - a.score);
611
+ const keepCount = Math.max(1, Math.ceil(entries.length * (threshold / 100)));
612
+ const kept = scored.slice(0, keepCount).map((s) => s.entry);
613
+ const removed = scored.slice(keepCount).map((s) => s.entry);
614
+ const prunedTokens = kept.reduce((sum, e) => sum + estimateTokens(e.content), 0);
615
+ return {
616
+ kept,
617
+ removed,
618
+ originalTokens,
619
+ prunedTokens
620
+ };
621
+ }
622
+ return {
623
+ prune,
624
+ scoreEntry
625
+ };
626
+ }
627
+
628
+ // src/adapters/generic.ts
412
629
  function createGenericAdapter(memory, config) {
413
630
  const pruner = createSparsePruner(config.pruning);
414
631
  const scorer = createEngramScorer(config.decay);
415
632
  const states = createConfidenceStates(config.states);
416
- const btsp = createBTSPEmbedder();
633
+ const btsp = createBTSPEmbedder({ customPatterns: config.btspPatterns });
417
634
  async function optimize(context, options = {}) {
418
635
  const startTime = Date.now();
419
636
  const lines = context.split("\n").filter((line) => line.trim().length > 0);
420
- const entries = lines.map((content) => ({
421
- id: randomUUID3(),
422
- content,
423
- hash: hashContent(content),
424
- timestamp: Date.now(),
425
- score: btsp.detectBTSP(content) ? 1 : 0.5,
426
- // BTSP gets high initial score
427
- ttl: config.decay.defaultTTL * 3600,
428
- // Convert hours to seconds
429
- state: "ready",
430
- accessCount: 0,
431
- tags: [],
432
- metadata: {},
433
- isBTSP: btsp.detectBTSP(content)
434
- }));
637
+ const now = Date.now();
638
+ const entries = lines.map((content, index) => {
639
+ const isBTSP = btsp.detectBTSP(content);
640
+ return {
641
+ id: randomUUID3(),
642
+ content,
643
+ hash: hashContent(content),
644
+ timestamp: now + index,
645
+ // Unique timestamps preserve ordering
646
+ score: isBTSP ? 1 : 0.5,
647
+ ttl: config.decay.defaultTTL * 3600,
648
+ state: "ready",
649
+ accessCount: 0,
650
+ tags: [],
651
+ metadata: {},
652
+ isBTSP
653
+ };
654
+ });
435
655
  const tokensBefore = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
436
656
  const scoredEntries = entries.map((entry) => ({
437
657
  ...entry,
@@ -483,111 +703,6 @@ function createGenericAdapter(memory, config) {
483
703
  };
484
704
  }
485
705
 
486
- // src/core/budget-pruner.ts
487
- function createBudgetPruner(config) {
488
- const { tokenBudget, decay } = config;
489
- const engramScorer = createEngramScorer(decay);
490
- function tokenize(text) {
491
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
492
- }
493
- function calculateTF(term, tokens) {
494
- const count = tokens.filter((t) => t === term).length;
495
- return Math.sqrt(count);
496
- }
497
- function calculateIDF(term, allEntries) {
498
- const totalDocs = allEntries.length;
499
- const docsWithTerm = allEntries.filter((entry) => {
500
- const tokens = tokenize(entry.content);
501
- return tokens.includes(term);
502
- }).length;
503
- if (docsWithTerm === 0) return 0;
504
- return Math.log(totalDocs / docsWithTerm);
505
- }
506
- function calculateTFIDF(entry, allEntries) {
507
- const tokens = tokenize(entry.content);
508
- if (tokens.length === 0) return 0;
509
- const uniqueTerms = [...new Set(tokens)];
510
- let totalScore = 0;
511
- for (const term of uniqueTerms) {
512
- const tf = calculateTF(term, tokens);
513
- const idf = calculateIDF(term, allEntries);
514
- totalScore += tf * idf;
515
- }
516
- return totalScore / tokens.length;
517
- }
518
- function getStateMultiplier(entry) {
519
- if (entry.isBTSP) return 2;
520
- switch (entry.state) {
521
- case "active":
522
- return 2;
523
- case "ready":
524
- return 1;
525
- case "silent":
526
- return 0.5;
527
- default:
528
- return 1;
529
- }
530
- }
531
- function priorityScore(entry, allEntries) {
532
- const tfidf = calculateTFIDF(entry, allEntries);
533
- const currentScore = engramScorer.calculateScore(entry);
534
- const engramDecay = 1 - currentScore;
535
- const stateMultiplier = getStateMultiplier(entry);
536
- return tfidf * (1 - engramDecay) * stateMultiplier;
537
- }
538
- function pruneToFit(entries, budget = tokenBudget) {
539
- if (entries.length === 0) {
540
- return {
541
- kept: [],
542
- removed: [],
543
- originalTokens: 0,
544
- prunedTokens: 0,
545
- budgetUtilization: 0
546
- };
547
- }
548
- const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
549
- const btspEntries = entries.filter((e) => e.isBTSP);
550
- const regularEntries = entries.filter((e) => !e.isBTSP);
551
- const btspTokens = btspEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
552
- const scored = regularEntries.map((entry) => ({
553
- entry,
554
- score: priorityScore(entry, entries),
555
- tokens: estimateTokens(entry.content)
556
- }));
557
- scored.sort((a, b) => b.score - a.score);
558
- const kept = [...btspEntries];
559
- const removed = [];
560
- let currentTokens = btspTokens;
561
- for (const item of scored) {
562
- if (currentTokens + item.tokens <= budget) {
563
- kept.push(item.entry);
564
- currentTokens += item.tokens;
565
- } else {
566
- removed.push(item.entry);
567
- }
568
- }
569
- const budgetUtilization = budget > 0 ? currentTokens / budget : 0;
570
- return {
571
- kept,
572
- removed,
573
- originalTokens,
574
- prunedTokens: currentTokens,
575
- budgetUtilization
576
- };
577
- }
578
- return {
579
- pruneToFit,
580
- priorityScore
581
- };
582
- }
583
- function createBudgetPrunerFromConfig(realtimeConfig, decayConfig, statesConfig) {
584
- return createBudgetPruner({
585
- tokenBudget: realtimeConfig.tokenBudget,
586
- decay: decayConfig,
587
- states: statesConfig
588
- });
589
- }
590
-
591
706
  // src/core/metrics.ts
592
707
  function createMetricsCollector() {
593
708
  const optimizations = [];
@@ -621,11 +736,10 @@ function createMetricsCollector() {
621
736
  ...metric
622
737
  };
623
738
  }
624
- function calculatePercentile(values, percentile) {
625
- if (values.length === 0) return 0;
626
- const sorted = [...values].sort((a, b) => a - b);
627
- const index = Math.ceil(percentile / 100 * sorted.length) - 1;
628
- return sorted[index] || 0;
739
+ function calculatePercentile(sortedValues, percentile) {
740
+ if (sortedValues.length === 0) return 0;
741
+ const index = Math.ceil(percentile / 100 * sortedValues.length) - 1;
742
+ return sortedValues[index] || 0;
629
743
  }
630
744
  function getSnapshot() {
631
745
  const totalRuns = optimizations.length;
@@ -636,7 +750,7 @@ function createMetricsCollector() {
636
750
  );
637
751
  const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
638
752
  const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
639
- const durations = optimizations.map((m) => m.duration);
753
+ const sortedDurations = optimizations.map((m) => m.duration).sort((a, b) => a - b);
640
754
  const totalCacheQueries = cacheHits + cacheMisses;
641
755
  const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
642
756
  return {
@@ -646,9 +760,9 @@ function createMetricsCollector() {
646
760
  totalDuration,
647
761
  totalTokensSaved,
648
762
  averageReduction,
649
- p50Latency: calculatePercentile(durations, 50),
650
- p95Latency: calculatePercentile(durations, 95),
651
- p99Latency: calculatePercentile(durations, 99)
763
+ p50Latency: calculatePercentile(sortedDurations, 50),
764
+ p95Latency: calculatePercentile(sortedDurations, 95),
765
+ p99Latency: calculatePercentile(sortedDurations, 99)
652
766
  },
653
767
  cache: {
654
768
  hitRate,
@@ -706,9 +820,6 @@ function createIncrementalOptimizer(config) {
706
820
  updateCount: 0,
707
821
  lastFullOptimization: Date.now()
708
822
  };
709
- function tokenize(text) {
710
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
711
- }
712
823
  function updateDocumentFrequency(entries, remove = false) {
713
824
  for (const entry of entries) {
714
825
  const tokens = tokenize(entry.content);
@@ -731,7 +842,19 @@ function createIncrementalOptimizer(config) {
731
842
  if (!cached) return null;
732
843
  return cached.entry;
733
844
  }
845
+ const MAX_CACHE_SIZE = 1e4;
734
846
  function cacheEntry(entry, score) {
847
+ if (state.entryCache.size >= MAX_CACHE_SIZE) {
848
+ const entries = Array.from(state.entryCache.entries());
849
+ entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
850
+ const toRemove = Math.floor(MAX_CACHE_SIZE * 0.2);
851
+ for (let i = 0; i < toRemove && i < entries.length; i++) {
852
+ const entry2 = entries[i];
853
+ if (entry2) {
854
+ state.entryCache.delete(entry2[0]);
855
+ }
856
+ }
857
+ }
735
858
  state.entryCache.set(entry.hash, {
736
859
  entry,
737
860
  score,
@@ -858,13 +981,40 @@ function createIncrementalOptimizer(config) {
858
981
  lastFullOptimization: state.lastFullOptimization
859
982
  };
860
983
  }
984
+ function serializeState() {
985
+ const s = getState();
986
+ return JSON.stringify({
987
+ entryCache: Array.from(s.entryCache.entries()),
988
+ documentFrequency: Array.from(s.documentFrequency.entries()),
989
+ totalDocuments: s.totalDocuments,
990
+ updateCount: s.updateCount,
991
+ lastFullOptimization: s.lastFullOptimization
992
+ });
993
+ }
994
+ function deserializeState(json) {
995
+ try {
996
+ const parsed = JSON.parse(json);
997
+ restoreState({
998
+ entryCache: new Map(parsed.entryCache),
999
+ documentFrequency: new Map(parsed.documentFrequency),
1000
+ totalDocuments: parsed.totalDocuments,
1001
+ updateCount: parsed.updateCount,
1002
+ lastFullOptimization: parsed.lastFullOptimization
1003
+ });
1004
+ return true;
1005
+ } catch {
1006
+ return false;
1007
+ }
1008
+ }
861
1009
  return {
862
1010
  optimizeIncremental,
863
1011
  optimizeFull,
864
1012
  getState,
865
1013
  restoreState,
866
1014
  reset,
867
- getStats
1015
+ getStats,
1016
+ serializeState,
1017
+ deserializeState
868
1018
  };
869
1019
  }
870
1020
 
@@ -889,9 +1039,22 @@ function createContextPipeline(config) {
889
1039
  currentEntries = result.kept;
890
1040
  budgetUtilization = result.budgetUtilization;
891
1041
  if (currentEntries.length > windowSize) {
892
- const sorted = [...currentEntries].sort((a, b) => b.timestamp - a.timestamp);
893
- const toKeep = sorted.slice(0, windowSize);
894
- const toRemove = sorted.slice(windowSize);
1042
+ const timestamps = currentEntries.map((e) => e.timestamp);
1043
+ const minTs = Math.min(...timestamps);
1044
+ const maxTs = Math.max(...timestamps);
1045
+ const tsRange = maxTs - minTs || 1;
1046
+ const scored = currentEntries.map((entry) => {
1047
+ if (entry.isBTSP) return { entry, hybridScore: 2 };
1048
+ const ageNormalized = (entry.timestamp - minTs) / tsRange;
1049
+ const hybridScore = ageNormalized * 0.4 + entry.score * 0.6;
1050
+ return { entry, hybridScore };
1051
+ });
1052
+ scored.sort((a, b) => {
1053
+ if (b.hybridScore !== a.hybridScore) return b.hybridScore - a.hybridScore;
1054
+ return b.entry.timestamp - a.entry.timestamp;
1055
+ });
1056
+ const toKeep = scored.slice(0, windowSize).map((s) => s.entry);
1057
+ const toRemove = scored.slice(windowSize);
895
1058
  currentEntries = toKeep;
896
1059
  evictedEntries += toRemove.length;
897
1060
  }
@@ -920,25 +1083,631 @@ function createContextPipeline(config) {
920
1083
  }
921
1084
  };
922
1085
  }
923
- function clear() {
924
- totalIngested = 0;
925
- evictedEntries = 0;
926
- currentEntries = [];
927
- budgetUtilization = 0;
928
- optimizer.reset();
1086
+ function clear() {
1087
+ totalIngested = 0;
1088
+ evictedEntries = 0;
1089
+ currentEntries = [];
1090
+ budgetUtilization = 0;
1091
+ optimizer.reset();
1092
+ }
1093
+ function serializeOptimizerState() {
1094
+ return optimizer.serializeState();
1095
+ }
1096
+ function deserializeOptimizerState(json) {
1097
+ return optimizer.deserializeState(json);
1098
+ }
1099
+ return {
1100
+ serializeOptimizerState,
1101
+ deserializeOptimizerState,
1102
+ ingest,
1103
+ getContext,
1104
+ getEntries,
1105
+ getStats,
1106
+ clear
1107
+ };
1108
+ }
1109
+
1110
+ // src/core/debt-tracker.ts
1111
+ import { randomUUID as randomUUID4 } from "crypto";
1112
+ import Database from "better-sqlite3";
1113
+ function createDebtTracker(dbPath) {
1114
+ const db = new Database(dbPath);
1115
+ db.pragma("journal_mode = WAL");
1116
+ db.exec(`
1117
+ CREATE TABLE IF NOT EXISTS tech_debt (
1118
+ id TEXT PRIMARY KEY NOT NULL,
1119
+ description TEXT NOT NULL,
1120
+ created_at INTEGER NOT NULL,
1121
+ repayment_date INTEGER NOT NULL,
1122
+ severity TEXT NOT NULL CHECK(severity IN ('P0', 'P1', 'P2')),
1123
+ token_cost INTEGER NOT NULL DEFAULT 0,
1124
+ files_affected TEXT NOT NULL DEFAULT '[]',
1125
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'in_progress', 'resolved')),
1126
+ resolution_tokens INTEGER,
1127
+ resolved_at INTEGER
1128
+ );
1129
+ `);
1130
+ db.exec(`
1131
+ CREATE INDEX IF NOT EXISTS idx_debt_status ON tech_debt(status);
1132
+ CREATE INDEX IF NOT EXISTS idx_debt_severity ON tech_debt(severity);
1133
+ CREATE INDEX IF NOT EXISTS idx_debt_repayment ON tech_debt(repayment_date);
1134
+ `);
1135
+ const insertStmt = db.prepare(`
1136
+ INSERT INTO tech_debt (id, description, created_at, repayment_date, severity, token_cost, files_affected, status)
1137
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'open')
1138
+ `);
1139
+ const getStmt = db.prepare("SELECT * FROM tech_debt WHERE id = ?");
1140
+ function rowToDebt(row) {
1141
+ return {
1142
+ id: row["id"],
1143
+ description: row["description"],
1144
+ created_at: row["created_at"],
1145
+ repayment_date: row["repayment_date"],
1146
+ severity: row["severity"],
1147
+ token_cost: row["token_cost"],
1148
+ files_affected: JSON.parse(row["files_affected"] || "[]"),
1149
+ status: row["status"],
1150
+ resolution_tokens: row["resolution_tokens"],
1151
+ resolved_at: row["resolved_at"]
1152
+ };
1153
+ }
1154
+ async function add(debt) {
1155
+ const id = randomUUID4().split("-")[0] || "debt";
1156
+ const created_at = Date.now();
1157
+ insertStmt.run(
1158
+ id,
1159
+ debt.description,
1160
+ created_at,
1161
+ debt.repayment_date,
1162
+ debt.severity,
1163
+ debt.token_cost,
1164
+ JSON.stringify(debt.files_affected)
1165
+ );
1166
+ return {
1167
+ id,
1168
+ created_at,
1169
+ status: "open",
1170
+ description: debt.description,
1171
+ repayment_date: debt.repayment_date,
1172
+ severity: debt.severity,
1173
+ token_cost: debt.token_cost,
1174
+ files_affected: debt.files_affected
1175
+ };
1176
+ }
1177
+ async function list(filter = {}) {
1178
+ let sql = "SELECT * FROM tech_debt WHERE 1=1";
1179
+ const params = [];
1180
+ if (filter.status) {
1181
+ sql += " AND status = ?";
1182
+ params.push(filter.status);
1183
+ }
1184
+ if (filter.severity) {
1185
+ sql += " AND severity = ?";
1186
+ params.push(filter.severity);
1187
+ }
1188
+ if (filter.overdue) {
1189
+ sql += " AND repayment_date < ? AND status != ?";
1190
+ params.push(Date.now());
1191
+ params.push("resolved");
1192
+ }
1193
+ sql += " ORDER BY severity ASC, repayment_date ASC";
1194
+ const rows = db.prepare(sql).all(...params);
1195
+ return rows.map(rowToDebt);
1196
+ }
1197
+ async function get(id) {
1198
+ const row = getStmt.get(id);
1199
+ if (!row) return null;
1200
+ return rowToDebt(row);
1201
+ }
1202
+ async function resolve2(id, resolutionTokens) {
1203
+ const result = db.prepare(
1204
+ "UPDATE tech_debt SET status = ?, resolution_tokens = ?, resolved_at = ? WHERE id = ?"
1205
+ ).run("resolved", resolutionTokens ?? null, Date.now(), id);
1206
+ if (result.changes === 0) {
1207
+ throw new Error(`Debt not found: ${id}`);
1208
+ }
1209
+ }
1210
+ async function start(id) {
1211
+ const result = db.prepare("UPDATE tech_debt SET status = ? WHERE id = ?").run("in_progress", id);
1212
+ if (result.changes === 0) {
1213
+ throw new Error(`Debt not found: ${id}`);
1214
+ }
1215
+ }
1216
+ async function remove(id) {
1217
+ db.prepare("DELETE FROM tech_debt WHERE id = ?").run(id);
1218
+ }
1219
+ async function stats() {
1220
+ const all = db.prepare("SELECT * FROM tech_debt").all();
1221
+ const debts = all.map(rowToDebt);
1222
+ const now = Date.now();
1223
+ const open = debts.filter((d) => d.status === "open");
1224
+ const inProgress = debts.filter((d) => d.status === "in_progress");
1225
+ const resolved = debts.filter((d) => d.status === "resolved");
1226
+ const overdue = debts.filter((d) => d.status !== "resolved" && d.repayment_date < now);
1227
+ const totalTokenCost = debts.reduce((sum, d) => sum + d.token_cost, 0);
1228
+ const resolvedTokenCost = resolved.reduce(
1229
+ (sum, d) => sum + (d.resolution_tokens || d.token_cost),
1230
+ 0
1231
+ );
1232
+ const resolvedOnTime = resolved.filter(
1233
+ (d) => d.resolved_at && d.resolved_at <= d.repayment_date
1234
+ ).length;
1235
+ const repaymentRate = resolved.length > 0 ? resolvedOnTime / resolved.length : 0;
1236
+ return {
1237
+ total: debts.length,
1238
+ open: open.length,
1239
+ in_progress: inProgress.length,
1240
+ resolved: resolved.length,
1241
+ overdue: overdue.length,
1242
+ totalTokenCost,
1243
+ resolvedTokenCost,
1244
+ repaymentRate
1245
+ };
1246
+ }
1247
+ async function getCritical() {
1248
+ const rows = db.prepare(
1249
+ "SELECT * FROM tech_debt WHERE severity = 'P0' AND status != 'resolved' ORDER BY repayment_date ASC"
1250
+ ).all();
1251
+ return rows.map(rowToDebt);
1252
+ }
1253
+ async function close() {
1254
+ db.close();
1255
+ }
1256
+ return {
1257
+ add,
1258
+ list,
1259
+ get,
1260
+ resolve: resolve2,
1261
+ start,
1262
+ remove,
1263
+ stats,
1264
+ getCritical,
1265
+ close
1266
+ };
1267
+ }
1268
+
1269
+ // src/core/dependency-graph.ts
1270
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
1271
+ import { extname, join, relative, resolve } from "path";
1272
+ var IMPORT_PATTERNS = [
1273
+ // import { Foo, Bar } from './module'
1274
+ /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g,
1275
+ // import Foo from './module'
1276
+ /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
1277
+ // import * as Foo from './module'
1278
+ /import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
1279
+ // import './module' (side-effect)
1280
+ /import\s+['"]([^'"]+)['"]/g,
1281
+ // require('./module')
1282
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
1283
+ ];
1284
+ var EXPORT_PATTERNS = [
1285
+ // export { Foo, Bar }
1286
+ /export\s+\{([^}]+)\}/g,
1287
+ // export function/class/const/let/var/type/interface
1288
+ /export\s+(?:default\s+)?(?:function|class|const|let|var|type|interface|enum)\s+(\w+)/g,
1289
+ // export default
1290
+ /export\s+default\s+/g
1291
+ ];
1292
+ function parseImports(content, filePath) {
1293
+ const edges = [];
1294
+ const seen = /* @__PURE__ */ new Set();
1295
+ for (const pattern of IMPORT_PATTERNS) {
1296
+ const regex = new RegExp(pattern.source, pattern.flags);
1297
+ const matches = [...content.matchAll(regex)];
1298
+ for (const match of matches) {
1299
+ if (pattern.source.includes("from")) {
1300
+ const symbolsRaw = match[1] || "";
1301
+ const target = match[2] || "";
1302
+ if (!target || target.startsWith(".") === false && !target.startsWith("/")) {
1303
+ continue;
1304
+ }
1305
+ const symbols = symbolsRaw.split(",").map(
1306
+ (s) => s.trim().split(/\s+as\s+/)[0]?.trim() || ""
1307
+ ).filter(Boolean);
1308
+ const key = `${filePath}->${target}`;
1309
+ if (!seen.has(key)) {
1310
+ seen.add(key);
1311
+ edges.push({ source: filePath, target, symbols });
1312
+ }
1313
+ } else if (pattern.source.includes("require")) {
1314
+ const target = match[1] || "";
1315
+ if (!target || !target.startsWith(".") && !target.startsWith("/")) {
1316
+ continue;
1317
+ }
1318
+ const key = `${filePath}->${target}`;
1319
+ if (!seen.has(key)) {
1320
+ seen.add(key);
1321
+ edges.push({ source: filePath, target, symbols: [] });
1322
+ }
1323
+ } else {
1324
+ const target = match[1] || "";
1325
+ if (!target || !target.startsWith(".") && !target.startsWith("/")) {
1326
+ continue;
1327
+ }
1328
+ const key = `${filePath}->${target}`;
1329
+ if (!seen.has(key)) {
1330
+ seen.add(key);
1331
+ edges.push({ source: filePath, target, symbols: [] });
1332
+ }
1333
+ }
1334
+ }
1335
+ }
1336
+ return edges;
1337
+ }
1338
+ function parseExports(content) {
1339
+ const exportsList = [];
1340
+ for (const pattern of EXPORT_PATTERNS) {
1341
+ const regex = new RegExp(pattern.source, pattern.flags);
1342
+ const matches = [...content.matchAll(regex)];
1343
+ for (const match of matches) {
1344
+ if (match[1]) {
1345
+ const symbols = match[1].split(",").map(
1346
+ (s) => s.trim().split(/\s+as\s+/)[0]?.trim() || ""
1347
+ ).filter(Boolean);
1348
+ exportsList.push(...symbols);
1349
+ } else {
1350
+ exportsList.push("default");
1351
+ }
1352
+ }
1353
+ }
1354
+ return [...new Set(exportsList)];
1355
+ }
1356
+ function resolveImportPath(importPath, fromFile, projectRoot, extensions) {
1357
+ const cleanImport = importPath.replace(/\.(js|ts|tsx|jsx)$/, "");
1358
+ const baseDir = join(projectRoot, fromFile, "..");
1359
+ const candidates = [
1360
+ ...extensions.map((ext) => resolve(baseDir, `${cleanImport}${ext}`)),
1361
+ ...extensions.map((ext) => resolve(baseDir, cleanImport, `index${ext}`))
1362
+ ];
1363
+ for (const candidate of candidates) {
1364
+ if (existsSync(candidate)) {
1365
+ return relative(projectRoot, candidate).replace(/\\/g, "/");
1366
+ }
1367
+ }
1368
+ return null;
1369
+ }
1370
+ function collectFiles(dir, projectRoot, extensions, ignoreDirs) {
1371
+ const files = [];
1372
+ try {
1373
+ const entries = readdirSync(dir, { withFileTypes: true });
1374
+ for (const entry of entries) {
1375
+ const fullPath = join(dir, entry.name);
1376
+ if (entry.isDirectory()) {
1377
+ if (!ignoreDirs.includes(entry.name)) {
1378
+ files.push(...collectFiles(fullPath, projectRoot, extensions, ignoreDirs));
1379
+ }
1380
+ } else if (entry.isFile() && extensions.includes(extname(entry.name))) {
1381
+ files.push(relative(projectRoot, fullPath).replace(/\\/g, "/"));
1382
+ }
1383
+ }
1384
+ } catch {
1385
+ }
1386
+ return files;
1387
+ }
1388
+ function createDependencyGraph(config) {
1389
+ const {
1390
+ projectRoot,
1391
+ maxDepth = 50,
1392
+ extensions = [".ts", ".tsx", ".js", ".jsx"],
1393
+ ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"]
1394
+ } = config;
1395
+ const nodes = /* @__PURE__ */ new Map();
1396
+ let built = false;
1397
+ async function build() {
1398
+ nodes.clear();
1399
+ const files = collectFiles(projectRoot, projectRoot, extensions, ignoreDirs);
1400
+ for (const filePath of files) {
1401
+ const fullPath = join(projectRoot, filePath);
1402
+ try {
1403
+ const content = readFileSync(fullPath, "utf-8");
1404
+ const stat = statSync(fullPath);
1405
+ const exports = parseExports(content);
1406
+ const imports = parseImports(content, filePath);
1407
+ const resolvedImports = [];
1408
+ for (const imp of imports) {
1409
+ const resolved = resolveImportPath(imp.target, filePath, projectRoot, extensions);
1410
+ if (resolved) {
1411
+ resolvedImports.push({ ...imp, target: resolved });
1412
+ }
1413
+ }
1414
+ nodes.set(filePath, {
1415
+ filePath,
1416
+ exports,
1417
+ imports: resolvedImports,
1418
+ callers: [],
1419
+ // Populated in second pass
1420
+ engram_score: 0,
1421
+ lastModified: stat.mtimeMs,
1422
+ tokenEstimate: estimateTokens(content)
1423
+ });
1424
+ } catch {
1425
+ }
1426
+ }
1427
+ for (const [filePath, node] of nodes) {
1428
+ for (const imp of node.imports) {
1429
+ const targetNode = nodes.get(imp.target);
1430
+ if (targetNode && !targetNode.callers.includes(filePath)) {
1431
+ targetNode.callers.push(filePath);
1432
+ }
1433
+ }
1434
+ }
1435
+ built = true;
1436
+ return nodes;
1437
+ }
1438
+ async function analyze() {
1439
+ if (!built) await build();
1440
+ const entryPoints = [];
1441
+ const orphans = [];
1442
+ const callerCounts = /* @__PURE__ */ new Map();
1443
+ for (const [filePath, node] of nodes) {
1444
+ if (node.callers.length === 0 && node.imports.length > 0) {
1445
+ entryPoints.push(filePath);
1446
+ }
1447
+ if (node.callers.length === 0 && node.imports.length === 0) {
1448
+ orphans.push(filePath);
1449
+ }
1450
+ callerCounts.set(filePath, node.callers.length);
1451
+ }
1452
+ const sortedByCallers = [...callerCounts.entries()].sort((a, b) => b[1] - a[1]).filter(([, count]) => count > 0);
1453
+ const hotPaths = sortedByCallers.slice(0, 10).map(([path]) => path);
1454
+ const totalTokens = [...nodes.values()].reduce((sum, n) => sum + n.tokenEstimate, 0);
1455
+ const hotPathTokens = hotPaths.reduce(
1456
+ (sum, path) => sum + (nodes.get(path)?.tokenEstimate || 0),
1457
+ 0
1458
+ );
1459
+ return {
1460
+ entryPoints,
1461
+ hotPaths,
1462
+ orphans,
1463
+ totalTokens,
1464
+ optimizedTokens: hotPathTokens
1465
+ };
1466
+ }
1467
+ async function focus(pattern) {
1468
+ if (!built) await build();
1469
+ const matching = /* @__PURE__ */ new Map();
1470
+ const lowerPattern = pattern.toLowerCase();
1471
+ for (const [filePath, node] of nodes) {
1472
+ if (filePath.toLowerCase().includes(lowerPattern)) {
1473
+ matching.set(filePath, node);
1474
+ for (const imp of node.imports) {
1475
+ const depNode = nodes.get(imp.target);
1476
+ if (depNode) {
1477
+ matching.set(imp.target, depNode);
1478
+ }
1479
+ }
1480
+ for (const caller of node.callers) {
1481
+ const callerNode = nodes.get(caller);
1482
+ if (callerNode) {
1483
+ matching.set(caller, callerNode);
1484
+ }
1485
+ }
1486
+ }
1487
+ }
1488
+ return matching;
1489
+ }
1490
+ async function getFilesFromEntry(entryPoint, depth = maxDepth) {
1491
+ if (!built) await build();
1492
+ const visited = /* @__PURE__ */ new Set();
1493
+ const queue = [{ path: entryPoint, depth: 0 }];
1494
+ while (queue.length > 0) {
1495
+ const item = queue.shift();
1496
+ if (!item) break;
1497
+ if (visited.has(item.path) || item.depth > depth) continue;
1498
+ visited.add(item.path);
1499
+ const node = nodes.get(item.path);
1500
+ if (node) {
1501
+ for (const imp of node.imports) {
1502
+ if (!visited.has(imp.target)) {
1503
+ queue.push({ path: imp.target, depth: item.depth + 1 });
1504
+ }
1505
+ }
1506
+ }
1507
+ }
1508
+ return [...visited];
1509
+ }
1510
+ function getNodes() {
1511
+ return nodes;
929
1512
  }
930
1513
  return {
931
- ingest,
932
- getContext,
933
- getEntries,
934
- getStats,
935
- clear
1514
+ build,
1515
+ analyze,
1516
+ focus,
1517
+ getFilesFromEntry,
1518
+ getNodes
1519
+ };
1520
+ }
1521
+
1522
+ // src/core/docs-generator.ts
1523
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
1524
+ import { extname as extname2, join as join2, relative as relative2 } from "path";
1525
+ function detectEntryPoints(projectRoot) {
1526
+ const entries = [];
1527
+ const candidates = [
1528
+ { path: "src/index.ts", desc: "Library API" },
1529
+ { path: "src/cli/index.ts", desc: "CLI entry point" },
1530
+ { path: "src/daemon/index.ts", desc: "Daemon process" },
1531
+ { path: "src/mcp/index.ts", desc: "MCP server" },
1532
+ { path: "src/main.ts", desc: "Main entry" },
1533
+ { path: "src/app.ts", desc: "App entry" },
1534
+ { path: "src/server.ts", desc: "Server entry" },
1535
+ { path: "index.ts", desc: "Root entry" },
1536
+ { path: "index.js", desc: "Root entry" }
1537
+ ];
1538
+ for (const c of candidates) {
1539
+ if (existsSync2(join2(projectRoot, c.path))) {
1540
+ entries.push({ path: c.path, description: c.desc });
1541
+ }
1542
+ }
1543
+ return entries;
1544
+ }
1545
+ function scanModules(dir, projectRoot, ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"]) {
1546
+ const modules = [];
1547
+ try {
1548
+ const entries = readdirSync2(dir, { withFileTypes: true });
1549
+ for (const entry of entries) {
1550
+ const fullPath = join2(dir, entry.name);
1551
+ if (entry.isDirectory() && !ignoreDirs.includes(entry.name)) {
1552
+ modules.push(...scanModules(fullPath, projectRoot, ignoreDirs));
1553
+ } else if (entry.isFile() && [".ts", ".tsx", ".js", ".jsx"].includes(extname2(entry.name))) {
1554
+ try {
1555
+ const content = readFileSync2(fullPath, "utf-8");
1556
+ modules.push({
1557
+ path: relative2(projectRoot, fullPath).replace(/\\/g, "/"),
1558
+ lines: content.split("\n").length
1559
+ });
1560
+ } catch {
1561
+ }
1562
+ }
1563
+ }
1564
+ } catch {
1565
+ }
1566
+ return modules;
1567
+ }
1568
+ function readPackageJson(projectRoot) {
1569
+ const pkgPath = join2(projectRoot, "package.json");
1570
+ if (!existsSync2(pkgPath)) return null;
1571
+ try {
1572
+ return JSON.parse(readFileSync2(pkgPath, "utf-8"));
1573
+ } catch {
1574
+ return null;
1575
+ }
1576
+ }
1577
+ function detectStack(projectRoot) {
1578
+ const stack = [];
1579
+ const pkg = readPackageJson(projectRoot);
1580
+ if (!pkg) return stack;
1581
+ const allDeps = {
1582
+ ...pkg.dependencies,
1583
+ ...pkg.devDependencies
936
1584
  };
1585
+ if (allDeps["typescript"]) stack.push("TypeScript");
1586
+ if (allDeps["vitest"]) stack.push("Vitest");
1587
+ if (allDeps["@biomejs/biome"]) stack.push("Biome");
1588
+ if (allDeps["eslint"]) stack.push("ESLint");
1589
+ if (allDeps["prettier"]) stack.push("Prettier");
1590
+ if (allDeps["react"]) stack.push("React");
1591
+ if (allDeps["next"]) stack.push("Next.js");
1592
+ if (allDeps["express"]) stack.push("Express");
1593
+ if (allDeps["commander"]) stack.push("Commander.js CLI");
1594
+ if (allDeps["better-sqlite3"]) stack.push("SQLite (better-sqlite3)");
1595
+ if (allDeps["zod"]) stack.push("Zod validation");
1596
+ return stack;
1597
+ }
1598
+ function createDocsGenerator(config) {
1599
+ const { projectRoot, includeGraph = true } = config;
1600
+ async function generate(graph) {
1601
+ const lines = [];
1602
+ const pkg = readPackageJson(projectRoot);
1603
+ const projectName = pkg?.name || "Project";
1604
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1605
+ lines.push(`# ${projectName} \u2014 Developer Guide`);
1606
+ lines.push(`<!-- Auto-generated by Sparn v1.3.0 \u2014 ${now} -->`);
1607
+ lines.push("");
1608
+ const stack = detectStack(projectRoot);
1609
+ if (stack.length > 0) {
1610
+ lines.push(`**Stack**: ${stack.join(", ")}`);
1611
+ lines.push("");
1612
+ }
1613
+ if (pkg?.scripts) {
1614
+ lines.push("## Commands");
1615
+ lines.push("");
1616
+ const important = ["build", "dev", "test", "lint", "typecheck", "start"];
1617
+ for (const cmd of important) {
1618
+ if (pkg.scripts[cmd]) {
1619
+ lines.push(`- \`npm run ${cmd}\` \u2014 \`${pkg.scripts[cmd]}\``);
1620
+ }
1621
+ }
1622
+ lines.push("");
1623
+ }
1624
+ const entryPoints = detectEntryPoints(projectRoot);
1625
+ if (entryPoints.length > 0) {
1626
+ lines.push("## Entry Points");
1627
+ lines.push("");
1628
+ for (const ep of entryPoints) {
1629
+ lines.push(`- \`${ep.path}\` \u2014 ${ep.description}`);
1630
+ }
1631
+ lines.push("");
1632
+ }
1633
+ const srcDir = join2(projectRoot, "src");
1634
+ if (existsSync2(srcDir)) {
1635
+ const modules = scanModules(srcDir, projectRoot);
1636
+ const dirGroups = /* @__PURE__ */ new Map();
1637
+ for (const mod of modules) {
1638
+ const parts = mod.path.split("/");
1639
+ const dir = parts.length > 2 ? parts.slice(0, 2).join("/") : parts[0] || "";
1640
+ if (!dirGroups.has(dir)) {
1641
+ dirGroups.set(dir, []);
1642
+ }
1643
+ dirGroups.get(dir)?.push({
1644
+ file: parts[parts.length - 1] || mod.path,
1645
+ lines: mod.lines
1646
+ });
1647
+ }
1648
+ lines.push("## Structure");
1649
+ lines.push("");
1650
+ for (const [dir, files] of dirGroups) {
1651
+ lines.push(`### ${dir}/ (${files.length} files)`);
1652
+ const shown = files.slice(0, 8);
1653
+ for (const f of shown) {
1654
+ lines.push(`- \`${f.file}\` (${f.lines}L)`);
1655
+ }
1656
+ if (files.length > 8) {
1657
+ lines.push(`- ... and ${files.length - 8} more`);
1658
+ }
1659
+ lines.push("");
1660
+ }
1661
+ }
1662
+ if (includeGraph && graph) {
1663
+ try {
1664
+ const analysis = await graph.analyze();
1665
+ lines.push("## Hot Dependencies (most imported)");
1666
+ lines.push("");
1667
+ for (const path of analysis.hotPaths.slice(0, 5)) {
1668
+ const node = graph.getNodes().get(path);
1669
+ const callerCount = node?.callers.length || 0;
1670
+ lines.push(`- \`${path}\` (imported by ${callerCount} modules)`);
1671
+ }
1672
+ lines.push("");
1673
+ if (analysis.orphans.length > 0) {
1674
+ lines.push(
1675
+ `**Orphaned files** (${analysis.orphans.length}): ${analysis.orphans.slice(0, 3).join(", ")}${analysis.orphans.length > 3 ? "..." : ""}`
1676
+ );
1677
+ lines.push("");
1678
+ }
1679
+ lines.push(
1680
+ `**Total tokens**: ${analysis.totalTokens.toLocaleString()} | **Hot path tokens**: ${analysis.optimizedTokens.toLocaleString()}`
1681
+ );
1682
+ lines.push("");
1683
+ } catch {
1684
+ }
1685
+ }
1686
+ const sparnConfigPath = join2(projectRoot, ".sparn", "config.yaml");
1687
+ if (existsSync2(sparnConfigPath)) {
1688
+ lines.push("## Sparn Optimization");
1689
+ lines.push("");
1690
+ lines.push("Sparn is active in this project. Key features:");
1691
+ lines.push("- Context optimization (60-70% token reduction)");
1692
+ lines.push("- Use `sparn search` before reading files");
1693
+ lines.push("- Use `sparn graph` to understand dependencies");
1694
+ lines.push("- Use `sparn plan` for planning, `sparn exec` for execution");
1695
+ lines.push("");
1696
+ }
1697
+ if (config.customSections) {
1698
+ for (const section of config.customSections) {
1699
+ lines.push(section);
1700
+ lines.push("");
1701
+ }
1702
+ }
1703
+ return lines.join("\n");
1704
+ }
1705
+ return { generate };
937
1706
  }
938
1707
 
939
1708
  // src/core/kv-memory.ts
940
- import { copyFileSync, existsSync } from "fs";
941
- import Database from "better-sqlite3";
1709
+ import { copyFileSync, existsSync as existsSync3 } from "fs";
1710
+ import Database2 from "better-sqlite3";
942
1711
  function createBackup(dbPath) {
943
1712
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
944
1713
  const backupPath = `${dbPath}.backup-${timestamp}`;
@@ -954,29 +1723,30 @@ function createBackup(dbPath) {
954
1723
  async function createKVMemory(dbPath) {
955
1724
  let db;
956
1725
  try {
957
- db = new Database(dbPath);
1726
+ db = new Database2(dbPath);
958
1727
  const integrityCheck = db.pragma("quick_check", { simple: true });
959
1728
  if (integrityCheck !== "ok") {
960
1729
  console.error("\u26A0 Database corruption detected!");
961
- if (existsSync(dbPath)) {
1730
+ db.close();
1731
+ if (existsSync3(dbPath)) {
962
1732
  const backupPath = createBackup(dbPath);
963
1733
  if (backupPath) {
964
1734
  console.log(`Backup created at: ${backupPath}`);
965
1735
  }
966
1736
  }
967
1737
  console.log("Attempting database recovery...");
968
- db.close();
969
- db = new Database(dbPath);
1738
+ db = new Database2(dbPath);
970
1739
  }
971
1740
  } catch (error) {
972
1741
  console.error("\u26A0 Database error detected:", error);
973
- if (existsSync(dbPath)) {
1742
+ if (existsSync3(dbPath)) {
974
1743
  createBackup(dbPath);
975
1744
  console.log("Creating new database...");
976
1745
  }
977
- db = new Database(dbPath);
1746
+ db = new Database2(dbPath);
978
1747
  }
979
1748
  db.pragma("journal_mode = WAL");
1749
+ db.pragma("foreign_keys = ON");
980
1750
  db.exec(`
981
1751
  CREATE TABLE IF NOT EXISTS entries_index (
982
1752
  id TEXT PRIMARY KEY NOT NULL,
@@ -1016,6 +1786,36 @@ async function createKVMemory(dbPath) {
1016
1786
  CREATE INDEX IF NOT EXISTS idx_entries_timestamp ON entries_index(timestamp DESC);
1017
1787
  CREATE INDEX IF NOT EXISTS idx_stats_timestamp ON optimization_stats(timestamp DESC);
1018
1788
  `);
1789
+ db.exec(`
1790
+ CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(id, content, tokenize='porter');
1791
+ `);
1792
+ db.exec(`
1793
+ CREATE TRIGGER IF NOT EXISTS entries_fts_insert
1794
+ AFTER INSERT ON entries_value
1795
+ BEGIN
1796
+ INSERT OR REPLACE INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
1797
+ END;
1798
+ `);
1799
+ db.exec(`
1800
+ CREATE TRIGGER IF NOT EXISTS entries_fts_delete
1801
+ AFTER DELETE ON entries_value
1802
+ BEGIN
1803
+ DELETE FROM entries_fts WHERE id = OLD.id;
1804
+ END;
1805
+ `);
1806
+ db.exec(`
1807
+ CREATE TRIGGER IF NOT EXISTS entries_fts_update
1808
+ AFTER UPDATE ON entries_value
1809
+ BEGIN
1810
+ DELETE FROM entries_fts WHERE id = OLD.id;
1811
+ INSERT INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
1812
+ END;
1813
+ `);
1814
+ db.exec(`
1815
+ INSERT OR IGNORE INTO entries_fts(id, content)
1816
+ SELECT id, content FROM entries_value
1817
+ WHERE id NOT IN (SELECT id FROM entries_fts);
1818
+ `);
1019
1819
  const putIndexStmt = db.prepare(`
1020
1820
  INSERT OR REPLACE INTO entries_index
1021
1821
  (id, hash, timestamp, score, ttl, state, accessCount, isBTSP)
@@ -1104,14 +1904,20 @@ async function createKVMemory(dbPath) {
1104
1904
  sql += " AND i.isBTSP = ?";
1105
1905
  params.push(filters.isBTSP ? 1 : 0);
1106
1906
  }
1907
+ if (filters.tags && filters.tags.length > 0) {
1908
+ for (const tag of filters.tags) {
1909
+ sql += " AND v.tags LIKE ?";
1910
+ params.push(`%"${tag}"%`);
1911
+ }
1912
+ }
1107
1913
  sql += " ORDER BY i.score DESC";
1108
1914
  if (filters.limit) {
1109
1915
  sql += " LIMIT ?";
1110
1916
  params.push(filters.limit);
1111
- }
1112
- if (filters.offset) {
1113
- sql += " OFFSET ?";
1114
- params.push(filters.offset);
1917
+ if (filters.offset) {
1918
+ sql += " OFFSET ?";
1919
+ params.push(filters.offset);
1920
+ }
1115
1921
  }
1116
1922
  const stmt = db.prepare(sql);
1117
1923
  const rows = stmt.all(...params);
@@ -1146,7 +1952,22 @@ async function createKVMemory(dbPath) {
1146
1952
  },
1147
1953
  async compact() {
1148
1954
  const before = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
1149
- db.exec("DELETE FROM entries_index WHERE ttl <= 0");
1955
+ const now = Date.now();
1956
+ db.prepare("DELETE FROM entries_index WHERE isBTSP = 0 AND (timestamp + ttl * 1000) < ?").run(
1957
+ now
1958
+ );
1959
+ db.exec("DELETE FROM entries_index WHERE isBTSP = 0 AND ttl <= 0");
1960
+ const candidates = db.prepare("SELECT id, timestamp, ttl FROM entries_index WHERE isBTSP = 0").all();
1961
+ for (const row of candidates) {
1962
+ const ageSeconds = Math.max(0, (now - row.timestamp) / 1e3);
1963
+ const ttlSeconds = row.ttl;
1964
+ if (ttlSeconds <= 0) continue;
1965
+ const decay = 1 - Math.exp(-ageSeconds / ttlSeconds);
1966
+ if (decay >= 0.95) {
1967
+ db.prepare("DELETE FROM entries_index WHERE id = ?").run(row.id);
1968
+ }
1969
+ }
1970
+ db.exec("DELETE FROM entries_value WHERE id NOT IN (SELECT id FROM entries_index)");
1150
1971
  db.exec("VACUUM");
1151
1972
  const after = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
1152
1973
  return before.count - after.count;
@@ -1166,6 +1987,9 @@ async function createKVMemory(dbPath) {
1166
1987
  stats.entries_pruned,
1167
1988
  stats.duration_ms
1168
1989
  );
1990
+ db.prepare(
1991
+ "DELETE FROM optimization_stats WHERE id NOT IN (SELECT id FROM optimization_stats ORDER BY timestamp DESC LIMIT 1000)"
1992
+ ).run();
1169
1993
  },
1170
1994
  async getOptimizationStats() {
1171
1995
  const stmt = db.prepare(`
@@ -1178,7 +2002,286 @@ async function createKVMemory(dbPath) {
1178
2002
  },
1179
2003
  async clearOptimizationStats() {
1180
2004
  db.exec("DELETE FROM optimization_stats");
2005
+ },
2006
+ async searchFTS(query, limit = 10) {
2007
+ if (!query || query.trim().length === 0) return [];
2008
+ const sanitized = query.replace(/[{}()[\]"':*^~]/g, " ").trim();
2009
+ if (sanitized.length === 0) return [];
2010
+ const stmt = db.prepare(`
2011
+ SELECT
2012
+ f.id, f.content, rank,
2013
+ i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
2014
+ v.tags, v.metadata
2015
+ FROM entries_fts f
2016
+ JOIN entries_index i ON f.id = i.id
2017
+ JOIN entries_value v ON f.id = v.id
2018
+ WHERE entries_fts MATCH ?
2019
+ ORDER BY rank
2020
+ LIMIT ?
2021
+ `);
2022
+ try {
2023
+ const rows = stmt.all(sanitized, limit);
2024
+ return rows.map((r) => ({
2025
+ entry: {
2026
+ id: r.id,
2027
+ content: r.content,
2028
+ hash: r.hash,
2029
+ timestamp: r.timestamp,
2030
+ score: r.score,
2031
+ ttl: r.ttl,
2032
+ state: r.state,
2033
+ accessCount: r.accessCount,
2034
+ tags: r.tags ? JSON.parse(r.tags) : [],
2035
+ metadata: r.metadata ? JSON.parse(r.metadata) : {},
2036
+ isBTSP: r.isBTSP === 1
2037
+ },
2038
+ rank: r.rank
2039
+ }));
2040
+ } catch {
2041
+ return [];
2042
+ }
2043
+ }
2044
+ };
2045
+ }
2046
+
2047
+ // src/core/search-engine.ts
2048
+ import { execFileSync, execSync } from "child_process";
2049
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
2050
+ import { extname as extname3, join as join3, relative as relative3 } from "path";
2051
+ import Database3 from "better-sqlite3";
2052
+ function hasRipgrep() {
2053
+ try {
2054
+ execSync("rg --version", { stdio: "pipe" });
2055
+ return true;
2056
+ } catch {
2057
+ return false;
2058
+ }
2059
+ }
2060
+ function ripgrepSearch(query, projectRoot, opts = {}) {
2061
+ try {
2062
+ const args = ["--line-number", "--no-heading", "--color=never"];
2063
+ if (opts.fileGlob) {
2064
+ args.push("--glob", opts.fileGlob);
2065
+ }
2066
+ args.push(
2067
+ "--glob",
2068
+ "!node_modules",
2069
+ "--glob",
2070
+ "!dist",
2071
+ "--glob",
2072
+ "!.git",
2073
+ "--glob",
2074
+ "!.sparn",
2075
+ "--glob",
2076
+ "!coverage"
2077
+ );
2078
+ const maxResults = opts.maxResults || 20;
2079
+ args.push("--max-count", String(maxResults));
2080
+ if (opts.includeContext) {
2081
+ args.push("-C", "2");
2082
+ }
2083
+ args.push("--", query, projectRoot);
2084
+ const output = execFileSync("rg", args, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
2085
+ const results = [];
2086
+ const lines = output.split("\n").filter(Boolean);
2087
+ for (const line of lines) {
2088
+ const match = line.match(/^(.+?):(\d+):(.*)/);
2089
+ if (match) {
2090
+ const filePath = relative3(projectRoot, match[1] || "").replace(/\\/g, "/");
2091
+ const lineNumber = Number.parseInt(match[2] || "0", 10);
2092
+ const content = (match[3] || "").trim();
2093
+ results.push({
2094
+ filePath,
2095
+ lineNumber,
2096
+ content,
2097
+ score: 0.8,
2098
+ // Exact match gets high base score
2099
+ context: [],
2100
+ engram_score: 0
2101
+ });
2102
+ }
2103
+ }
2104
+ return results.slice(0, maxResults);
2105
+ } catch {
2106
+ return [];
2107
+ }
2108
+ }
2109
+ function collectIndexableFiles(dir, projectRoot, ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"], exts = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".yaml", ".yml"]) {
2110
+ const files = [];
2111
+ try {
2112
+ const entries = readdirSync3(dir, { withFileTypes: true });
2113
+ for (const entry of entries) {
2114
+ const fullPath = join3(dir, entry.name);
2115
+ if (entry.isDirectory()) {
2116
+ if (!ignoreDirs.includes(entry.name)) {
2117
+ files.push(...collectIndexableFiles(fullPath, projectRoot, ignoreDirs, exts));
2118
+ }
2119
+ } else if (entry.isFile() && exts.includes(extname3(entry.name))) {
2120
+ try {
2121
+ const stat = statSync2(fullPath);
2122
+ if (stat.size < 100 * 1024) {
2123
+ files.push(relative3(projectRoot, fullPath).replace(/\\/g, "/"));
2124
+ }
2125
+ } catch {
2126
+ }
2127
+ }
2128
+ }
2129
+ } catch {
2130
+ }
2131
+ return files;
2132
+ }
2133
+ function createSearchEngine(dbPath) {
2134
+ let db = null;
2135
+ let projectRoot = "";
2136
+ const rgAvailable = hasRipgrep();
2137
+ async function init(root) {
2138
+ if (db) {
2139
+ try {
2140
+ db.close();
2141
+ } catch {
2142
+ }
2143
+ }
2144
+ projectRoot = root;
2145
+ db = new Database3(dbPath);
2146
+ db.pragma("journal_mode = WAL");
2147
+ db.exec(`
2148
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
2149
+ filepath, line_number, content, tokenize='porter'
2150
+ );
2151
+ `);
2152
+ db.exec(`
2153
+ CREATE TABLE IF NOT EXISTS search_meta (
2154
+ filepath TEXT PRIMARY KEY,
2155
+ mtime INTEGER NOT NULL,
2156
+ indexed_at INTEGER NOT NULL
2157
+ );
2158
+ `);
2159
+ }
2160
+ async function index(paths) {
2161
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
2162
+ const startTime = Date.now();
2163
+ const filesToIndex = paths || collectIndexableFiles(projectRoot, projectRoot);
2164
+ let filesIndexed = 0;
2165
+ let totalLines = 0;
2166
+ const insertStmt = db.prepare(
2167
+ "INSERT INTO search_index(filepath, line_number, content) VALUES (?, ?, ?)"
2168
+ );
2169
+ const metaStmt = db.prepare(
2170
+ "INSERT OR REPLACE INTO search_meta(filepath, mtime, indexed_at) VALUES (?, ?, ?)"
2171
+ );
2172
+ const checkMeta = db.prepare("SELECT mtime FROM search_meta WHERE filepath = ?");
2173
+ const deleteFile = db.prepare("DELETE FROM search_index WHERE filepath = ?");
2174
+ const transaction = db.transaction(() => {
2175
+ for (const filePath of filesToIndex) {
2176
+ const fullPath = join3(projectRoot, filePath);
2177
+ if (!existsSync4(fullPath)) continue;
2178
+ try {
2179
+ const stat = statSync2(fullPath);
2180
+ const existing = checkMeta.get(filePath);
2181
+ if (existing && existing.mtime >= stat.mtimeMs) {
2182
+ continue;
2183
+ }
2184
+ deleteFile.run(filePath);
2185
+ const content = readFileSync3(fullPath, "utf-8");
2186
+ const lines = content.split("\n");
2187
+ for (let i = 0; i < lines.length; i++) {
2188
+ const line = lines[i];
2189
+ if (line && line.trim().length > 0) {
2190
+ insertStmt.run(filePath, i + 1, line);
2191
+ totalLines++;
2192
+ }
2193
+ }
2194
+ metaStmt.run(filePath, stat.mtimeMs, Date.now());
2195
+ filesIndexed++;
2196
+ } catch {
2197
+ }
2198
+ }
2199
+ });
2200
+ transaction();
2201
+ return {
2202
+ filesIndexed,
2203
+ totalLines,
2204
+ duration: Date.now() - startTime
2205
+ };
2206
+ }
2207
+ async function search(query, opts = {}) {
2208
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
2209
+ const maxResults = opts.maxResults || 10;
2210
+ const results = [];
2211
+ const seen = /* @__PURE__ */ new Set();
2212
+ try {
2213
+ const ftsQuery = query.replace(/['"*(){}[\]^~\\:]/g, " ").trim().split(/\s+/).filter((w) => w.length > 0).map((w) => `content:${w}`).join(" ");
2214
+ if (ftsQuery.length > 0) {
2215
+ let sql = `
2216
+ SELECT filepath, line_number, content, rank
2217
+ FROM search_index
2218
+ WHERE search_index MATCH ?
2219
+ `;
2220
+ const params = [ftsQuery];
2221
+ if (opts.fileGlob) {
2222
+ const likePattern = opts.fileGlob.replace(/\*/g, "%").replace(/\?/g, "_");
2223
+ sql += " AND filepath LIKE ?";
2224
+ params.push(likePattern);
2225
+ }
2226
+ sql += " ORDER BY rank LIMIT ?";
2227
+ params.push(maxResults * 2);
2228
+ const rows = db.prepare(sql).all(...params);
2229
+ for (const row of rows) {
2230
+ const key = `${row.filepath}:${row.line_number}`;
2231
+ if (!seen.has(key)) {
2232
+ seen.add(key);
2233
+ const score = Math.min(1, Math.max(0, 1 + row.rank / 10));
2234
+ const context = [];
2235
+ if (opts.includeContext) {
2236
+ const contextRows = db.prepare(
2237
+ `SELECT content FROM search_index WHERE filepath = ? AND CAST(line_number AS INTEGER) BETWEEN ? AND ? ORDER BY CAST(line_number AS INTEGER)`
2238
+ ).all(row.filepath, row.line_number - 2, row.line_number + 2);
2239
+ context.push(...contextRows.map((r) => r.content));
2240
+ }
2241
+ results.push({
2242
+ filePath: row.filepath,
2243
+ lineNumber: row.line_number,
2244
+ content: row.content,
2245
+ score,
2246
+ context,
2247
+ engram_score: 0
2248
+ });
2249
+ }
2250
+ }
2251
+ }
2252
+ } catch {
2253
+ }
2254
+ if (rgAvailable && opts.useRipgrep !== false) {
2255
+ const rgResults = ripgrepSearch(query, projectRoot, opts);
2256
+ for (const result of rgResults) {
2257
+ const key = `${result.filePath}:${result.lineNumber}`;
2258
+ if (!seen.has(key)) {
2259
+ seen.add(key);
2260
+ results.push(result);
2261
+ }
2262
+ }
2263
+ }
2264
+ results.sort((a, b) => b.score - a.score);
2265
+ return results.slice(0, maxResults);
2266
+ }
2267
+ async function refresh() {
2268
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
2269
+ db.exec("DELETE FROM search_index");
2270
+ db.exec("DELETE FROM search_meta");
2271
+ return index();
2272
+ }
2273
+ async function close() {
2274
+ if (db) {
2275
+ db.close();
2276
+ db = null;
1181
2277
  }
2278
+ }
2279
+ return {
2280
+ init,
2281
+ index,
2282
+ search,
2283
+ refresh,
2284
+ close
1182
2285
  };
1183
2286
  }
1184
2287
 
@@ -1273,13 +2376,15 @@ function createSleepCompressor() {
1273
2376
  function cosineSimilarity(text1, text2) {
1274
2377
  const words1 = tokenize(text1);
1275
2378
  const words2 = tokenize(text2);
1276
- const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
1277
2379
  const vec1 = {};
1278
2380
  const vec2 = {};
1279
- for (const word of vocab) {
1280
- vec1[word] = words1.filter((w) => w === word).length;
1281
- vec2[word] = words2.filter((w) => w === word).length;
2381
+ for (const word of words1) {
2382
+ vec1[word] = (vec1[word] ?? 0) + 1;
2383
+ }
2384
+ for (const word of words2) {
2385
+ vec2[word] = (vec2[word] ?? 0) + 1;
1282
2386
  }
2387
+ const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
1283
2388
  let dotProduct = 0;
1284
2389
  let mag1 = 0;
1285
2390
  let mag2 = 0;
@@ -1295,9 +2400,6 @@ function createSleepCompressor() {
1295
2400
  if (mag1 === 0 || mag2 === 0) return 0;
1296
2401
  return dotProduct / (mag1 * mag2);
1297
2402
  }
1298
- function tokenize(text) {
1299
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
1300
- }
1301
2403
  return {
1302
2404
  consolidate,
1303
2405
  findDuplicates,
@@ -1305,18 +2407,164 @@ function createSleepCompressor() {
1305
2407
  };
1306
2408
  }
1307
2409
 
2410
+ // src/core/workflow-planner.ts
2411
+ import { randomUUID as randomUUID5 } from "crypto";
2412
+ import { existsSync as existsSync5, mkdirSync, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync } from "fs";
2413
+ import { join as join4 } from "path";
2414
+ function createWorkflowPlanner(projectRoot) {
2415
+ const plansDir = join4(projectRoot, ".sparn", "plans");
2416
+ if (!existsSync5(plansDir)) {
2417
+ mkdirSync(plansDir, { recursive: true });
2418
+ }
2419
+ function sanitizeId(id) {
2420
+ return id.replace(/[/\\:.]/g, "").replace(/\.\./g, "");
2421
+ }
2422
+ function planPath(planId) {
2423
+ const safeId = sanitizeId(planId);
2424
+ if (!safeId) throw new Error("Invalid plan ID");
2425
+ return join4(plansDir, `plan-${safeId}.json`);
2426
+ }
2427
+ async function createPlan(taskDescription, filesNeeded, searchQueries, steps, tokenBudget = { planning: 0, estimated_execution: 0, max_file_reads: 5 }) {
2428
+ const id = randomUUID5().split("-")[0] || "plan";
2429
+ const plan = {
2430
+ id,
2431
+ created_at: Date.now(),
2432
+ task_description: taskDescription,
2433
+ steps: steps.map((s) => ({ ...s, status: "pending" })),
2434
+ token_budget: tokenBudget,
2435
+ files_needed: filesNeeded,
2436
+ search_queries: searchQueries,
2437
+ status: "draft"
2438
+ };
2439
+ writeFileSync(planPath(id), JSON.stringify(plan, null, 2), "utf-8");
2440
+ return plan;
2441
+ }
2442
+ async function loadPlan(planId) {
2443
+ const path = planPath(planId);
2444
+ if (!existsSync5(path)) return null;
2445
+ try {
2446
+ return JSON.parse(readFileSync4(path, "utf-8"));
2447
+ } catch {
2448
+ return null;
2449
+ }
2450
+ }
2451
+ async function listPlans() {
2452
+ if (!existsSync5(plansDir)) return [];
2453
+ const files = readdirSync4(plansDir).filter((f) => f.startsWith("plan-") && f.endsWith(".json"));
2454
+ const plans = [];
2455
+ for (const file of files) {
2456
+ try {
2457
+ const plan = JSON.parse(readFileSync4(join4(plansDir, file), "utf-8"));
2458
+ plans.push({
2459
+ id: plan.id,
2460
+ task: plan.task_description,
2461
+ status: plan.status,
2462
+ created: plan.created_at
2463
+ });
2464
+ } catch {
2465
+ }
2466
+ }
2467
+ return plans.sort((a, b) => b.created - a.created);
2468
+ }
2469
+ async function updateStep(planId, stepOrder, status, result) {
2470
+ const plan = await loadPlan(planId);
2471
+ if (!plan) throw new Error(`Plan ${planId} not found`);
2472
+ const step = plan.steps.find((s) => s.order === stepOrder);
2473
+ if (!step) throw new Error(`Step ${stepOrder} not found in plan ${planId}`);
2474
+ step.status = status;
2475
+ if (result !== void 0) {
2476
+ step.result = result;
2477
+ }
2478
+ writeFileSync(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
2479
+ }
2480
+ async function startExec(planId) {
2481
+ const plan = await loadPlan(planId);
2482
+ if (!plan) throw new Error(`Plan ${planId} not found`);
2483
+ plan.status = "executing";
2484
+ writeFileSync(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
2485
+ return {
2486
+ maxFileReads: plan.token_budget.max_file_reads,
2487
+ tokenBudget: plan.token_budget.estimated_execution,
2488
+ allowReplan: false
2489
+ };
2490
+ }
2491
+ async function verify(planId) {
2492
+ const plan = await loadPlan(planId);
2493
+ if (!plan) throw new Error(`Plan ${planId} not found`);
2494
+ let stepsCompleted = 0;
2495
+ let stepsFailed = 0;
2496
+ let stepsSkipped = 0;
2497
+ let tokensUsed = 0;
2498
+ const details = [];
2499
+ for (const step of plan.steps) {
2500
+ switch (step.status) {
2501
+ case "completed":
2502
+ stepsCompleted++;
2503
+ tokensUsed += step.estimated_tokens;
2504
+ break;
2505
+ case "failed":
2506
+ stepsFailed++;
2507
+ break;
2508
+ case "skipped":
2509
+ stepsSkipped++;
2510
+ break;
2511
+ default:
2512
+ break;
2513
+ }
2514
+ details.push({
2515
+ step: step.order,
2516
+ action: step.action,
2517
+ target: step.target,
2518
+ status: step.status
2519
+ });
2520
+ }
2521
+ const totalSteps = plan.steps.length;
2522
+ const success = stepsFailed === 0 && stepsCompleted === totalSteps;
2523
+ const hasInProgress = plan.steps.some(
2524
+ (s) => s.status === "pending" || s.status === "in_progress"
2525
+ );
2526
+ if (!hasInProgress) {
2527
+ plan.status = success ? "completed" : "failed";
2528
+ writeFileSync(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
2529
+ }
2530
+ return {
2531
+ planId,
2532
+ stepsCompleted,
2533
+ stepsFailed,
2534
+ stepsSkipped,
2535
+ totalSteps,
2536
+ tokensBudgeted: plan.token_budget.estimated_execution,
2537
+ tokensUsed,
2538
+ success,
2539
+ details
2540
+ };
2541
+ }
2542
+ function getPlansDir() {
2543
+ return plansDir;
2544
+ }
2545
+ return {
2546
+ createPlan,
2547
+ loadPlan,
2548
+ listPlans,
2549
+ updateStep,
2550
+ startExec,
2551
+ verify,
2552
+ getPlansDir
2553
+ };
2554
+ }
2555
+
1308
2556
  // src/daemon/daemon-process.ts
1309
- import { fork } from "child_process";
1310
- import { existsSync as existsSync2, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
1311
- import { dirname, join } from "path";
2557
+ import { spawn } from "child_process";
2558
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
2559
+ import { dirname, join as join5 } from "path";
1312
2560
  import { fileURLToPath } from "url";
1313
2561
  function createDaemonCommand() {
1314
2562
  function isDaemonRunning(pidFile) {
1315
- if (!existsSync2(pidFile)) {
2563
+ if (!existsSync6(pidFile)) {
1316
2564
  return { running: false };
1317
2565
  }
1318
2566
  try {
1319
- const pidStr = readFileSync(pidFile, "utf-8").trim();
2567
+ const pidStr = readFileSync5(pidFile, "utf-8").trim();
1320
2568
  const pid = Number.parseInt(pidStr, 10);
1321
2569
  if (Number.isNaN(pid)) {
1322
2570
  return { running: false };
@@ -1334,13 +2582,13 @@ function createDaemonCommand() {
1334
2582
  }
1335
2583
  function writePidFile(pidFile, pid) {
1336
2584
  const dir = dirname(pidFile);
1337
- if (!existsSync2(dir)) {
1338
- mkdirSync(dir, { recursive: true });
2585
+ if (!existsSync6(dir)) {
2586
+ mkdirSync2(dir, { recursive: true });
1339
2587
  }
1340
- writeFileSync(pidFile, String(pid), "utf-8");
2588
+ writeFileSync2(pidFile, String(pid), "utf-8");
1341
2589
  }
1342
2590
  function removePidFile(pidFile) {
1343
- if (existsSync2(pidFile)) {
2591
+ if (existsSync6(pidFile)) {
1344
2592
  unlinkSync(pidFile);
1345
2593
  }
1346
2594
  }
@@ -1358,16 +2606,60 @@ function createDaemonCommand() {
1358
2606
  try {
1359
2607
  const __filename2 = fileURLToPath(import.meta.url);
1360
2608
  const __dirname2 = dirname(__filename2);
1361
- const daemonPath = join(__dirname2, "index.js");
1362
- const child = fork(daemonPath, [], {
2609
+ const daemonPath = join5(__dirname2, "..", "daemon", "index.js");
2610
+ const isWindows = process.platform === "win32";
2611
+ const childEnv = {
2612
+ ...process.env,
2613
+ SPARN_CONFIG: JSON.stringify(config),
2614
+ SPARN_PID_FILE: pidFile,
2615
+ SPARN_LOG_FILE: logFile
2616
+ };
2617
+ if (isWindows) {
2618
+ const configFile = join5(dirname(pidFile), "daemon-config.json");
2619
+ writeFileSync2(configFile, JSON.stringify({ config, pidFile, logFile }), "utf-8");
2620
+ const launcherFile = join5(dirname(pidFile), "daemon-launcher.mjs");
2621
+ const launcherCode = [
2622
+ `import { readFileSync } from 'node:fs';`,
2623
+ `const cfg = JSON.parse(readFileSync(${JSON.stringify(configFile)}, 'utf-8'));`,
2624
+ `process.env.SPARN_CONFIG = JSON.stringify(cfg.config);`,
2625
+ `process.env.SPARN_PID_FILE = cfg.pidFile;`,
2626
+ `process.env.SPARN_LOG_FILE = cfg.logFile;`,
2627
+ `await import(${JSON.stringify(`file:///${daemonPath.replace(/\\/g, "/")}`)});`
2628
+ ].join("\n");
2629
+ writeFileSync2(launcherFile, launcherCode, "utf-8");
2630
+ const ps = spawn(
2631
+ "powershell.exe",
2632
+ [
2633
+ "-NoProfile",
2634
+ "-WindowStyle",
2635
+ "Hidden",
2636
+ "-Command",
2637
+ `Start-Process -FilePath '${process.execPath}' -ArgumentList '${launcherFile}' -WindowStyle Hidden`
2638
+ ],
2639
+ { stdio: "ignore", windowsHide: true }
2640
+ );
2641
+ ps.unref();
2642
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3));
2643
+ if (existsSync6(pidFile)) {
2644
+ const pid = Number.parseInt(readFileSync5(pidFile, "utf-8").trim(), 10);
2645
+ if (!Number.isNaN(pid)) {
2646
+ return {
2647
+ success: true,
2648
+ pid,
2649
+ message: `Daemon started (PID ${pid})`
2650
+ };
2651
+ }
2652
+ }
2653
+ return {
2654
+ success: false,
2655
+ message: "Daemon failed to start (no PID file written)",
2656
+ error: "Timeout waiting for daemon PID"
2657
+ };
2658
+ }
2659
+ const child = spawn(process.execPath, [daemonPath], {
1363
2660
  detached: true,
1364
2661
  stdio: "ignore",
1365
- env: {
1366
- ...process.env,
1367
- SPARN_CONFIG: JSON.stringify(config),
1368
- SPARN_PID_FILE: pidFile,
1369
- SPARN_LOG_FILE: logFile
1370
- }
2662
+ env: childEnv
1371
2663
  });
1372
2664
  child.unref();
1373
2665
  if (child.pid) {
@@ -1401,14 +2693,19 @@ function createDaemonCommand() {
1401
2693
  };
1402
2694
  }
1403
2695
  try {
1404
- process.kill(status2.pid, "SIGTERM");
2696
+ const isWindows = process.platform === "win32";
2697
+ if (isWindows) {
2698
+ process.kill(status2.pid);
2699
+ } else {
2700
+ process.kill(status2.pid, "SIGTERM");
2701
+ }
1405
2702
  const maxWait = 5e3;
1406
2703
  const interval = 100;
1407
2704
  let waited = 0;
1408
2705
  while (waited < maxWait) {
1409
2706
  try {
1410
2707
  process.kill(status2.pid, 0);
1411
- await new Promise((resolve) => setTimeout(resolve, interval));
2708
+ await new Promise((resolve2) => setTimeout(resolve2, interval));
1412
2709
  waited += interval;
1413
2710
  } catch {
1414
2711
  removePidFile(pidFile);
@@ -1419,7 +2716,9 @@ function createDaemonCommand() {
1419
2716
  }
1420
2717
  }
1421
2718
  try {
1422
- process.kill(status2.pid, "SIGKILL");
2719
+ if (!isWindows) {
2720
+ process.kill(status2.pid, "SIGKILL");
2721
+ }
1423
2722
  removePidFile(pidFile);
1424
2723
  return {
1425
2724
  success: true,
@@ -1449,13 +2748,9 @@ function createDaemonCommand() {
1449
2748
  message: "Daemon not running"
1450
2749
  };
1451
2750
  }
1452
- const metrics = getMetrics().getSnapshot();
1453
2751
  return {
1454
2752
  running: true,
1455
2753
  pid: daemonStatus.pid,
1456
- uptime: metrics.daemon.uptime,
1457
- sessionsWatched: metrics.daemon.sessionsWatched,
1458
- tokensSaved: metrics.optimization.totalTokensSaved,
1459
2754
  message: `Daemon running (PID ${daemonStatus.pid})`
1460
2755
  };
1461
2756
  }
@@ -1467,12 +2762,12 @@ function createDaemonCommand() {
1467
2762
  }
1468
2763
 
1469
2764
  // src/daemon/file-tracker.ts
1470
- import { readFileSync as readFileSync2, statSync } from "fs";
2765
+ import { closeSync, openSync, readSync, statSync as statSync3 } from "fs";
1471
2766
  function createFileTracker() {
1472
2767
  const positions = /* @__PURE__ */ new Map();
1473
2768
  function readNewLines(filePath) {
1474
2769
  try {
1475
- const stats = statSync(filePath);
2770
+ const stats = statSync3(filePath);
1476
2771
  const currentSize = stats.size;
1477
2772
  const currentModified = stats.mtimeMs;
1478
2773
  let pos = positions.get(filePath);
@@ -1493,9 +2788,14 @@ function createFileTracker() {
1493
2788
  }
1494
2789
  return [];
1495
2790
  }
1496
- const buffer = Buffer.alloc(currentSize - pos.position);
1497
- const fd = readFileSync2(filePath);
1498
- fd.copy(buffer, 0, pos.position, currentSize);
2791
+ const bytesToRead = currentSize - pos.position;
2792
+ const buffer = Buffer.alloc(bytesToRead);
2793
+ const fd = openSync(filePath, "r");
2794
+ try {
2795
+ readSync(fd, buffer, 0, bytesToRead, pos.position);
2796
+ } finally {
2797
+ closeSync(fd);
2798
+ }
1499
2799
  const newContent = (pos.partialLine + buffer.toString("utf-8")).split("\n");
1500
2800
  const partialLine = newContent.pop() || "";
1501
2801
  pos.position = currentSize;
@@ -1529,9 +2829,17 @@ function createFileTracker() {
1529
2829
  }
1530
2830
 
1531
2831
  // src/daemon/session-watcher.ts
1532
- import { readdirSync, statSync as statSync2, watch } from "fs";
2832
+ import {
2833
+ existsSync as existsSync7,
2834
+ mkdirSync as mkdirSync3,
2835
+ readdirSync as readdirSync5,
2836
+ readFileSync as readFileSync6,
2837
+ statSync as statSync4,
2838
+ watch,
2839
+ writeFileSync as writeFileSync3
2840
+ } from "fs";
1533
2841
  import { homedir } from "os";
1534
- import { dirname as dirname2, join as join2 } from "path";
2842
+ import { dirname as dirname2, join as join6 } from "path";
1535
2843
  function createSessionWatcher(config) {
1536
2844
  const { config: sparnConfig, onOptimize, onError } = config;
1537
2845
  const { realtime, decay, states } = sparnConfig;
@@ -1540,7 +2848,7 @@ function createSessionWatcher(config) {
1540
2848
  const watchers = [];
1541
2849
  const debounceTimers = /* @__PURE__ */ new Map();
1542
2850
  function getProjectsDir() {
1543
- return join2(homedir(), ".claude", "projects");
2851
+ return join6(homedir(), ".claude", "projects");
1544
2852
  }
1545
2853
  function getSessionId(filePath) {
1546
2854
  const filename = filePath.split(/[/\\]/).pop() || "";
@@ -1557,6 +2865,7 @@ function createSessionWatcher(config) {
1557
2865
  fullOptimizationInterval: 50
1558
2866
  // Full re-optimization every 50 incremental updates
1559
2867
  });
2868
+ loadState(sessionId, pipeline);
1560
2869
  pipelines.set(sessionId, pipeline);
1561
2870
  }
1562
2871
  return pipeline;
@@ -1598,16 +2907,16 @@ function createSessionWatcher(config) {
1598
2907
  function findJsonlFiles(dir) {
1599
2908
  const files = [];
1600
2909
  try {
1601
- const entries = readdirSync(dir);
2910
+ const entries = readdirSync5(dir);
1602
2911
  for (const entry of entries) {
1603
- const fullPath = join2(dir, entry);
1604
- const stat = statSync2(fullPath);
2912
+ const fullPath = join6(dir, entry);
2913
+ const stat = statSync4(fullPath);
1605
2914
  if (stat.isDirectory()) {
1606
2915
  files.push(...findJsonlFiles(fullPath));
1607
2916
  } else if (entry.endsWith(".jsonl")) {
1608
2917
  const matches = realtime.watchPatterns.some((pattern) => {
1609
2918
  const regex = new RegExp(
1610
- pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/\\\\]*").replace(/\./g, "\\.")
2919
+ pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/(?<!\.)(\*)/g, "[^/\\\\]*")
1611
2920
  );
1612
2921
  return regex.test(fullPath);
1613
2922
  });
@@ -1636,34 +2945,69 @@ function createSessionWatcher(config) {
1636
2945
  async function start() {
1637
2946
  const projectsDir = getProjectsDir();
1638
2947
  const jsonlFiles = findJsonlFiles(projectsDir);
1639
- const watchedDirs = /* @__PURE__ */ new Set();
1640
- for (const file of jsonlFiles) {
1641
- const dir = dirname2(file);
1642
- if (!watchedDirs.has(dir)) {
1643
- const watcher = watch(dir, { recursive: false }, (_eventType, filename) => {
1644
- if (filename?.endsWith(".jsonl")) {
1645
- const fullPath = join2(dir, filename);
1646
- handleFileChange(fullPath);
1647
- }
1648
- });
1649
- watchers.push(watcher);
1650
- watchedDirs.add(dir);
2948
+ try {
2949
+ const projectsWatcher = watch(projectsDir, { recursive: true }, (_eventType, filename) => {
2950
+ if (filename?.endsWith(".jsonl")) {
2951
+ const fullPath = join6(projectsDir, filename);
2952
+ handleFileChange(fullPath);
2953
+ }
2954
+ });
2955
+ watchers.push(projectsWatcher);
2956
+ } catch {
2957
+ const watchedDirs = /* @__PURE__ */ new Set();
2958
+ for (const file of jsonlFiles) {
2959
+ const dir = dirname2(file);
2960
+ if (!watchedDirs.has(dir)) {
2961
+ const watcher = watch(dir, { recursive: false }, (_eventType, filename) => {
2962
+ if (filename?.endsWith(".jsonl")) {
2963
+ const fullPath = join6(dir, filename);
2964
+ handleFileChange(fullPath);
2965
+ }
2966
+ });
2967
+ watchers.push(watcher);
2968
+ watchedDirs.add(dir);
2969
+ }
1651
2970
  }
1652
2971
  }
1653
- const projectsWatcher = watch(projectsDir, { recursive: true }, (_eventType, filename) => {
1654
- if (filename?.endsWith(".jsonl")) {
1655
- const fullPath = join2(projectsDir, filename);
1656
- handleFileChange(fullPath);
1657
- }
1658
- });
1659
- watchers.push(projectsWatcher);
1660
2972
  getMetrics().updateDaemon({
1661
2973
  startTime: Date.now(),
1662
2974
  sessionsWatched: jsonlFiles.length,
1663
2975
  memoryUsage: process.memoryUsage().heapUsed
1664
2976
  });
1665
2977
  }
2978
+ function getStatePath() {
2979
+ return join6(homedir(), ".sparn", "optimizer-state.json");
2980
+ }
2981
+ function saveState() {
2982
+ try {
2983
+ const stateMap = {};
2984
+ for (const [sessionId, pipeline] of pipelines.entries()) {
2985
+ stateMap[sessionId] = pipeline.serializeOptimizerState();
2986
+ }
2987
+ const statePath = getStatePath();
2988
+ const dir = dirname2(statePath);
2989
+ if (!existsSync7(dir)) {
2990
+ mkdirSync3(dir, { recursive: true });
2991
+ }
2992
+ writeFileSync3(statePath, JSON.stringify(stateMap), "utf-8");
2993
+ } catch {
2994
+ }
2995
+ }
2996
+ function loadState(sessionId, pipeline) {
2997
+ try {
2998
+ const statePath = getStatePath();
2999
+ if (!existsSync7(statePath)) return;
3000
+ const raw = readFileSync6(statePath, "utf-8");
3001
+ const stateMap = JSON.parse(raw);
3002
+ const sessionState = stateMap[sessionId];
3003
+ if (sessionState) {
3004
+ pipeline.deserializeOptimizerState(sessionState);
3005
+ }
3006
+ } catch {
3007
+ }
3008
+ }
1666
3009
  function stop() {
3010
+ saveState();
1667
3011
  for (const watcher of watchers) {
1668
3012
  watcher.close();
1669
3013
  }
@@ -1750,11 +3094,12 @@ function createSparnMcpServer(options) {
1750
3094
  const { memory, config = DEFAULT_CONFIG } = options;
1751
3095
  const server = new McpServer({
1752
3096
  name: "sparn",
1753
- version: "1.1.1"
3097
+ version: "1.4.0"
1754
3098
  });
1755
3099
  registerOptimizeTool(server, memory, config);
1756
3100
  registerStatsTool(server, memory);
1757
3101
  registerConsolidateTool(server, memory);
3102
+ registerSearchTool(server, memory);
1758
3103
  return server;
1759
3104
  }
1760
3105
  function registerOptimizeTool(server, memory, config) {
@@ -1762,7 +3107,7 @@ function registerOptimizeTool(server, memory, config) {
1762
3107
  "sparn_optimize",
1763
3108
  {
1764
3109
  title: "Sparn Optimize",
1765
- 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.",
3110
+ 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.",
1766
3111
  inputSchema: {
1767
3112
  context: z.string().describe("The context text to optimize"),
1768
3113
  dryRun: z.boolean().optional().default(false).describe("If true, do not persist changes to the memory store"),
@@ -1892,12 +3237,58 @@ function registerStatsTool(server, memory) {
1892
3237
  }
1893
3238
  );
1894
3239
  }
3240
+ function registerSearchTool(server, memory) {
3241
+ server.registerTool(
3242
+ "sparn_search",
3243
+ {
3244
+ title: "Sparn Search",
3245
+ description: "Search memory entries using full-text search. Returns matching entries with relevance ranking, score, and state information.",
3246
+ inputSchema: {
3247
+ query: z.string().describe("Search query text"),
3248
+ limit: z.number().int().min(1).max(100).optional().default(10).describe("Maximum number of results (1-100, default 10)")
3249
+ }
3250
+ },
3251
+ async ({ query, limit }) => {
3252
+ try {
3253
+ const results = await memory.searchFTS(query, limit);
3254
+ const response = results.map((r) => ({
3255
+ id: r.entry.id,
3256
+ content: r.entry.content.length > 500 ? `${r.entry.content.slice(0, 500)}...` : r.entry.content,
3257
+ score: r.entry.score,
3258
+ state: r.entry.state,
3259
+ rank: r.rank,
3260
+ tags: r.entry.tags,
3261
+ isBTSP: r.entry.isBTSP
3262
+ }));
3263
+ return {
3264
+ content: [
3265
+ {
3266
+ type: "text",
3267
+ text: JSON.stringify({ results: response, total: response.length }, null, 2)
3268
+ }
3269
+ ]
3270
+ };
3271
+ } catch (error) {
3272
+ const message = error instanceof Error ? error.message : String(error);
3273
+ return {
3274
+ content: [
3275
+ {
3276
+ type: "text",
3277
+ text: JSON.stringify({ error: message })
3278
+ }
3279
+ ],
3280
+ isError: true
3281
+ };
3282
+ }
3283
+ }
3284
+ );
3285
+ }
1895
3286
  function registerConsolidateTool(server, memory) {
1896
3287
  server.registerTool(
1897
3288
  "sparn_consolidate",
1898
3289
  {
1899
3290
  title: "Sparn Consolidate",
1900
- 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."
3291
+ description: "Run memory consolidation. Removes decayed entries and merges duplicates to reclaim space."
1901
3292
  },
1902
3293
  async () => {
1903
3294
  try {
@@ -1972,6 +3363,10 @@ function createLogger(verbose = false) {
1972
3363
  }
1973
3364
  export {
1974
3365
  DEFAULT_CONFIG,
3366
+ calculateIDF,
3367
+ calculateTF,
3368
+ calculateTFIDF,
3369
+ countTokensPrecise,
1975
3370
  createBTSPEmbedder,
1976
3371
  createBudgetPruner,
1977
3372
  createBudgetPrunerFromConfig,
@@ -1979,6 +3374,9 @@ export {
1979
3374
  createConfidenceStates,
1980
3375
  createContextPipeline,
1981
3376
  createDaemonCommand,
3377
+ createDebtTracker,
3378
+ createDependencyGraph,
3379
+ createDocsGenerator,
1982
3380
  createEngramScorer,
1983
3381
  createEntry,
1984
3382
  createFileTracker,
@@ -1986,13 +3384,23 @@ export {
1986
3384
  createIncrementalOptimizer,
1987
3385
  createKVMemory,
1988
3386
  createLogger,
3387
+ createMetricsCollector,
3388
+ createSearchEngine,
1989
3389
  createSessionWatcher,
1990
3390
  createSleepCompressor,
1991
3391
  createSparnMcpServer,
1992
3392
  createSparsePruner,
3393
+ createTFIDFIndex,
3394
+ createWorkflowPlanner,
1993
3395
  estimateTokens,
3396
+ getMetrics,
1994
3397
  hashContent,
1995
3398
  parseClaudeCodeContext,
1996
- parseGenericContext
3399
+ parseGenericContext,
3400
+ parseJSONLContext,
3401
+ parseJSONLLine,
3402
+ scoreTFIDF,
3403
+ setPreciseTokenCounting,
3404
+ tokenize
1997
3405
  };
1998
3406
  //# sourceMappingURL=index.js.map