@ulrichc1/sparn 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/PRIVACY.md +1 -1
  2. package/README.md +136 -642
  3. package/SECURITY.md +1 -1
  4. package/dist/cli/dashboard.cjs +3977 -0
  5. package/dist/cli/dashboard.cjs.map +1 -0
  6. package/dist/cli/dashboard.d.cts +17 -0
  7. package/dist/cli/dashboard.d.ts +17 -0
  8. package/dist/cli/dashboard.js +3932 -0
  9. package/dist/cli/dashboard.js.map +1 -0
  10. package/dist/cli/index.cjs +3855 -486
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +3812 -459
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/daemon/index.cjs +411 -99
  15. package/dist/daemon/index.cjs.map +1 -1
  16. package/dist/daemon/index.js +423 -103
  17. package/dist/daemon/index.js.map +1 -1
  18. package/dist/hooks/post-tool-result.cjs +129 -225
  19. package/dist/hooks/post-tool-result.cjs.map +1 -1
  20. package/dist/hooks/post-tool-result.js +129 -225
  21. package/dist/hooks/post-tool-result.js.map +1 -1
  22. package/dist/hooks/pre-prompt.cjs +206 -242
  23. package/dist/hooks/pre-prompt.cjs.map +1 -1
  24. package/dist/hooks/pre-prompt.js +192 -243
  25. package/dist/hooks/pre-prompt.js.map +1 -1
  26. package/dist/hooks/stop-docs-refresh.cjs +123 -0
  27. package/dist/hooks/stop-docs-refresh.cjs.map +1 -0
  28. package/dist/hooks/stop-docs-refresh.d.cts +1 -0
  29. package/dist/hooks/stop-docs-refresh.d.ts +1 -0
  30. package/dist/hooks/stop-docs-refresh.js +126 -0
  31. package/dist/hooks/stop-docs-refresh.js.map +1 -0
  32. package/dist/index.cjs +1756 -339
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +540 -41
  35. package/dist/index.d.ts +540 -41
  36. package/dist/index.js +1739 -331
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp/index.cjs +306 -73
  39. package/dist/mcp/index.cjs.map +1 -1
  40. package/dist/mcp/index.js +310 -73
  41. package/dist/mcp/index.js.map +1 -1
  42. package/package.json +10 -3
package/dist/index.cjs CHANGED
@@ -31,6 +31,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  DEFAULT_CONFIG: () => DEFAULT_CONFIG,
34
+ calculateIDF: () => calculateIDF,
35
+ calculateTF: () => calculateTF,
36
+ calculateTFIDF: () => calculateTFIDF,
37
+ countTokensPrecise: () => countTokensPrecise,
34
38
  createBTSPEmbedder: () => createBTSPEmbedder,
35
39
  createBudgetPruner: () => createBudgetPruner,
36
40
  createBudgetPrunerFromConfig: () => createBudgetPrunerFromConfig,
@@ -38,6 +42,9 @@ __export(src_exports, {
38
42
  createConfidenceStates: () => createConfidenceStates,
39
43
  createContextPipeline: () => createContextPipeline,
40
44
  createDaemonCommand: () => createDaemonCommand,
45
+ createDebtTracker: () => createDebtTracker,
46
+ createDependencyGraph: () => createDependencyGraph,
47
+ createDocsGenerator: () => createDocsGenerator,
41
48
  createEngramScorer: () => createEngramScorer,
42
49
  createEntry: () => createEntry,
43
50
  createFileTracker: () => createFileTracker,
@@ -45,14 +52,24 @@ __export(src_exports, {
45
52
  createIncrementalOptimizer: () => createIncrementalOptimizer,
46
53
  createKVMemory: () => createKVMemory,
47
54
  createLogger: () => createLogger,
55
+ createMetricsCollector: () => createMetricsCollector,
56
+ createSearchEngine: () => createSearchEngine,
48
57
  createSessionWatcher: () => createSessionWatcher,
49
58
  createSleepCompressor: () => createSleepCompressor,
50
59
  createSparnMcpServer: () => createSparnMcpServer,
51
60
  createSparsePruner: () => createSparsePruner,
61
+ createTFIDFIndex: () => createTFIDFIndex,
62
+ createWorkflowPlanner: () => createWorkflowPlanner,
52
63
  estimateTokens: () => estimateTokens,
64
+ getMetrics: () => getMetrics,
53
65
  hashContent: () => hashContent,
54
66
  parseClaudeCodeContext: () => parseClaudeCodeContext,
55
- parseGenericContext: () => parseGenericContext
67
+ parseGenericContext: () => parseGenericContext,
68
+ parseJSONLContext: () => parseJSONLContext,
69
+ parseJSONLLine: () => parseJSONLLine,
70
+ scoreTFIDF: () => scoreTFIDF,
71
+ setPreciseTokenCounting: () => setPreciseTokenCounting,
72
+ tokenize: () => tokenize
56
73
  });
57
74
  module.exports = __toCommonJS(src_exports);
58
75
 
@@ -70,7 +87,7 @@ function hashContent(content) {
70
87
  }
71
88
 
72
89
  // src/core/btsp-embedder.ts
73
- function createBTSPEmbedder() {
90
+ function createBTSPEmbedder(config) {
74
91
  const BTSP_PATTERNS = [
75
92
  // Error patterns
76
93
  /\b(error|exception|failure|fatal|critical|panic)\b/i,
@@ -89,6 +106,14 @@ function createBTSPEmbedder() {
89
106
  /^=======/m,
90
107
  /^>>>>>>> /m
91
108
  ];
109
+ if (config?.customPatterns) {
110
+ for (const pattern of config.customPatterns) {
111
+ try {
112
+ BTSP_PATTERNS.push(new RegExp(pattern));
113
+ } catch {
114
+ }
115
+ }
116
+ }
92
117
  function detectBTSP(content) {
93
118
  return BTSP_PATTERNS.some((pattern) => pattern.test(content));
94
119
  }
@@ -116,51 +141,96 @@ function createBTSPEmbedder() {
116
141
  };
117
142
  }
118
143
 
119
- // src/core/confidence-states.ts
120
- function createConfidenceStates(config) {
121
- const { activeThreshold, readyThreshold } = config;
122
- function calculateState(entry) {
123
- if (entry.isBTSP) {
124
- return "active";
125
- }
126
- if (entry.score > activeThreshold) {
127
- return "active";
128
- }
129
- if (entry.score >= readyThreshold) {
130
- return "ready";
131
- }
132
- return "silent";
133
- }
134
- function transition(entry) {
135
- const newState = calculateState(entry);
136
- return {
137
- ...entry,
138
- state: newState
139
- };
140
- }
141
- function getDistribution(entries) {
142
- const distribution = {
143
- silent: 0,
144
- ready: 0,
145
- active: 0,
146
- total: entries.length
147
- };
148
- for (const entry of entries) {
149
- const state = calculateState(entry);
150
- distribution[state]++;
144
+ // src/utils/tfidf.ts
145
+ function tokenize(text) {
146
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
147
+ }
148
+ function calculateTF(term, tokens) {
149
+ const count = tokens.filter((t) => t === term).length;
150
+ return Math.sqrt(count);
151
+ }
152
+ function calculateIDF(term, allEntries) {
153
+ const totalDocs = allEntries.length;
154
+ const docsWithTerm = allEntries.filter((entry) => {
155
+ const tokens = tokenize(entry.content);
156
+ return tokens.includes(term);
157
+ }).length;
158
+ if (docsWithTerm === 0) return 0;
159
+ return Math.log(totalDocs / docsWithTerm);
160
+ }
161
+ function calculateTFIDF(entry, allEntries) {
162
+ const tokens = tokenize(entry.content);
163
+ if (tokens.length === 0) return 0;
164
+ const uniqueTerms = [...new Set(tokens)];
165
+ let totalScore = 0;
166
+ for (const term of uniqueTerms) {
167
+ const tf = calculateTF(term, tokens);
168
+ const idf = calculateIDF(term, allEntries);
169
+ totalScore += tf * idf;
170
+ }
171
+ return totalScore / tokens.length;
172
+ }
173
+ function createTFIDFIndex(entries) {
174
+ const documentFrequency = /* @__PURE__ */ new Map();
175
+ for (const entry of entries) {
176
+ const tokens = tokenize(entry.content);
177
+ const uniqueTerms = new Set(tokens);
178
+ for (const term of uniqueTerms) {
179
+ documentFrequency.set(term, (documentFrequency.get(term) || 0) + 1);
151
180
  }
152
- return distribution;
153
181
  }
154
182
  return {
155
- calculateState,
156
- transition,
157
- getDistribution
183
+ documentFrequency,
184
+ totalDocuments: entries.length
158
185
  };
159
186
  }
187
+ function scoreTFIDF(entry, index) {
188
+ const tokens = tokenize(entry.content);
189
+ if (tokens.length === 0) return 0;
190
+ const uniqueTerms = new Set(tokens);
191
+ let totalScore = 0;
192
+ for (const term of uniqueTerms) {
193
+ const tf = calculateTF(term, tokens);
194
+ const docsWithTerm = index.documentFrequency.get(term) || 0;
195
+ if (docsWithTerm === 0) continue;
196
+ const idf = Math.log(index.totalDocuments / docsWithTerm);
197
+ totalScore += tf * idf;
198
+ }
199
+ return totalScore / tokens.length;
200
+ }
201
+
202
+ // src/utils/tokenizer.ts
203
+ var import_gpt_tokenizer = require("gpt-tokenizer");
204
+ var usePrecise = false;
205
+ function setPreciseTokenCounting(enabled) {
206
+ usePrecise = enabled;
207
+ }
208
+ function countTokensPrecise(text) {
209
+ if (!text || text.length === 0) {
210
+ return 0;
211
+ }
212
+ return (0, import_gpt_tokenizer.encode)(text).length;
213
+ }
214
+ function estimateTokens(text) {
215
+ if (!text || text.length === 0) {
216
+ return 0;
217
+ }
218
+ if (usePrecise) {
219
+ return (0, import_gpt_tokenizer.encode)(text).length;
220
+ }
221
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
222
+ const wordCount = words.length;
223
+ const charCount = text.length;
224
+ const charEstimate = Math.ceil(charCount / 4);
225
+ const wordEstimate = Math.ceil(wordCount * 0.75);
226
+ return Math.max(wordEstimate, charEstimate);
227
+ }
160
228
 
161
229
  // src/core/engram-scorer.ts
162
230
  function createEngramScorer(config) {
163
231
  const { defaultTTL } = config;
232
+ const recencyWindowMs = (config.recencyBoostMinutes ?? 30) * 60 * 1e3;
233
+ const recencyMultiplier = config.recencyBoostMultiplier ?? 1.3;
164
234
  function calculateDecay(ageInSeconds, ttlInSeconds) {
165
235
  if (ttlInSeconds === 0) return 1;
166
236
  if (ageInSeconds <= 0) return 0;
@@ -180,6 +250,13 @@ function createEngramScorer(config) {
180
250
  if (entry.isBTSP) {
181
251
  score = Math.max(score, 0.9);
182
252
  }
253
+ if (!entry.isBTSP && recencyWindowMs > 0) {
254
+ const ageMs = currentTime - entry.timestamp;
255
+ if (ageMs >= 0 && ageMs < recencyWindowMs) {
256
+ const boostFactor = 1 + (recencyMultiplier - 1) * (1 - ageMs / recencyWindowMs);
257
+ score = score * boostFactor;
258
+ }
259
+ }
183
260
  return Math.max(0, Math.min(1, score));
184
261
  }
185
262
  function refreshTTL(entry) {
@@ -197,85 +274,151 @@ function createEngramScorer(config) {
197
274
  };
198
275
  }
199
276
 
200
- // src/utils/tokenizer.ts
201
- function estimateTokens(text) {
202
- if (!text || text.length === 0) {
203
- return 0;
204
- }
205
- const words = text.split(/\s+/).filter((w) => w.length > 0);
206
- const wordCount = words.length;
207
- const charCount = text.length;
208
- const charEstimate = Math.ceil(charCount / 4);
209
- const wordEstimate = Math.ceil(wordCount * 0.75);
210
- return Math.max(wordEstimate, charEstimate);
211
- }
212
-
213
- // src/core/sparse-pruner.ts
214
- function createSparsePruner(config) {
215
- const { threshold } = config;
216
- function tokenize(text) {
217
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
218
- }
219
- function calculateTF(term, tokens) {
220
- const count = tokens.filter((t) => t === term).length;
221
- return Math.sqrt(count);
222
- }
223
- function calculateIDF(term, allEntries) {
224
- const totalDocs = allEntries.length;
225
- const docsWithTerm = allEntries.filter((entry) => {
226
- const tokens = tokenize(entry.content);
227
- return tokens.includes(term);
228
- }).length;
229
- if (docsWithTerm === 0) return 0;
230
- return Math.log(totalDocs / docsWithTerm);
231
- }
232
- function scoreEntry(entry, allEntries) {
233
- const tokens = tokenize(entry.content);
234
- if (tokens.length === 0) return 0;
235
- const uniqueTerms = [...new Set(tokens)];
236
- let totalScore = 0;
237
- for (const term of uniqueTerms) {
238
- const tf = calculateTF(term, tokens);
239
- const idf = calculateIDF(term, allEntries);
240
- totalScore += tf * idf;
277
+ // src/core/budget-pruner.ts
278
+ function createBudgetPruner(config) {
279
+ const { tokenBudget, decay } = config;
280
+ const engramScorer = createEngramScorer(decay);
281
+ function getStateMultiplier(entry) {
282
+ if (entry.isBTSP) return 2;
283
+ switch (entry.state) {
284
+ case "active":
285
+ return 2;
286
+ case "ready":
287
+ return 1;
288
+ case "silent":
289
+ return 0.5;
290
+ default:
291
+ return 1;
241
292
  }
242
- return totalScore / tokens.length;
243
293
  }
244
- function prune(entries) {
294
+ function priorityScore(entry, allEntries, index) {
295
+ const tfidf = index ? scoreTFIDF(entry, index) : scoreTFIDF(entry, createTFIDFIndex(allEntries));
296
+ const currentScore = engramScorer.calculateScore(entry);
297
+ const engramDecay = 1 - currentScore;
298
+ const stateMultiplier = getStateMultiplier(entry);
299
+ return tfidf * (1 - engramDecay) * stateMultiplier;
300
+ }
301
+ function pruneToFit(entries, budget = tokenBudget) {
245
302
  if (entries.length === 0) {
246
303
  return {
247
304
  kept: [],
248
305
  removed: [],
249
306
  originalTokens: 0,
250
- prunedTokens: 0
307
+ prunedTokens: 0,
308
+ budgetUtilization: 0
251
309
  };
252
310
  }
253
311
  const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
254
- const scored = entries.map((entry) => ({
312
+ const btspEntries = entries.filter((e) => e.isBTSP);
313
+ const regularEntries = entries.filter((e) => !e.isBTSP);
314
+ let includedBtsp = [];
315
+ let btspTokens = 0;
316
+ const sortedBtsp = [...btspEntries].sort((a, b) => b.timestamp - a.timestamp);
317
+ for (const entry of sortedBtsp) {
318
+ const tokens = estimateTokens(entry.content);
319
+ if (btspTokens + tokens <= budget * 0.8) {
320
+ includedBtsp.push(entry);
321
+ btspTokens += tokens;
322
+ }
323
+ }
324
+ if (includedBtsp.length === 0 && sortedBtsp.length > 0) {
325
+ const firstBtsp = sortedBtsp[0];
326
+ if (firstBtsp) {
327
+ includedBtsp = [firstBtsp];
328
+ btspTokens = estimateTokens(firstBtsp.content);
329
+ }
330
+ }
331
+ const excludedBtsp = btspEntries.filter((e) => !includedBtsp.includes(e));
332
+ const tfidfIndex = createTFIDFIndex(entries);
333
+ const scored = regularEntries.map((entry) => ({
255
334
  entry,
256
- score: scoreEntry(entry, entries)
335
+ score: priorityScore(entry, entries, tfidfIndex),
336
+ tokens: estimateTokens(entry.content)
257
337
  }));
258
338
  scored.sort((a, b) => b.score - a.score);
259
- const keepCount = Math.max(1, Math.ceil(entries.length * (threshold / 100)));
260
- const kept = scored.slice(0, keepCount).map((s) => s.entry);
261
- const removed = scored.slice(keepCount).map((s) => s.entry);
262
- const prunedTokens = kept.reduce((sum, e) => sum + estimateTokens(e.content), 0);
339
+ const kept = [...includedBtsp];
340
+ const removed = [...excludedBtsp];
341
+ let currentTokens = btspTokens;
342
+ for (const item of scored) {
343
+ if (currentTokens + item.tokens <= budget) {
344
+ kept.push(item.entry);
345
+ currentTokens += item.tokens;
346
+ } else {
347
+ removed.push(item.entry);
348
+ }
349
+ }
350
+ const budgetUtilization = budget > 0 ? currentTokens / budget : 0;
263
351
  return {
264
352
  kept,
265
353
  removed,
266
354
  originalTokens,
267
- prunedTokens
355
+ prunedTokens: currentTokens,
356
+ budgetUtilization
268
357
  };
269
358
  }
270
359
  return {
271
- prune,
272
- scoreEntry
360
+ pruneToFit,
361
+ priorityScore
362
+ };
363
+ }
364
+ function createBudgetPrunerFromConfig(realtimeConfig, decayConfig, statesConfig) {
365
+ return createBudgetPruner({
366
+ tokenBudget: realtimeConfig.tokenBudget,
367
+ decay: decayConfig,
368
+ states: statesConfig
369
+ });
370
+ }
371
+
372
+ // src/core/confidence-states.ts
373
+ function createConfidenceStates(config) {
374
+ const { activeThreshold, readyThreshold } = config;
375
+ function calculateState(entry) {
376
+ if (entry.isBTSP) {
377
+ return "active";
378
+ }
379
+ if (entry.score >= activeThreshold) {
380
+ return "active";
381
+ }
382
+ if (entry.score >= readyThreshold) {
383
+ return "ready";
384
+ }
385
+ return "silent";
386
+ }
387
+ function transition(entry) {
388
+ const newState = calculateState(entry);
389
+ return {
390
+ ...entry,
391
+ state: newState
392
+ };
393
+ }
394
+ function getDistribution(entries) {
395
+ const distribution = {
396
+ silent: 0,
397
+ ready: 0,
398
+ active: 0,
399
+ total: entries.length
400
+ };
401
+ for (const entry of entries) {
402
+ const state = calculateState(entry);
403
+ distribution[state]++;
404
+ }
405
+ return distribution;
406
+ }
407
+ return {
408
+ calculateState,
409
+ transition,
410
+ getDistribution
273
411
  };
274
412
  }
275
413
 
276
414
  // src/utils/context-parser.ts
277
415
  var import_node_crypto3 = require("crypto");
278
416
  function parseClaudeCodeContext(context) {
417
+ const firstNonEmpty = context.split("\n").find((line) => line.trim().length > 0);
418
+ if (firstNonEmpty?.trim().startsWith("{")) {
419
+ const jsonlEntries = parseJSONLContext(context);
420
+ if (jsonlEntries.length > 0) return jsonlEntries;
421
+ }
279
422
  const entries = [];
280
423
  const now = Date.now();
281
424
  const lines = context.split("\n");
@@ -328,7 +471,7 @@ function createEntry(content, type, baseTime) {
328
471
  hash: hashContent(content),
329
472
  timestamp: baseTime,
330
473
  score: initialScore,
331
- state: initialScore > 0.7 ? "active" : initialScore > 0.3 ? "ready" : "silent",
474
+ state: initialScore >= 0.7 ? "active" : initialScore >= 0.3 ? "ready" : "silent",
332
475
  ttl: 24 * 3600,
333
476
  // 24 hours default
334
477
  accessCount: 0,
@@ -337,6 +480,58 @@ function createEntry(content, type, baseTime) {
337
480
  isBTSP: false
338
481
  };
339
482
  }
483
+ function parseJSONLLine(line) {
484
+ const trimmed = line.trim();
485
+ if (trimmed.length === 0) return null;
486
+ try {
487
+ return JSON.parse(trimmed);
488
+ } catch {
489
+ return null;
490
+ }
491
+ }
492
+ function extractContent(content) {
493
+ if (typeof content === "string") return content;
494
+ if (Array.isArray(content)) {
495
+ return content.map((block) => {
496
+ if (block.type === "text" && block.text) return block.text;
497
+ if (block.type === "tool_use" && block.name) return `[tool_use: ${block.name}]`;
498
+ if (block.type === "tool_result") {
499
+ if (typeof block.content === "string") return block.content;
500
+ if (Array.isArray(block.content)) {
501
+ return block.content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n");
502
+ }
503
+ }
504
+ return "";
505
+ }).filter((s) => s.length > 0).join("\n");
506
+ }
507
+ return "";
508
+ }
509
+ function classifyJSONLMessage(msg) {
510
+ if (Array.isArray(msg.content)) {
511
+ const hasToolUse = msg.content.some((b) => b.type === "tool_use");
512
+ const hasToolResult = msg.content.some((b) => b.type === "tool_result");
513
+ if (hasToolUse) return "tool";
514
+ if (hasToolResult) return "result";
515
+ }
516
+ if (msg.type === "tool_use" || msg.tool_use) return "tool";
517
+ if (msg.type === "tool_result" || msg.tool_result) return "result";
518
+ if (msg.role === "user" || msg.role === "assistant") return "conversation";
519
+ return "other";
520
+ }
521
+ function parseJSONLContext(context) {
522
+ const entries = [];
523
+ const now = Date.now();
524
+ const lines = context.split("\n");
525
+ for (const line of lines) {
526
+ const msg = parseJSONLLine(line);
527
+ if (!msg) continue;
528
+ const content = extractContent(msg.content);
529
+ if (!content || content.trim().length === 0) continue;
530
+ const blockType = classifyJSONLMessage(msg);
531
+ entries.push(createEntry(content, blockType, now));
532
+ }
533
+ return entries;
534
+ }
340
535
  function parseGenericContext(context) {
341
536
  const entries = [];
342
537
  const now = Date.now();
@@ -351,15 +546,9 @@ function parseGenericContext(context) {
351
546
 
352
547
  // src/adapters/claude-code.ts
353
548
  var CLAUDE_CODE_PROFILE = {
354
- // More aggressive pruning for tool results (they can be verbose)
355
- toolResultThreshold: 3,
356
- // Keep top 3% of tool results
357
549
  // Preserve conversation turns more aggressively
358
550
  conversationBoost: 1.5,
359
551
  // 50% boost for User/Assistant exchanges
360
- // Prioritize recent context (Claude Code sessions are typically focused)
361
- recentContextWindow: 10 * 60,
362
- // Last 10 minutes gets priority
363
552
  // BTSP patterns specific to Claude Code
364
553
  btspPatterns: [
365
554
  // Error patterns
@@ -380,12 +569,14 @@ var CLAUDE_CODE_PROFILE = {
380
569
  ]
381
570
  };
382
571
  function createClaudeCodeAdapter(memory, config) {
383
- const pruner = createSparsePruner({
384
- threshold: config.pruning.threshold
572
+ const pruner = createBudgetPruner({
573
+ tokenBudget: config.realtime.tokenBudget,
574
+ decay: config.decay,
575
+ states: config.states
385
576
  });
386
577
  const scorer = createEngramScorer(config.decay);
387
578
  const states = createConfidenceStates(config.states);
388
- const btsp = createBTSPEmbedder();
579
+ const btsp = createBTSPEmbedder({ customPatterns: config.btspPatterns });
389
580
  async function optimize(context, options = {}) {
390
581
  const startTime = Date.now();
391
582
  const entries = parseClaudeCodeContext(context);
@@ -416,9 +607,11 @@ function createClaudeCodeAdapter(memory, config) {
416
607
  });
417
608
  const scoredEntries = boostedEntries.map((entry) => {
418
609
  const decayScore = scorer.calculateScore(entry);
610
+ const isConversationTurn = entry.content.trim().startsWith("User:") || entry.content.trim().startsWith("Assistant:");
611
+ const finalScore = isConversationTurn ? Math.min(1, decayScore * CLAUDE_CODE_PROFILE.conversationBoost) : decayScore;
419
612
  return {
420
613
  ...entry,
421
- score: decayScore
614
+ score: finalScore
422
615
  };
423
616
  });
424
617
  const entriesWithStates = scoredEntries.map((entry) => {
@@ -428,7 +621,7 @@ function createClaudeCodeAdapter(memory, config) {
428
621
  state
429
622
  };
430
623
  });
431
- const pruneResult = pruner.prune(entriesWithStates);
624
+ const pruneResult = pruner.pruneToFit(entriesWithStates);
432
625
  if (!options.dryRun) {
433
626
  for (const entry of pruneResult.kept) {
434
627
  await memory.put(entry);
@@ -471,29 +664,73 @@ function createClaudeCodeAdapter(memory, config) {
471
664
 
472
665
  // src/adapters/generic.ts
473
666
  var import_node_crypto4 = require("crypto");
667
+
668
+ // src/core/sparse-pruner.ts
669
+ function createSparsePruner(config) {
670
+ const { threshold } = config;
671
+ function scoreEntry(entry, allEntries) {
672
+ return scoreTFIDF(entry, createTFIDFIndex(allEntries));
673
+ }
674
+ function prune(entries) {
675
+ if (entries.length === 0) {
676
+ return {
677
+ kept: [],
678
+ removed: [],
679
+ originalTokens: 0,
680
+ prunedTokens: 0
681
+ };
682
+ }
683
+ const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
684
+ const tfidfIndex = createTFIDFIndex(entries);
685
+ const scored = entries.map((entry) => ({
686
+ entry,
687
+ score: scoreTFIDF(entry, tfidfIndex)
688
+ }));
689
+ scored.sort((a, b) => b.score - a.score);
690
+ const keepCount = Math.max(1, Math.ceil(entries.length * (threshold / 100)));
691
+ const kept = scored.slice(0, keepCount).map((s) => s.entry);
692
+ const removed = scored.slice(keepCount).map((s) => s.entry);
693
+ const prunedTokens = kept.reduce((sum, e) => sum + estimateTokens(e.content), 0);
694
+ return {
695
+ kept,
696
+ removed,
697
+ originalTokens,
698
+ prunedTokens
699
+ };
700
+ }
701
+ return {
702
+ prune,
703
+ scoreEntry
704
+ };
705
+ }
706
+
707
+ // src/adapters/generic.ts
474
708
  function createGenericAdapter(memory, config) {
475
709
  const pruner = createSparsePruner(config.pruning);
476
710
  const scorer = createEngramScorer(config.decay);
477
711
  const states = createConfidenceStates(config.states);
478
- const btsp = createBTSPEmbedder();
712
+ const btsp = createBTSPEmbedder({ customPatterns: config.btspPatterns });
479
713
  async function optimize(context, options = {}) {
480
714
  const startTime = Date.now();
481
715
  const lines = context.split("\n").filter((line) => line.trim().length > 0);
482
- const entries = lines.map((content) => ({
483
- id: (0, import_node_crypto4.randomUUID)(),
484
- content,
485
- hash: hashContent(content),
486
- timestamp: Date.now(),
487
- score: btsp.detectBTSP(content) ? 1 : 0.5,
488
- // BTSP gets high initial score
489
- ttl: config.decay.defaultTTL * 3600,
490
- // Convert hours to seconds
491
- state: "ready",
492
- accessCount: 0,
493
- tags: [],
494
- metadata: {},
495
- isBTSP: btsp.detectBTSP(content)
496
- }));
716
+ const now = Date.now();
717
+ const entries = lines.map((content, index) => {
718
+ const isBTSP = btsp.detectBTSP(content);
719
+ return {
720
+ id: (0, import_node_crypto4.randomUUID)(),
721
+ content,
722
+ hash: hashContent(content),
723
+ timestamp: now + index,
724
+ // Unique timestamps preserve ordering
725
+ score: isBTSP ? 1 : 0.5,
726
+ ttl: config.decay.defaultTTL * 3600,
727
+ state: "ready",
728
+ accessCount: 0,
729
+ tags: [],
730
+ metadata: {},
731
+ isBTSP
732
+ };
733
+ });
497
734
  const tokensBefore = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
498
735
  const scoredEntries = entries.map((entry) => ({
499
736
  ...entry,
@@ -545,111 +782,6 @@ function createGenericAdapter(memory, config) {
545
782
  };
546
783
  }
547
784
 
548
- // src/core/budget-pruner.ts
549
- function createBudgetPruner(config) {
550
- const { tokenBudget, decay } = config;
551
- const engramScorer = createEngramScorer(decay);
552
- function tokenize(text) {
553
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
554
- }
555
- function calculateTF(term, tokens) {
556
- const count = tokens.filter((t) => t === term).length;
557
- return Math.sqrt(count);
558
- }
559
- function calculateIDF(term, allEntries) {
560
- const totalDocs = allEntries.length;
561
- const docsWithTerm = allEntries.filter((entry) => {
562
- const tokens = tokenize(entry.content);
563
- return tokens.includes(term);
564
- }).length;
565
- if (docsWithTerm === 0) return 0;
566
- return Math.log(totalDocs / docsWithTerm);
567
- }
568
- function calculateTFIDF(entry, allEntries) {
569
- const tokens = tokenize(entry.content);
570
- if (tokens.length === 0) return 0;
571
- const uniqueTerms = [...new Set(tokens)];
572
- let totalScore = 0;
573
- for (const term of uniqueTerms) {
574
- const tf = calculateTF(term, tokens);
575
- const idf = calculateIDF(term, allEntries);
576
- totalScore += tf * idf;
577
- }
578
- return totalScore / tokens.length;
579
- }
580
- function getStateMultiplier(entry) {
581
- if (entry.isBTSP) return 2;
582
- switch (entry.state) {
583
- case "active":
584
- return 2;
585
- case "ready":
586
- return 1;
587
- case "silent":
588
- return 0.5;
589
- default:
590
- return 1;
591
- }
592
- }
593
- function priorityScore(entry, allEntries) {
594
- const tfidf = calculateTFIDF(entry, allEntries);
595
- const currentScore = engramScorer.calculateScore(entry);
596
- const engramDecay = 1 - currentScore;
597
- const stateMultiplier = getStateMultiplier(entry);
598
- return tfidf * (1 - engramDecay) * stateMultiplier;
599
- }
600
- function pruneToFit(entries, budget = tokenBudget) {
601
- if (entries.length === 0) {
602
- return {
603
- kept: [],
604
- removed: [],
605
- originalTokens: 0,
606
- prunedTokens: 0,
607
- budgetUtilization: 0
608
- };
609
- }
610
- const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
611
- const btspEntries = entries.filter((e) => e.isBTSP);
612
- const regularEntries = entries.filter((e) => !e.isBTSP);
613
- const btspTokens = btspEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
614
- const scored = regularEntries.map((entry) => ({
615
- entry,
616
- score: priorityScore(entry, entries),
617
- tokens: estimateTokens(entry.content)
618
- }));
619
- scored.sort((a, b) => b.score - a.score);
620
- const kept = [...btspEntries];
621
- const removed = [];
622
- let currentTokens = btspTokens;
623
- for (const item of scored) {
624
- if (currentTokens + item.tokens <= budget) {
625
- kept.push(item.entry);
626
- currentTokens += item.tokens;
627
- } else {
628
- removed.push(item.entry);
629
- }
630
- }
631
- const budgetUtilization = budget > 0 ? currentTokens / budget : 0;
632
- return {
633
- kept,
634
- removed,
635
- originalTokens,
636
- prunedTokens: currentTokens,
637
- budgetUtilization
638
- };
639
- }
640
- return {
641
- pruneToFit,
642
- priorityScore
643
- };
644
- }
645
- function createBudgetPrunerFromConfig(realtimeConfig, decayConfig, statesConfig) {
646
- return createBudgetPruner({
647
- tokenBudget: realtimeConfig.tokenBudget,
648
- decay: decayConfig,
649
- states: statesConfig
650
- });
651
- }
652
-
653
785
  // src/core/metrics.ts
654
786
  function createMetricsCollector() {
655
787
  const optimizations = [];
@@ -683,11 +815,10 @@ function createMetricsCollector() {
683
815
  ...metric
684
816
  };
685
817
  }
686
- function calculatePercentile(values, percentile) {
687
- if (values.length === 0) return 0;
688
- const sorted = [...values].sort((a, b) => a - b);
689
- const index = Math.ceil(percentile / 100 * sorted.length) - 1;
690
- return sorted[index] || 0;
818
+ function calculatePercentile(sortedValues, percentile) {
819
+ if (sortedValues.length === 0) return 0;
820
+ const index = Math.ceil(percentile / 100 * sortedValues.length) - 1;
821
+ return sortedValues[index] || 0;
691
822
  }
692
823
  function getSnapshot() {
693
824
  const totalRuns = optimizations.length;
@@ -698,7 +829,7 @@ function createMetricsCollector() {
698
829
  );
699
830
  const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
700
831
  const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
701
- const durations = optimizations.map((m) => m.duration);
832
+ const sortedDurations = optimizations.map((m) => m.duration).sort((a, b) => a - b);
702
833
  const totalCacheQueries = cacheHits + cacheMisses;
703
834
  const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
704
835
  return {
@@ -708,9 +839,9 @@ function createMetricsCollector() {
708
839
  totalDuration,
709
840
  totalTokensSaved,
710
841
  averageReduction,
711
- p50Latency: calculatePercentile(durations, 50),
712
- p95Latency: calculatePercentile(durations, 95),
713
- p99Latency: calculatePercentile(durations, 99)
842
+ p50Latency: calculatePercentile(sortedDurations, 50),
843
+ p95Latency: calculatePercentile(sortedDurations, 95),
844
+ p99Latency: calculatePercentile(sortedDurations, 99)
714
845
  },
715
846
  cache: {
716
847
  hitRate,
@@ -768,9 +899,6 @@ function createIncrementalOptimizer(config) {
768
899
  updateCount: 0,
769
900
  lastFullOptimization: Date.now()
770
901
  };
771
- function tokenize(text) {
772
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
773
- }
774
902
  function updateDocumentFrequency(entries, remove = false) {
775
903
  for (const entry of entries) {
776
904
  const tokens = tokenize(entry.content);
@@ -793,7 +921,19 @@ function createIncrementalOptimizer(config) {
793
921
  if (!cached) return null;
794
922
  return cached.entry;
795
923
  }
924
+ const MAX_CACHE_SIZE = 1e4;
796
925
  function cacheEntry(entry, score) {
926
+ if (state.entryCache.size >= MAX_CACHE_SIZE) {
927
+ const entries = Array.from(state.entryCache.entries());
928
+ entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
929
+ const toRemove = Math.floor(MAX_CACHE_SIZE * 0.2);
930
+ for (let i = 0; i < toRemove && i < entries.length; i++) {
931
+ const entry2 = entries[i];
932
+ if (entry2) {
933
+ state.entryCache.delete(entry2[0]);
934
+ }
935
+ }
936
+ }
797
937
  state.entryCache.set(entry.hash, {
798
938
  entry,
799
939
  score,
@@ -920,13 +1060,40 @@ function createIncrementalOptimizer(config) {
920
1060
  lastFullOptimization: state.lastFullOptimization
921
1061
  };
922
1062
  }
1063
+ function serializeState() {
1064
+ const s = getState();
1065
+ return JSON.stringify({
1066
+ entryCache: Array.from(s.entryCache.entries()),
1067
+ documentFrequency: Array.from(s.documentFrequency.entries()),
1068
+ totalDocuments: s.totalDocuments,
1069
+ updateCount: s.updateCount,
1070
+ lastFullOptimization: s.lastFullOptimization
1071
+ });
1072
+ }
1073
+ function deserializeState(json) {
1074
+ try {
1075
+ const parsed = JSON.parse(json);
1076
+ restoreState({
1077
+ entryCache: new Map(parsed.entryCache),
1078
+ documentFrequency: new Map(parsed.documentFrequency),
1079
+ totalDocuments: parsed.totalDocuments,
1080
+ updateCount: parsed.updateCount,
1081
+ lastFullOptimization: parsed.lastFullOptimization
1082
+ });
1083
+ return true;
1084
+ } catch {
1085
+ return false;
1086
+ }
1087
+ }
923
1088
  return {
924
1089
  optimizeIncremental,
925
1090
  optimizeFull,
926
1091
  getState,
927
1092
  restoreState,
928
1093
  reset,
929
- getStats
1094
+ getStats,
1095
+ serializeState,
1096
+ deserializeState
930
1097
  };
931
1098
  }
932
1099
 
@@ -951,9 +1118,22 @@ function createContextPipeline(config) {
951
1118
  currentEntries = result.kept;
952
1119
  budgetUtilization = result.budgetUtilization;
953
1120
  if (currentEntries.length > windowSize) {
954
- const sorted = [...currentEntries].sort((a, b) => b.timestamp - a.timestamp);
955
- const toKeep = sorted.slice(0, windowSize);
956
- const toRemove = sorted.slice(windowSize);
1121
+ const timestamps = currentEntries.map((e) => e.timestamp);
1122
+ const minTs = Math.min(...timestamps);
1123
+ const maxTs = Math.max(...timestamps);
1124
+ const tsRange = maxTs - minTs || 1;
1125
+ const scored = currentEntries.map((entry) => {
1126
+ if (entry.isBTSP) return { entry, hybridScore: 2 };
1127
+ const ageNormalized = (entry.timestamp - minTs) / tsRange;
1128
+ const hybridScore = ageNormalized * 0.4 + entry.score * 0.6;
1129
+ return { entry, hybridScore };
1130
+ });
1131
+ scored.sort((a, b) => {
1132
+ if (b.hybridScore !== a.hybridScore) return b.hybridScore - a.hybridScore;
1133
+ return b.entry.timestamp - a.entry.timestamp;
1134
+ });
1135
+ const toKeep = scored.slice(0, windowSize).map((s) => s.entry);
1136
+ const toRemove = scored.slice(windowSize);
957
1137
  currentEntries = toKeep;
958
1138
  evictedEntries += toRemove.length;
959
1139
  }
@@ -980,32 +1160,638 @@ function createContextPipeline(config) {
980
1160
  uniqueTerms: optimizerStats.uniqueTerms,
981
1161
  updateCount: optimizerStats.updateCount
982
1162
  }
983
- };
1163
+ };
1164
+ }
1165
+ function clear() {
1166
+ totalIngested = 0;
1167
+ evictedEntries = 0;
1168
+ currentEntries = [];
1169
+ budgetUtilization = 0;
1170
+ optimizer.reset();
1171
+ }
1172
+ function serializeOptimizerState() {
1173
+ return optimizer.serializeState();
1174
+ }
1175
+ function deserializeOptimizerState(json) {
1176
+ return optimizer.deserializeState(json);
1177
+ }
1178
+ return {
1179
+ serializeOptimizerState,
1180
+ deserializeOptimizerState,
1181
+ ingest,
1182
+ getContext,
1183
+ getEntries,
1184
+ getStats,
1185
+ clear
1186
+ };
1187
+ }
1188
+
1189
+ // src/core/debt-tracker.ts
1190
+ var import_node_crypto5 = require("crypto");
1191
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
1192
+ function createDebtTracker(dbPath) {
1193
+ const db = new import_better_sqlite3.default(dbPath);
1194
+ db.pragma("journal_mode = WAL");
1195
+ db.exec(`
1196
+ CREATE TABLE IF NOT EXISTS tech_debt (
1197
+ id TEXT PRIMARY KEY NOT NULL,
1198
+ description TEXT NOT NULL,
1199
+ created_at INTEGER NOT NULL,
1200
+ repayment_date INTEGER NOT NULL,
1201
+ severity TEXT NOT NULL CHECK(severity IN ('P0', 'P1', 'P2')),
1202
+ token_cost INTEGER NOT NULL DEFAULT 0,
1203
+ files_affected TEXT NOT NULL DEFAULT '[]',
1204
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'in_progress', 'resolved')),
1205
+ resolution_tokens INTEGER,
1206
+ resolved_at INTEGER
1207
+ );
1208
+ `);
1209
+ db.exec(`
1210
+ CREATE INDEX IF NOT EXISTS idx_debt_status ON tech_debt(status);
1211
+ CREATE INDEX IF NOT EXISTS idx_debt_severity ON tech_debt(severity);
1212
+ CREATE INDEX IF NOT EXISTS idx_debt_repayment ON tech_debt(repayment_date);
1213
+ `);
1214
+ const insertStmt = db.prepare(`
1215
+ INSERT INTO tech_debt (id, description, created_at, repayment_date, severity, token_cost, files_affected, status)
1216
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'open')
1217
+ `);
1218
+ const getStmt = db.prepare("SELECT * FROM tech_debt WHERE id = ?");
1219
+ function rowToDebt(row) {
1220
+ return {
1221
+ id: row["id"],
1222
+ description: row["description"],
1223
+ created_at: row["created_at"],
1224
+ repayment_date: row["repayment_date"],
1225
+ severity: row["severity"],
1226
+ token_cost: row["token_cost"],
1227
+ files_affected: JSON.parse(row["files_affected"] || "[]"),
1228
+ status: row["status"],
1229
+ resolution_tokens: row["resolution_tokens"],
1230
+ resolved_at: row["resolved_at"]
1231
+ };
1232
+ }
1233
+ async function add(debt) {
1234
+ const id = (0, import_node_crypto5.randomUUID)().split("-")[0] || "debt";
1235
+ const created_at = Date.now();
1236
+ insertStmt.run(
1237
+ id,
1238
+ debt.description,
1239
+ created_at,
1240
+ debt.repayment_date,
1241
+ debt.severity,
1242
+ debt.token_cost,
1243
+ JSON.stringify(debt.files_affected)
1244
+ );
1245
+ return {
1246
+ id,
1247
+ created_at,
1248
+ status: "open",
1249
+ description: debt.description,
1250
+ repayment_date: debt.repayment_date,
1251
+ severity: debt.severity,
1252
+ token_cost: debt.token_cost,
1253
+ files_affected: debt.files_affected
1254
+ };
1255
+ }
1256
+ async function list(filter = {}) {
1257
+ let sql = "SELECT * FROM tech_debt WHERE 1=1";
1258
+ const params = [];
1259
+ if (filter.status) {
1260
+ sql += " AND status = ?";
1261
+ params.push(filter.status);
1262
+ }
1263
+ if (filter.severity) {
1264
+ sql += " AND severity = ?";
1265
+ params.push(filter.severity);
1266
+ }
1267
+ if (filter.overdue) {
1268
+ sql += " AND repayment_date < ? AND status != ?";
1269
+ params.push(Date.now());
1270
+ params.push("resolved");
1271
+ }
1272
+ sql += " ORDER BY severity ASC, repayment_date ASC";
1273
+ const rows = db.prepare(sql).all(...params);
1274
+ return rows.map(rowToDebt);
1275
+ }
1276
+ async function get(id) {
1277
+ const row = getStmt.get(id);
1278
+ if (!row) return null;
1279
+ return rowToDebt(row);
1280
+ }
1281
+ async function resolve2(id, resolutionTokens) {
1282
+ const result = db.prepare(
1283
+ "UPDATE tech_debt SET status = ?, resolution_tokens = ?, resolved_at = ? WHERE id = ?"
1284
+ ).run("resolved", resolutionTokens ?? null, Date.now(), id);
1285
+ if (result.changes === 0) {
1286
+ throw new Error(`Debt not found: ${id}`);
1287
+ }
1288
+ }
1289
+ async function start(id) {
1290
+ const result = db.prepare("UPDATE tech_debt SET status = ? WHERE id = ?").run("in_progress", id);
1291
+ if (result.changes === 0) {
1292
+ throw new Error(`Debt not found: ${id}`);
1293
+ }
1294
+ }
1295
+ async function remove(id) {
1296
+ db.prepare("DELETE FROM tech_debt WHERE id = ?").run(id);
1297
+ }
1298
+ async function stats() {
1299
+ const all = db.prepare("SELECT * FROM tech_debt").all();
1300
+ const debts = all.map(rowToDebt);
1301
+ const now = Date.now();
1302
+ const open = debts.filter((d) => d.status === "open");
1303
+ const inProgress = debts.filter((d) => d.status === "in_progress");
1304
+ const resolved = debts.filter((d) => d.status === "resolved");
1305
+ const overdue = debts.filter((d) => d.status !== "resolved" && d.repayment_date < now);
1306
+ const totalTokenCost = debts.reduce((sum, d) => sum + d.token_cost, 0);
1307
+ const resolvedTokenCost = resolved.reduce(
1308
+ (sum, d) => sum + (d.resolution_tokens || d.token_cost),
1309
+ 0
1310
+ );
1311
+ const resolvedOnTime = resolved.filter(
1312
+ (d) => d.resolved_at && d.resolved_at <= d.repayment_date
1313
+ ).length;
1314
+ const repaymentRate = resolved.length > 0 ? resolvedOnTime / resolved.length : 0;
1315
+ return {
1316
+ total: debts.length,
1317
+ open: open.length,
1318
+ in_progress: inProgress.length,
1319
+ resolved: resolved.length,
1320
+ overdue: overdue.length,
1321
+ totalTokenCost,
1322
+ resolvedTokenCost,
1323
+ repaymentRate
1324
+ };
1325
+ }
1326
+ async function getCritical() {
1327
+ const rows = db.prepare(
1328
+ "SELECT * FROM tech_debt WHERE severity = 'P0' AND status != 'resolved' ORDER BY repayment_date ASC"
1329
+ ).all();
1330
+ return rows.map(rowToDebt);
1331
+ }
1332
+ async function close() {
1333
+ db.close();
1334
+ }
1335
+ return {
1336
+ add,
1337
+ list,
1338
+ get,
1339
+ resolve: resolve2,
1340
+ start,
1341
+ remove,
1342
+ stats,
1343
+ getCritical,
1344
+ close
1345
+ };
1346
+ }
1347
+
1348
+ // src/core/dependency-graph.ts
1349
+ var import_node_fs = require("fs");
1350
+ var import_node_path = require("path");
1351
+ var IMPORT_PATTERNS = [
1352
+ // import { Foo, Bar } from './module'
1353
+ /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g,
1354
+ // import Foo from './module'
1355
+ /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
1356
+ // import * as Foo from './module'
1357
+ /import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
1358
+ // import './module' (side-effect)
1359
+ /import\s+['"]([^'"]+)['"]/g,
1360
+ // require('./module')
1361
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
1362
+ ];
1363
+ var EXPORT_PATTERNS = [
1364
+ // export { Foo, Bar }
1365
+ /export\s+\{([^}]+)\}/g,
1366
+ // export function/class/const/let/var/type/interface
1367
+ /export\s+(?:default\s+)?(?:function|class|const|let|var|type|interface|enum)\s+(\w+)/g,
1368
+ // export default
1369
+ /export\s+default\s+/g
1370
+ ];
1371
+ function parseImports(content, filePath) {
1372
+ const edges = [];
1373
+ const seen = /* @__PURE__ */ new Set();
1374
+ for (const pattern of IMPORT_PATTERNS) {
1375
+ const regex = new RegExp(pattern.source, pattern.flags);
1376
+ const matches = [...content.matchAll(regex)];
1377
+ for (const match of matches) {
1378
+ if (pattern.source.includes("from")) {
1379
+ const symbolsRaw = match[1] || "";
1380
+ const target = match[2] || "";
1381
+ if (!target || target.startsWith(".") === false && !target.startsWith("/")) {
1382
+ continue;
1383
+ }
1384
+ const symbols = symbolsRaw.split(",").map(
1385
+ (s) => s.trim().split(/\s+as\s+/)[0]?.trim() || ""
1386
+ ).filter(Boolean);
1387
+ const key = `${filePath}->${target}`;
1388
+ if (!seen.has(key)) {
1389
+ seen.add(key);
1390
+ edges.push({ source: filePath, target, symbols });
1391
+ }
1392
+ } else if (pattern.source.includes("require")) {
1393
+ const target = match[1] || "";
1394
+ if (!target || !target.startsWith(".") && !target.startsWith("/")) {
1395
+ continue;
1396
+ }
1397
+ const key = `${filePath}->${target}`;
1398
+ if (!seen.has(key)) {
1399
+ seen.add(key);
1400
+ edges.push({ source: filePath, target, symbols: [] });
1401
+ }
1402
+ } else {
1403
+ const target = match[1] || "";
1404
+ if (!target || !target.startsWith(".") && !target.startsWith("/")) {
1405
+ continue;
1406
+ }
1407
+ const key = `${filePath}->${target}`;
1408
+ if (!seen.has(key)) {
1409
+ seen.add(key);
1410
+ edges.push({ source: filePath, target, symbols: [] });
1411
+ }
1412
+ }
1413
+ }
1414
+ }
1415
+ return edges;
1416
+ }
1417
+ function parseExports(content) {
1418
+ const exportsList = [];
1419
+ for (const pattern of EXPORT_PATTERNS) {
1420
+ const regex = new RegExp(pattern.source, pattern.flags);
1421
+ const matches = [...content.matchAll(regex)];
1422
+ for (const match of matches) {
1423
+ if (match[1]) {
1424
+ const symbols = match[1].split(",").map(
1425
+ (s) => s.trim().split(/\s+as\s+/)[0]?.trim() || ""
1426
+ ).filter(Boolean);
1427
+ exportsList.push(...symbols);
1428
+ } else {
1429
+ exportsList.push("default");
1430
+ }
1431
+ }
1432
+ }
1433
+ return [...new Set(exportsList)];
1434
+ }
1435
+ function resolveImportPath(importPath, fromFile, projectRoot, extensions) {
1436
+ const cleanImport = importPath.replace(/\.(js|ts|tsx|jsx)$/, "");
1437
+ const baseDir = (0, import_node_path.join)(projectRoot, fromFile, "..");
1438
+ const candidates = [
1439
+ ...extensions.map((ext) => (0, import_node_path.resolve)(baseDir, `${cleanImport}${ext}`)),
1440
+ ...extensions.map((ext) => (0, import_node_path.resolve)(baseDir, cleanImport, `index${ext}`))
1441
+ ];
1442
+ for (const candidate of candidates) {
1443
+ if ((0, import_node_fs.existsSync)(candidate)) {
1444
+ return (0, import_node_path.relative)(projectRoot, candidate).replace(/\\/g, "/");
1445
+ }
1446
+ }
1447
+ return null;
1448
+ }
1449
+ function collectFiles(dir, projectRoot, extensions, ignoreDirs) {
1450
+ const files = [];
1451
+ try {
1452
+ const entries = (0, import_node_fs.readdirSync)(dir, { withFileTypes: true });
1453
+ for (const entry of entries) {
1454
+ const fullPath = (0, import_node_path.join)(dir, entry.name);
1455
+ if (entry.isDirectory()) {
1456
+ if (!ignoreDirs.includes(entry.name)) {
1457
+ files.push(...collectFiles(fullPath, projectRoot, extensions, ignoreDirs));
1458
+ }
1459
+ } else if (entry.isFile() && extensions.includes((0, import_node_path.extname)(entry.name))) {
1460
+ files.push((0, import_node_path.relative)(projectRoot, fullPath).replace(/\\/g, "/"));
1461
+ }
1462
+ }
1463
+ } catch {
1464
+ }
1465
+ return files;
1466
+ }
1467
+ function createDependencyGraph(config) {
1468
+ const {
1469
+ projectRoot,
1470
+ maxDepth = 50,
1471
+ extensions = [".ts", ".tsx", ".js", ".jsx"],
1472
+ ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"]
1473
+ } = config;
1474
+ const nodes = /* @__PURE__ */ new Map();
1475
+ let built = false;
1476
+ async function build() {
1477
+ nodes.clear();
1478
+ const files = collectFiles(projectRoot, projectRoot, extensions, ignoreDirs);
1479
+ for (const filePath of files) {
1480
+ const fullPath = (0, import_node_path.join)(projectRoot, filePath);
1481
+ try {
1482
+ const content = (0, import_node_fs.readFileSync)(fullPath, "utf-8");
1483
+ const stat = (0, import_node_fs.statSync)(fullPath);
1484
+ const exports2 = parseExports(content);
1485
+ const imports = parseImports(content, filePath);
1486
+ const resolvedImports = [];
1487
+ for (const imp of imports) {
1488
+ const resolved = resolveImportPath(imp.target, filePath, projectRoot, extensions);
1489
+ if (resolved) {
1490
+ resolvedImports.push({ ...imp, target: resolved });
1491
+ }
1492
+ }
1493
+ nodes.set(filePath, {
1494
+ filePath,
1495
+ exports: exports2,
1496
+ imports: resolvedImports,
1497
+ callers: [],
1498
+ // Populated in second pass
1499
+ engram_score: 0,
1500
+ lastModified: stat.mtimeMs,
1501
+ tokenEstimate: estimateTokens(content)
1502
+ });
1503
+ } catch {
1504
+ }
1505
+ }
1506
+ for (const [filePath, node] of nodes) {
1507
+ for (const imp of node.imports) {
1508
+ const targetNode = nodes.get(imp.target);
1509
+ if (targetNode && !targetNode.callers.includes(filePath)) {
1510
+ targetNode.callers.push(filePath);
1511
+ }
1512
+ }
1513
+ }
1514
+ built = true;
1515
+ return nodes;
1516
+ }
1517
+ async function analyze() {
1518
+ if (!built) await build();
1519
+ const entryPoints = [];
1520
+ const orphans = [];
1521
+ const callerCounts = /* @__PURE__ */ new Map();
1522
+ for (const [filePath, node] of nodes) {
1523
+ if (node.callers.length === 0 && node.imports.length > 0) {
1524
+ entryPoints.push(filePath);
1525
+ }
1526
+ if (node.callers.length === 0 && node.imports.length === 0) {
1527
+ orphans.push(filePath);
1528
+ }
1529
+ callerCounts.set(filePath, node.callers.length);
1530
+ }
1531
+ const sortedByCallers = [...callerCounts.entries()].sort((a, b) => b[1] - a[1]).filter(([, count]) => count > 0);
1532
+ const hotPaths = sortedByCallers.slice(0, 10).map(([path]) => path);
1533
+ const totalTokens = [...nodes.values()].reduce((sum, n) => sum + n.tokenEstimate, 0);
1534
+ const hotPathTokens = hotPaths.reduce(
1535
+ (sum, path) => sum + (nodes.get(path)?.tokenEstimate || 0),
1536
+ 0
1537
+ );
1538
+ return {
1539
+ entryPoints,
1540
+ hotPaths,
1541
+ orphans,
1542
+ totalTokens,
1543
+ optimizedTokens: hotPathTokens
1544
+ };
1545
+ }
1546
+ async function focus(pattern) {
1547
+ if (!built) await build();
1548
+ const matching = /* @__PURE__ */ new Map();
1549
+ const lowerPattern = pattern.toLowerCase();
1550
+ for (const [filePath, node] of nodes) {
1551
+ if (filePath.toLowerCase().includes(lowerPattern)) {
1552
+ matching.set(filePath, node);
1553
+ for (const imp of node.imports) {
1554
+ const depNode = nodes.get(imp.target);
1555
+ if (depNode) {
1556
+ matching.set(imp.target, depNode);
1557
+ }
1558
+ }
1559
+ for (const caller of node.callers) {
1560
+ const callerNode = nodes.get(caller);
1561
+ if (callerNode) {
1562
+ matching.set(caller, callerNode);
1563
+ }
1564
+ }
1565
+ }
1566
+ }
1567
+ return matching;
1568
+ }
1569
+ async function getFilesFromEntry(entryPoint, depth = maxDepth) {
1570
+ if (!built) await build();
1571
+ const visited = /* @__PURE__ */ new Set();
1572
+ const queue = [{ path: entryPoint, depth: 0 }];
1573
+ while (queue.length > 0) {
1574
+ const item = queue.shift();
1575
+ if (!item) break;
1576
+ if (visited.has(item.path) || item.depth > depth) continue;
1577
+ visited.add(item.path);
1578
+ const node = nodes.get(item.path);
1579
+ if (node) {
1580
+ for (const imp of node.imports) {
1581
+ if (!visited.has(imp.target)) {
1582
+ queue.push({ path: imp.target, depth: item.depth + 1 });
1583
+ }
1584
+ }
1585
+ }
1586
+ }
1587
+ return [...visited];
1588
+ }
1589
+ function getNodes() {
1590
+ return nodes;
1591
+ }
1592
+ return {
1593
+ build,
1594
+ analyze,
1595
+ focus,
1596
+ getFilesFromEntry,
1597
+ getNodes
1598
+ };
1599
+ }
1600
+
1601
+ // src/core/docs-generator.ts
1602
+ var import_node_fs2 = require("fs");
1603
+ var import_node_path2 = require("path");
1604
+ function detectEntryPoints(projectRoot) {
1605
+ const entries = [];
1606
+ const candidates = [
1607
+ { path: "src/index.ts", desc: "Library API" },
1608
+ { path: "src/cli/index.ts", desc: "CLI entry point" },
1609
+ { path: "src/daemon/index.ts", desc: "Daemon process" },
1610
+ { path: "src/mcp/index.ts", desc: "MCP server" },
1611
+ { path: "src/main.ts", desc: "Main entry" },
1612
+ { path: "src/app.ts", desc: "App entry" },
1613
+ { path: "src/server.ts", desc: "Server entry" },
1614
+ { path: "index.ts", desc: "Root entry" },
1615
+ { path: "index.js", desc: "Root entry" }
1616
+ ];
1617
+ for (const c of candidates) {
1618
+ if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(projectRoot, c.path))) {
1619
+ entries.push({ path: c.path, description: c.desc });
1620
+ }
1621
+ }
1622
+ return entries;
1623
+ }
1624
+ function scanModules(dir, projectRoot, ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"]) {
1625
+ const modules = [];
1626
+ try {
1627
+ const entries = (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true });
1628
+ for (const entry of entries) {
1629
+ const fullPath = (0, import_node_path2.join)(dir, entry.name);
1630
+ if (entry.isDirectory() && !ignoreDirs.includes(entry.name)) {
1631
+ modules.push(...scanModules(fullPath, projectRoot, ignoreDirs));
1632
+ } else if (entry.isFile() && [".ts", ".tsx", ".js", ".jsx"].includes((0, import_node_path2.extname)(entry.name))) {
1633
+ try {
1634
+ const content = (0, import_node_fs2.readFileSync)(fullPath, "utf-8");
1635
+ modules.push({
1636
+ path: (0, import_node_path2.relative)(projectRoot, fullPath).replace(/\\/g, "/"),
1637
+ lines: content.split("\n").length
1638
+ });
1639
+ } catch {
1640
+ }
1641
+ }
1642
+ }
1643
+ } catch {
984
1644
  }
985
- function clear() {
986
- totalIngested = 0;
987
- evictedEntries = 0;
988
- currentEntries = [];
989
- budgetUtilization = 0;
990
- optimizer.reset();
1645
+ return modules;
1646
+ }
1647
+ function readPackageJson(projectRoot) {
1648
+ const pkgPath = (0, import_node_path2.join)(projectRoot, "package.json");
1649
+ if (!(0, import_node_fs2.existsSync)(pkgPath)) return null;
1650
+ try {
1651
+ return JSON.parse((0, import_node_fs2.readFileSync)(pkgPath, "utf-8"));
1652
+ } catch {
1653
+ return null;
991
1654
  }
992
- return {
993
- ingest,
994
- getContext,
995
- getEntries,
996
- getStats,
997
- clear
1655
+ }
1656
+ function detectStack(projectRoot) {
1657
+ const stack = [];
1658
+ const pkg = readPackageJson(projectRoot);
1659
+ if (!pkg) return stack;
1660
+ const allDeps = {
1661
+ ...pkg.dependencies,
1662
+ ...pkg.devDependencies
998
1663
  };
1664
+ if (allDeps["typescript"]) stack.push("TypeScript");
1665
+ if (allDeps["vitest"]) stack.push("Vitest");
1666
+ if (allDeps["@biomejs/biome"]) stack.push("Biome");
1667
+ if (allDeps["eslint"]) stack.push("ESLint");
1668
+ if (allDeps["prettier"]) stack.push("Prettier");
1669
+ if (allDeps["react"]) stack.push("React");
1670
+ if (allDeps["next"]) stack.push("Next.js");
1671
+ if (allDeps["express"]) stack.push("Express");
1672
+ if (allDeps["commander"]) stack.push("Commander.js CLI");
1673
+ if (allDeps["better-sqlite3"]) stack.push("SQLite (better-sqlite3)");
1674
+ if (allDeps["zod"]) stack.push("Zod validation");
1675
+ return stack;
1676
+ }
1677
+ function createDocsGenerator(config) {
1678
+ const { projectRoot, includeGraph = true } = config;
1679
+ async function generate(graph) {
1680
+ const lines = [];
1681
+ const pkg = readPackageJson(projectRoot);
1682
+ const projectName = pkg?.name || "Project";
1683
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1684
+ lines.push(`# ${projectName} \u2014 Developer Guide`);
1685
+ lines.push(`<!-- Auto-generated by Sparn v1.3.0 \u2014 ${now} -->`);
1686
+ lines.push("");
1687
+ const stack = detectStack(projectRoot);
1688
+ if (stack.length > 0) {
1689
+ lines.push(`**Stack**: ${stack.join(", ")}`);
1690
+ lines.push("");
1691
+ }
1692
+ if (pkg?.scripts) {
1693
+ lines.push("## Commands");
1694
+ lines.push("");
1695
+ const important = ["build", "dev", "test", "lint", "typecheck", "start"];
1696
+ for (const cmd of important) {
1697
+ if (pkg.scripts[cmd]) {
1698
+ lines.push(`- \`npm run ${cmd}\` \u2014 \`${pkg.scripts[cmd]}\``);
1699
+ }
1700
+ }
1701
+ lines.push("");
1702
+ }
1703
+ const entryPoints = detectEntryPoints(projectRoot);
1704
+ if (entryPoints.length > 0) {
1705
+ lines.push("## Entry Points");
1706
+ lines.push("");
1707
+ for (const ep of entryPoints) {
1708
+ lines.push(`- \`${ep.path}\` \u2014 ${ep.description}`);
1709
+ }
1710
+ lines.push("");
1711
+ }
1712
+ const srcDir = (0, import_node_path2.join)(projectRoot, "src");
1713
+ if ((0, import_node_fs2.existsSync)(srcDir)) {
1714
+ const modules = scanModules(srcDir, projectRoot);
1715
+ const dirGroups = /* @__PURE__ */ new Map();
1716
+ for (const mod of modules) {
1717
+ const parts = mod.path.split("/");
1718
+ const dir = parts.length > 2 ? parts.slice(0, 2).join("/") : parts[0] || "";
1719
+ if (!dirGroups.has(dir)) {
1720
+ dirGroups.set(dir, []);
1721
+ }
1722
+ dirGroups.get(dir)?.push({
1723
+ file: parts[parts.length - 1] || mod.path,
1724
+ lines: mod.lines
1725
+ });
1726
+ }
1727
+ lines.push("## Structure");
1728
+ lines.push("");
1729
+ for (const [dir, files] of dirGroups) {
1730
+ lines.push(`### ${dir}/ (${files.length} files)`);
1731
+ const shown = files.slice(0, 8);
1732
+ for (const f of shown) {
1733
+ lines.push(`- \`${f.file}\` (${f.lines}L)`);
1734
+ }
1735
+ if (files.length > 8) {
1736
+ lines.push(`- ... and ${files.length - 8} more`);
1737
+ }
1738
+ lines.push("");
1739
+ }
1740
+ }
1741
+ if (includeGraph && graph) {
1742
+ try {
1743
+ const analysis = await graph.analyze();
1744
+ lines.push("## Hot Dependencies (most imported)");
1745
+ lines.push("");
1746
+ for (const path of analysis.hotPaths.slice(0, 5)) {
1747
+ const node = graph.getNodes().get(path);
1748
+ const callerCount = node?.callers.length || 0;
1749
+ lines.push(`- \`${path}\` (imported by ${callerCount} modules)`);
1750
+ }
1751
+ lines.push("");
1752
+ if (analysis.orphans.length > 0) {
1753
+ lines.push(
1754
+ `**Orphaned files** (${analysis.orphans.length}): ${analysis.orphans.slice(0, 3).join(", ")}${analysis.orphans.length > 3 ? "..." : ""}`
1755
+ );
1756
+ lines.push("");
1757
+ }
1758
+ lines.push(
1759
+ `**Total tokens**: ${analysis.totalTokens.toLocaleString()} | **Hot path tokens**: ${analysis.optimizedTokens.toLocaleString()}`
1760
+ );
1761
+ lines.push("");
1762
+ } catch {
1763
+ }
1764
+ }
1765
+ const sparnConfigPath = (0, import_node_path2.join)(projectRoot, ".sparn", "config.yaml");
1766
+ if ((0, import_node_fs2.existsSync)(sparnConfigPath)) {
1767
+ lines.push("## Sparn Optimization");
1768
+ lines.push("");
1769
+ lines.push("Sparn is active in this project. Key features:");
1770
+ lines.push("- Context optimization (60-70% token reduction)");
1771
+ lines.push("- Use `sparn search` before reading files");
1772
+ lines.push("- Use `sparn graph` to understand dependencies");
1773
+ lines.push("- Use `sparn plan` for planning, `sparn exec` for execution");
1774
+ lines.push("");
1775
+ }
1776
+ if (config.customSections) {
1777
+ for (const section of config.customSections) {
1778
+ lines.push(section);
1779
+ lines.push("");
1780
+ }
1781
+ }
1782
+ return lines.join("\n");
1783
+ }
1784
+ return { generate };
999
1785
  }
1000
1786
 
1001
1787
  // src/core/kv-memory.ts
1002
- var import_node_fs = require("fs");
1003
- var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
1788
+ var import_node_fs3 = require("fs");
1789
+ var import_better_sqlite32 = __toESM(require("better-sqlite3"), 1);
1004
1790
  function createBackup(dbPath) {
1005
1791
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1006
1792
  const backupPath = `${dbPath}.backup-${timestamp}`;
1007
1793
  try {
1008
- (0, import_node_fs.copyFileSync)(dbPath, backupPath);
1794
+ (0, import_node_fs3.copyFileSync)(dbPath, backupPath);
1009
1795
  console.log(`\u2713 Database backed up to: ${backupPath}`);
1010
1796
  return backupPath;
1011
1797
  } catch (error) {
@@ -1016,29 +1802,30 @@ function createBackup(dbPath) {
1016
1802
  async function createKVMemory(dbPath) {
1017
1803
  let db;
1018
1804
  try {
1019
- db = new import_better_sqlite3.default(dbPath);
1805
+ db = new import_better_sqlite32.default(dbPath);
1020
1806
  const integrityCheck = db.pragma("quick_check", { simple: true });
1021
1807
  if (integrityCheck !== "ok") {
1022
1808
  console.error("\u26A0 Database corruption detected!");
1023
- if ((0, import_node_fs.existsSync)(dbPath)) {
1809
+ db.close();
1810
+ if ((0, import_node_fs3.existsSync)(dbPath)) {
1024
1811
  const backupPath = createBackup(dbPath);
1025
1812
  if (backupPath) {
1026
1813
  console.log(`Backup created at: ${backupPath}`);
1027
1814
  }
1028
1815
  }
1029
1816
  console.log("Attempting database recovery...");
1030
- db.close();
1031
- db = new import_better_sqlite3.default(dbPath);
1817
+ db = new import_better_sqlite32.default(dbPath);
1032
1818
  }
1033
1819
  } catch (error) {
1034
1820
  console.error("\u26A0 Database error detected:", error);
1035
- if ((0, import_node_fs.existsSync)(dbPath)) {
1821
+ if ((0, import_node_fs3.existsSync)(dbPath)) {
1036
1822
  createBackup(dbPath);
1037
1823
  console.log("Creating new database...");
1038
1824
  }
1039
- db = new import_better_sqlite3.default(dbPath);
1825
+ db = new import_better_sqlite32.default(dbPath);
1040
1826
  }
1041
1827
  db.pragma("journal_mode = WAL");
1828
+ db.pragma("foreign_keys = ON");
1042
1829
  db.exec(`
1043
1830
  CREATE TABLE IF NOT EXISTS entries_index (
1044
1831
  id TEXT PRIMARY KEY NOT NULL,
@@ -1078,6 +1865,36 @@ async function createKVMemory(dbPath) {
1078
1865
  CREATE INDEX IF NOT EXISTS idx_entries_timestamp ON entries_index(timestamp DESC);
1079
1866
  CREATE INDEX IF NOT EXISTS idx_stats_timestamp ON optimization_stats(timestamp DESC);
1080
1867
  `);
1868
+ db.exec(`
1869
+ CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(id, content, tokenize='porter');
1870
+ `);
1871
+ db.exec(`
1872
+ CREATE TRIGGER IF NOT EXISTS entries_fts_insert
1873
+ AFTER INSERT ON entries_value
1874
+ BEGIN
1875
+ INSERT OR REPLACE INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
1876
+ END;
1877
+ `);
1878
+ db.exec(`
1879
+ CREATE TRIGGER IF NOT EXISTS entries_fts_delete
1880
+ AFTER DELETE ON entries_value
1881
+ BEGIN
1882
+ DELETE FROM entries_fts WHERE id = OLD.id;
1883
+ END;
1884
+ `);
1885
+ db.exec(`
1886
+ CREATE TRIGGER IF NOT EXISTS entries_fts_update
1887
+ AFTER UPDATE ON entries_value
1888
+ BEGIN
1889
+ DELETE FROM entries_fts WHERE id = OLD.id;
1890
+ INSERT INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
1891
+ END;
1892
+ `);
1893
+ db.exec(`
1894
+ INSERT OR IGNORE INTO entries_fts(id, content)
1895
+ SELECT id, content FROM entries_value
1896
+ WHERE id NOT IN (SELECT id FROM entries_fts);
1897
+ `);
1081
1898
  const putIndexStmt = db.prepare(`
1082
1899
  INSERT OR REPLACE INTO entries_index
1083
1900
  (id, hash, timestamp, score, ttl, state, accessCount, isBTSP)
@@ -1166,14 +1983,20 @@ async function createKVMemory(dbPath) {
1166
1983
  sql += " AND i.isBTSP = ?";
1167
1984
  params.push(filters.isBTSP ? 1 : 0);
1168
1985
  }
1986
+ if (filters.tags && filters.tags.length > 0) {
1987
+ for (const tag of filters.tags) {
1988
+ sql += " AND v.tags LIKE ?";
1989
+ params.push(`%"${tag}"%`);
1990
+ }
1991
+ }
1169
1992
  sql += " ORDER BY i.score DESC";
1170
1993
  if (filters.limit) {
1171
1994
  sql += " LIMIT ?";
1172
1995
  params.push(filters.limit);
1173
- }
1174
- if (filters.offset) {
1175
- sql += " OFFSET ?";
1176
- params.push(filters.offset);
1996
+ if (filters.offset) {
1997
+ sql += " OFFSET ?";
1998
+ params.push(filters.offset);
1999
+ }
1177
2000
  }
1178
2001
  const stmt = db.prepare(sql);
1179
2002
  const rows = stmt.all(...params);
@@ -1208,7 +2031,22 @@ async function createKVMemory(dbPath) {
1208
2031
  },
1209
2032
  async compact() {
1210
2033
  const before = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
1211
- db.exec("DELETE FROM entries_index WHERE ttl <= 0");
2034
+ const now = Date.now();
2035
+ db.prepare("DELETE FROM entries_index WHERE isBTSP = 0 AND (timestamp + ttl * 1000) < ?").run(
2036
+ now
2037
+ );
2038
+ db.exec("DELETE FROM entries_index WHERE isBTSP = 0 AND ttl <= 0");
2039
+ const candidates = db.prepare("SELECT id, timestamp, ttl FROM entries_index WHERE isBTSP = 0").all();
2040
+ for (const row of candidates) {
2041
+ const ageSeconds = Math.max(0, (now - row.timestamp) / 1e3);
2042
+ const ttlSeconds = row.ttl;
2043
+ if (ttlSeconds <= 0) continue;
2044
+ const decay = 1 - Math.exp(-ageSeconds / ttlSeconds);
2045
+ if (decay >= 0.95) {
2046
+ db.prepare("DELETE FROM entries_index WHERE id = ?").run(row.id);
2047
+ }
2048
+ }
2049
+ db.exec("DELETE FROM entries_value WHERE id NOT IN (SELECT id FROM entries_index)");
1212
2050
  db.exec("VACUUM");
1213
2051
  const after = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
1214
2052
  return before.count - after.count;
@@ -1228,6 +2066,9 @@ async function createKVMemory(dbPath) {
1228
2066
  stats.entries_pruned,
1229
2067
  stats.duration_ms
1230
2068
  );
2069
+ db.prepare(
2070
+ "DELETE FROM optimization_stats WHERE id NOT IN (SELECT id FROM optimization_stats ORDER BY timestamp DESC LIMIT 1000)"
2071
+ ).run();
1231
2072
  },
1232
2073
  async getOptimizationStats() {
1233
2074
  const stmt = db.prepare(`
@@ -1240,7 +2081,286 @@ async function createKVMemory(dbPath) {
1240
2081
  },
1241
2082
  async clearOptimizationStats() {
1242
2083
  db.exec("DELETE FROM optimization_stats");
2084
+ },
2085
+ async searchFTS(query, limit = 10) {
2086
+ if (!query || query.trim().length === 0) return [];
2087
+ const sanitized = query.replace(/[{}()[\]"':*^~]/g, " ").trim();
2088
+ if (sanitized.length === 0) return [];
2089
+ const stmt = db.prepare(`
2090
+ SELECT
2091
+ f.id, f.content, rank,
2092
+ i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
2093
+ v.tags, v.metadata
2094
+ FROM entries_fts f
2095
+ JOIN entries_index i ON f.id = i.id
2096
+ JOIN entries_value v ON f.id = v.id
2097
+ WHERE entries_fts MATCH ?
2098
+ ORDER BY rank
2099
+ LIMIT ?
2100
+ `);
2101
+ try {
2102
+ const rows = stmt.all(sanitized, limit);
2103
+ return rows.map((r) => ({
2104
+ entry: {
2105
+ id: r.id,
2106
+ content: r.content,
2107
+ hash: r.hash,
2108
+ timestamp: r.timestamp,
2109
+ score: r.score,
2110
+ ttl: r.ttl,
2111
+ state: r.state,
2112
+ accessCount: r.accessCount,
2113
+ tags: r.tags ? JSON.parse(r.tags) : [],
2114
+ metadata: r.metadata ? JSON.parse(r.metadata) : {},
2115
+ isBTSP: r.isBTSP === 1
2116
+ },
2117
+ rank: r.rank
2118
+ }));
2119
+ } catch {
2120
+ return [];
2121
+ }
2122
+ }
2123
+ };
2124
+ }
2125
+
2126
+ // src/core/search-engine.ts
2127
+ var import_node_child_process = require("child_process");
2128
+ var import_node_fs4 = require("fs");
2129
+ var import_node_path3 = require("path");
2130
+ var import_better_sqlite33 = __toESM(require("better-sqlite3"), 1);
2131
+ function hasRipgrep() {
2132
+ try {
2133
+ (0, import_node_child_process.execSync)("rg --version", { stdio: "pipe" });
2134
+ return true;
2135
+ } catch {
2136
+ return false;
2137
+ }
2138
+ }
2139
+ function ripgrepSearch(query, projectRoot, opts = {}) {
2140
+ try {
2141
+ const args = ["--line-number", "--no-heading", "--color=never"];
2142
+ if (opts.fileGlob) {
2143
+ args.push("--glob", opts.fileGlob);
2144
+ }
2145
+ args.push(
2146
+ "--glob",
2147
+ "!node_modules",
2148
+ "--glob",
2149
+ "!dist",
2150
+ "--glob",
2151
+ "!.git",
2152
+ "--glob",
2153
+ "!.sparn",
2154
+ "--glob",
2155
+ "!coverage"
2156
+ );
2157
+ const maxResults = opts.maxResults || 20;
2158
+ args.push("--max-count", String(maxResults));
2159
+ if (opts.includeContext) {
2160
+ args.push("-C", "2");
2161
+ }
2162
+ args.push("--", query, projectRoot);
2163
+ const output = (0, import_node_child_process.execFileSync)("rg", args, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
2164
+ const results = [];
2165
+ const lines = output.split("\n").filter(Boolean);
2166
+ for (const line of lines) {
2167
+ const match = line.match(/^(.+?):(\d+):(.*)/);
2168
+ if (match) {
2169
+ const filePath = (0, import_node_path3.relative)(projectRoot, match[1] || "").replace(/\\/g, "/");
2170
+ const lineNumber = Number.parseInt(match[2] || "0", 10);
2171
+ const content = (match[3] || "").trim();
2172
+ results.push({
2173
+ filePath,
2174
+ lineNumber,
2175
+ content,
2176
+ score: 0.8,
2177
+ // Exact match gets high base score
2178
+ context: [],
2179
+ engram_score: 0
2180
+ });
2181
+ }
2182
+ }
2183
+ return results.slice(0, maxResults);
2184
+ } catch {
2185
+ return [];
2186
+ }
2187
+ }
2188
+ function collectIndexableFiles(dir, projectRoot, ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"], exts = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".yaml", ".yml"]) {
2189
+ const files = [];
2190
+ try {
2191
+ const entries = (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true });
2192
+ for (const entry of entries) {
2193
+ const fullPath = (0, import_node_path3.join)(dir, entry.name);
2194
+ if (entry.isDirectory()) {
2195
+ if (!ignoreDirs.includes(entry.name)) {
2196
+ files.push(...collectIndexableFiles(fullPath, projectRoot, ignoreDirs, exts));
2197
+ }
2198
+ } else if (entry.isFile() && exts.includes((0, import_node_path3.extname)(entry.name))) {
2199
+ try {
2200
+ const stat = (0, import_node_fs4.statSync)(fullPath);
2201
+ if (stat.size < 100 * 1024) {
2202
+ files.push((0, import_node_path3.relative)(projectRoot, fullPath).replace(/\\/g, "/"));
2203
+ }
2204
+ } catch {
2205
+ }
2206
+ }
2207
+ }
2208
+ } catch {
2209
+ }
2210
+ return files;
2211
+ }
2212
+ function createSearchEngine(dbPath) {
2213
+ let db = null;
2214
+ let projectRoot = "";
2215
+ const rgAvailable = hasRipgrep();
2216
+ async function init(root) {
2217
+ if (db) {
2218
+ try {
2219
+ db.close();
2220
+ } catch {
2221
+ }
2222
+ }
2223
+ projectRoot = root;
2224
+ db = new import_better_sqlite33.default(dbPath);
2225
+ db.pragma("journal_mode = WAL");
2226
+ db.exec(`
2227
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
2228
+ filepath, line_number, content, tokenize='porter'
2229
+ );
2230
+ `);
2231
+ db.exec(`
2232
+ CREATE TABLE IF NOT EXISTS search_meta (
2233
+ filepath TEXT PRIMARY KEY,
2234
+ mtime INTEGER NOT NULL,
2235
+ indexed_at INTEGER NOT NULL
2236
+ );
2237
+ `);
2238
+ }
2239
+ async function index(paths) {
2240
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
2241
+ const startTime = Date.now();
2242
+ const filesToIndex = paths || collectIndexableFiles(projectRoot, projectRoot);
2243
+ let filesIndexed = 0;
2244
+ let totalLines = 0;
2245
+ const insertStmt = db.prepare(
2246
+ "INSERT INTO search_index(filepath, line_number, content) VALUES (?, ?, ?)"
2247
+ );
2248
+ const metaStmt = db.prepare(
2249
+ "INSERT OR REPLACE INTO search_meta(filepath, mtime, indexed_at) VALUES (?, ?, ?)"
2250
+ );
2251
+ const checkMeta = db.prepare("SELECT mtime FROM search_meta WHERE filepath = ?");
2252
+ const deleteFile = db.prepare("DELETE FROM search_index WHERE filepath = ?");
2253
+ const transaction = db.transaction(() => {
2254
+ for (const filePath of filesToIndex) {
2255
+ const fullPath = (0, import_node_path3.join)(projectRoot, filePath);
2256
+ if (!(0, import_node_fs4.existsSync)(fullPath)) continue;
2257
+ try {
2258
+ const stat = (0, import_node_fs4.statSync)(fullPath);
2259
+ const existing = checkMeta.get(filePath);
2260
+ if (existing && existing.mtime >= stat.mtimeMs) {
2261
+ continue;
2262
+ }
2263
+ deleteFile.run(filePath);
2264
+ const content = (0, import_node_fs4.readFileSync)(fullPath, "utf-8");
2265
+ const lines = content.split("\n");
2266
+ for (let i = 0; i < lines.length; i++) {
2267
+ const line = lines[i];
2268
+ if (line && line.trim().length > 0) {
2269
+ insertStmt.run(filePath, i + 1, line);
2270
+ totalLines++;
2271
+ }
2272
+ }
2273
+ metaStmt.run(filePath, stat.mtimeMs, Date.now());
2274
+ filesIndexed++;
2275
+ } catch {
2276
+ }
2277
+ }
2278
+ });
2279
+ transaction();
2280
+ return {
2281
+ filesIndexed,
2282
+ totalLines,
2283
+ duration: Date.now() - startTime
2284
+ };
2285
+ }
2286
+ async function search(query, opts = {}) {
2287
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
2288
+ const maxResults = opts.maxResults || 10;
2289
+ const results = [];
2290
+ const seen = /* @__PURE__ */ new Set();
2291
+ try {
2292
+ const ftsQuery = query.replace(/['"*(){}[\]^~\\:]/g, " ").trim().split(/\s+/).filter((w) => w.length > 0).map((w) => `content:${w}`).join(" ");
2293
+ if (ftsQuery.length > 0) {
2294
+ let sql = `
2295
+ SELECT filepath, line_number, content, rank
2296
+ FROM search_index
2297
+ WHERE search_index MATCH ?
2298
+ `;
2299
+ const params = [ftsQuery];
2300
+ if (opts.fileGlob) {
2301
+ const likePattern = opts.fileGlob.replace(/\*/g, "%").replace(/\?/g, "_");
2302
+ sql += " AND filepath LIKE ?";
2303
+ params.push(likePattern);
2304
+ }
2305
+ sql += " ORDER BY rank LIMIT ?";
2306
+ params.push(maxResults * 2);
2307
+ const rows = db.prepare(sql).all(...params);
2308
+ for (const row of rows) {
2309
+ const key = `${row.filepath}:${row.line_number}`;
2310
+ if (!seen.has(key)) {
2311
+ seen.add(key);
2312
+ const score = Math.min(1, Math.max(0, 1 + row.rank / 10));
2313
+ const context = [];
2314
+ if (opts.includeContext) {
2315
+ const contextRows = db.prepare(
2316
+ `SELECT content FROM search_index WHERE filepath = ? AND CAST(line_number AS INTEGER) BETWEEN ? AND ? ORDER BY CAST(line_number AS INTEGER)`
2317
+ ).all(row.filepath, row.line_number - 2, row.line_number + 2);
2318
+ context.push(...contextRows.map((r) => r.content));
2319
+ }
2320
+ results.push({
2321
+ filePath: row.filepath,
2322
+ lineNumber: row.line_number,
2323
+ content: row.content,
2324
+ score,
2325
+ context,
2326
+ engram_score: 0
2327
+ });
2328
+ }
2329
+ }
2330
+ }
2331
+ } catch {
1243
2332
  }
2333
+ if (rgAvailable && opts.useRipgrep !== false) {
2334
+ const rgResults = ripgrepSearch(query, projectRoot, opts);
2335
+ for (const result of rgResults) {
2336
+ const key = `${result.filePath}:${result.lineNumber}`;
2337
+ if (!seen.has(key)) {
2338
+ seen.add(key);
2339
+ results.push(result);
2340
+ }
2341
+ }
2342
+ }
2343
+ results.sort((a, b) => b.score - a.score);
2344
+ return results.slice(0, maxResults);
2345
+ }
2346
+ async function refresh() {
2347
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
2348
+ db.exec("DELETE FROM search_index");
2349
+ db.exec("DELETE FROM search_meta");
2350
+ return index();
2351
+ }
2352
+ async function close() {
2353
+ if (db) {
2354
+ db.close();
2355
+ db = null;
2356
+ }
2357
+ }
2358
+ return {
2359
+ init,
2360
+ index,
2361
+ search,
2362
+ refresh,
2363
+ close
1244
2364
  };
1245
2365
  }
1246
2366
 
@@ -1335,13 +2455,15 @@ function createSleepCompressor() {
1335
2455
  function cosineSimilarity(text1, text2) {
1336
2456
  const words1 = tokenize(text1);
1337
2457
  const words2 = tokenize(text2);
1338
- const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
1339
2458
  const vec1 = {};
1340
2459
  const vec2 = {};
1341
- for (const word of vocab) {
1342
- vec1[word] = words1.filter((w) => w === word).length;
1343
- vec2[word] = words2.filter((w) => w === word).length;
2460
+ for (const word of words1) {
2461
+ vec1[word] = (vec1[word] ?? 0) + 1;
2462
+ }
2463
+ for (const word of words2) {
2464
+ vec2[word] = (vec2[word] ?? 0) + 1;
1344
2465
  }
2466
+ const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
1345
2467
  let dotProduct = 0;
1346
2468
  let mag1 = 0;
1347
2469
  let mag2 = 0;
@@ -1357,9 +2479,6 @@ function createSleepCompressor() {
1357
2479
  if (mag1 === 0 || mag2 === 0) return 0;
1358
2480
  return dotProduct / (mag1 * mag2);
1359
2481
  }
1360
- function tokenize(text) {
1361
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
1362
- }
1363
2482
  return {
1364
2483
  consolidate,
1365
2484
  findDuplicates,
@@ -1367,18 +2486,164 @@ function createSleepCompressor() {
1367
2486
  };
1368
2487
  }
1369
2488
 
2489
+ // src/core/workflow-planner.ts
2490
+ var import_node_crypto6 = require("crypto");
2491
+ var import_node_fs5 = require("fs");
2492
+ var import_node_path4 = require("path");
2493
+ function createWorkflowPlanner(projectRoot) {
2494
+ const plansDir = (0, import_node_path4.join)(projectRoot, ".sparn", "plans");
2495
+ if (!(0, import_node_fs5.existsSync)(plansDir)) {
2496
+ (0, import_node_fs5.mkdirSync)(plansDir, { recursive: true });
2497
+ }
2498
+ function sanitizeId(id) {
2499
+ return id.replace(/[/\\:.]/g, "").replace(/\.\./g, "");
2500
+ }
2501
+ function planPath(planId) {
2502
+ const safeId = sanitizeId(planId);
2503
+ if (!safeId) throw new Error("Invalid plan ID");
2504
+ return (0, import_node_path4.join)(plansDir, `plan-${safeId}.json`);
2505
+ }
2506
+ async function createPlan(taskDescription, filesNeeded, searchQueries, steps, tokenBudget = { planning: 0, estimated_execution: 0, max_file_reads: 5 }) {
2507
+ const id = (0, import_node_crypto6.randomUUID)().split("-")[0] || "plan";
2508
+ const plan = {
2509
+ id,
2510
+ created_at: Date.now(),
2511
+ task_description: taskDescription,
2512
+ steps: steps.map((s) => ({ ...s, status: "pending" })),
2513
+ token_budget: tokenBudget,
2514
+ files_needed: filesNeeded,
2515
+ search_queries: searchQueries,
2516
+ status: "draft"
2517
+ };
2518
+ (0, import_node_fs5.writeFileSync)(planPath(id), JSON.stringify(plan, null, 2), "utf-8");
2519
+ return plan;
2520
+ }
2521
+ async function loadPlan(planId) {
2522
+ const path = planPath(planId);
2523
+ if (!(0, import_node_fs5.existsSync)(path)) return null;
2524
+ try {
2525
+ return JSON.parse((0, import_node_fs5.readFileSync)(path, "utf-8"));
2526
+ } catch {
2527
+ return null;
2528
+ }
2529
+ }
2530
+ async function listPlans() {
2531
+ if (!(0, import_node_fs5.existsSync)(plansDir)) return [];
2532
+ const files = (0, import_node_fs5.readdirSync)(plansDir).filter((f) => f.startsWith("plan-") && f.endsWith(".json"));
2533
+ const plans = [];
2534
+ for (const file of files) {
2535
+ try {
2536
+ const plan = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path4.join)(plansDir, file), "utf-8"));
2537
+ plans.push({
2538
+ id: plan.id,
2539
+ task: plan.task_description,
2540
+ status: plan.status,
2541
+ created: plan.created_at
2542
+ });
2543
+ } catch {
2544
+ }
2545
+ }
2546
+ return plans.sort((a, b) => b.created - a.created);
2547
+ }
2548
+ async function updateStep(planId, stepOrder, status, result) {
2549
+ const plan = await loadPlan(planId);
2550
+ if (!plan) throw new Error(`Plan ${planId} not found`);
2551
+ const step = plan.steps.find((s) => s.order === stepOrder);
2552
+ if (!step) throw new Error(`Step ${stepOrder} not found in plan ${planId}`);
2553
+ step.status = status;
2554
+ if (result !== void 0) {
2555
+ step.result = result;
2556
+ }
2557
+ (0, import_node_fs5.writeFileSync)(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
2558
+ }
2559
+ async function startExec(planId) {
2560
+ const plan = await loadPlan(planId);
2561
+ if (!plan) throw new Error(`Plan ${planId} not found`);
2562
+ plan.status = "executing";
2563
+ (0, import_node_fs5.writeFileSync)(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
2564
+ return {
2565
+ maxFileReads: plan.token_budget.max_file_reads,
2566
+ tokenBudget: plan.token_budget.estimated_execution,
2567
+ allowReplan: false
2568
+ };
2569
+ }
2570
+ async function verify(planId) {
2571
+ const plan = await loadPlan(planId);
2572
+ if (!plan) throw new Error(`Plan ${planId} not found`);
2573
+ let stepsCompleted = 0;
2574
+ let stepsFailed = 0;
2575
+ let stepsSkipped = 0;
2576
+ let tokensUsed = 0;
2577
+ const details = [];
2578
+ for (const step of plan.steps) {
2579
+ switch (step.status) {
2580
+ case "completed":
2581
+ stepsCompleted++;
2582
+ tokensUsed += step.estimated_tokens;
2583
+ break;
2584
+ case "failed":
2585
+ stepsFailed++;
2586
+ break;
2587
+ case "skipped":
2588
+ stepsSkipped++;
2589
+ break;
2590
+ default:
2591
+ break;
2592
+ }
2593
+ details.push({
2594
+ step: step.order,
2595
+ action: step.action,
2596
+ target: step.target,
2597
+ status: step.status
2598
+ });
2599
+ }
2600
+ const totalSteps = plan.steps.length;
2601
+ const success = stepsFailed === 0 && stepsCompleted === totalSteps;
2602
+ const hasInProgress = plan.steps.some(
2603
+ (s) => s.status === "pending" || s.status === "in_progress"
2604
+ );
2605
+ if (!hasInProgress) {
2606
+ plan.status = success ? "completed" : "failed";
2607
+ (0, import_node_fs5.writeFileSync)(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
2608
+ }
2609
+ return {
2610
+ planId,
2611
+ stepsCompleted,
2612
+ stepsFailed,
2613
+ stepsSkipped,
2614
+ totalSteps,
2615
+ tokensBudgeted: plan.token_budget.estimated_execution,
2616
+ tokensUsed,
2617
+ success,
2618
+ details
2619
+ };
2620
+ }
2621
+ function getPlansDir() {
2622
+ return plansDir;
2623
+ }
2624
+ return {
2625
+ createPlan,
2626
+ loadPlan,
2627
+ listPlans,
2628
+ updateStep,
2629
+ startExec,
2630
+ verify,
2631
+ getPlansDir
2632
+ };
2633
+ }
2634
+
1370
2635
  // src/daemon/daemon-process.ts
1371
- var import_node_child_process = require("child_process");
1372
- var import_node_fs2 = require("fs");
1373
- var import_node_path = require("path");
2636
+ var import_node_child_process2 = require("child_process");
2637
+ var import_node_fs6 = require("fs");
2638
+ var import_node_path5 = require("path");
1374
2639
  var import_node_url = require("url");
1375
2640
  function createDaemonCommand() {
1376
2641
  function isDaemonRunning(pidFile) {
1377
- if (!(0, import_node_fs2.existsSync)(pidFile)) {
2642
+ if (!(0, import_node_fs6.existsSync)(pidFile)) {
1378
2643
  return { running: false };
1379
2644
  }
1380
2645
  try {
1381
- const pidStr = (0, import_node_fs2.readFileSync)(pidFile, "utf-8").trim();
2646
+ const pidStr = (0, import_node_fs6.readFileSync)(pidFile, "utf-8").trim();
1382
2647
  const pid = Number.parseInt(pidStr, 10);
1383
2648
  if (Number.isNaN(pid)) {
1384
2649
  return { running: false };
@@ -1387,7 +2652,7 @@ function createDaemonCommand() {
1387
2652
  process.kill(pid, 0);
1388
2653
  return { running: true, pid };
1389
2654
  } catch {
1390
- (0, import_node_fs2.unlinkSync)(pidFile);
2655
+ (0, import_node_fs6.unlinkSync)(pidFile);
1391
2656
  return { running: false };
1392
2657
  }
1393
2658
  } catch {
@@ -1395,15 +2660,15 @@ function createDaemonCommand() {
1395
2660
  }
1396
2661
  }
1397
2662
  function writePidFile(pidFile, pid) {
1398
- const dir = (0, import_node_path.dirname)(pidFile);
1399
- if (!(0, import_node_fs2.existsSync)(dir)) {
1400
- (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
2663
+ const dir = (0, import_node_path5.dirname)(pidFile);
2664
+ if (!(0, import_node_fs6.existsSync)(dir)) {
2665
+ (0, import_node_fs6.mkdirSync)(dir, { recursive: true });
1401
2666
  }
1402
- (0, import_node_fs2.writeFileSync)(pidFile, String(pid), "utf-8");
2667
+ (0, import_node_fs6.writeFileSync)(pidFile, String(pid), "utf-8");
1403
2668
  }
1404
2669
  function removePidFile(pidFile) {
1405
- if ((0, import_node_fs2.existsSync)(pidFile)) {
1406
- (0, import_node_fs2.unlinkSync)(pidFile);
2670
+ if ((0, import_node_fs6.existsSync)(pidFile)) {
2671
+ (0, import_node_fs6.unlinkSync)(pidFile);
1407
2672
  }
1408
2673
  }
1409
2674
  async function start(config) {
@@ -1419,17 +2684,61 @@ function createDaemonCommand() {
1419
2684
  }
1420
2685
  try {
1421
2686
  const __filename2 = (0, import_node_url.fileURLToPath)(importMetaUrl);
1422
- const __dirname = (0, import_node_path.dirname)(__filename2);
1423
- const daemonPath = (0, import_node_path.join)(__dirname, "index.js");
1424
- const child = (0, import_node_child_process.fork)(daemonPath, [], {
2687
+ const __dirname = (0, import_node_path5.dirname)(__filename2);
2688
+ const daemonPath = (0, import_node_path5.join)(__dirname, "..", "daemon", "index.js");
2689
+ const isWindows = process.platform === "win32";
2690
+ const childEnv = {
2691
+ ...process.env,
2692
+ SPARN_CONFIG: JSON.stringify(config),
2693
+ SPARN_PID_FILE: pidFile,
2694
+ SPARN_LOG_FILE: logFile
2695
+ };
2696
+ if (isWindows) {
2697
+ const configFile = (0, import_node_path5.join)((0, import_node_path5.dirname)(pidFile), "daemon-config.json");
2698
+ (0, import_node_fs6.writeFileSync)(configFile, JSON.stringify({ config, pidFile, logFile }), "utf-8");
2699
+ const launcherFile = (0, import_node_path5.join)((0, import_node_path5.dirname)(pidFile), "daemon-launcher.mjs");
2700
+ const launcherCode = [
2701
+ `import { readFileSync } from 'node:fs';`,
2702
+ `const cfg = JSON.parse(readFileSync(${JSON.stringify(configFile)}, 'utf-8'));`,
2703
+ `process.env.SPARN_CONFIG = JSON.stringify(cfg.config);`,
2704
+ `process.env.SPARN_PID_FILE = cfg.pidFile;`,
2705
+ `process.env.SPARN_LOG_FILE = cfg.logFile;`,
2706
+ `await import(${JSON.stringify(`file:///${daemonPath.replace(/\\/g, "/")}`)});`
2707
+ ].join("\n");
2708
+ (0, import_node_fs6.writeFileSync)(launcherFile, launcherCode, "utf-8");
2709
+ const ps = (0, import_node_child_process2.spawn)(
2710
+ "powershell.exe",
2711
+ [
2712
+ "-NoProfile",
2713
+ "-WindowStyle",
2714
+ "Hidden",
2715
+ "-Command",
2716
+ `Start-Process -FilePath '${process.execPath}' -ArgumentList '${launcherFile}' -WindowStyle Hidden`
2717
+ ],
2718
+ { stdio: "ignore", windowsHide: true }
2719
+ );
2720
+ ps.unref();
2721
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3));
2722
+ if ((0, import_node_fs6.existsSync)(pidFile)) {
2723
+ const pid = Number.parseInt((0, import_node_fs6.readFileSync)(pidFile, "utf-8").trim(), 10);
2724
+ if (!Number.isNaN(pid)) {
2725
+ return {
2726
+ success: true,
2727
+ pid,
2728
+ message: `Daemon started (PID ${pid})`
2729
+ };
2730
+ }
2731
+ }
2732
+ return {
2733
+ success: false,
2734
+ message: "Daemon failed to start (no PID file written)",
2735
+ error: "Timeout waiting for daemon PID"
2736
+ };
2737
+ }
2738
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [daemonPath], {
1425
2739
  detached: true,
1426
2740
  stdio: "ignore",
1427
- env: {
1428
- ...process.env,
1429
- SPARN_CONFIG: JSON.stringify(config),
1430
- SPARN_PID_FILE: pidFile,
1431
- SPARN_LOG_FILE: logFile
1432
- }
2741
+ env: childEnv
1433
2742
  });
1434
2743
  child.unref();
1435
2744
  if (child.pid) {
@@ -1463,14 +2772,19 @@ function createDaemonCommand() {
1463
2772
  };
1464
2773
  }
1465
2774
  try {
1466
- process.kill(status2.pid, "SIGTERM");
2775
+ const isWindows = process.platform === "win32";
2776
+ if (isWindows) {
2777
+ process.kill(status2.pid);
2778
+ } else {
2779
+ process.kill(status2.pid, "SIGTERM");
2780
+ }
1467
2781
  const maxWait = 5e3;
1468
2782
  const interval = 100;
1469
2783
  let waited = 0;
1470
2784
  while (waited < maxWait) {
1471
2785
  try {
1472
2786
  process.kill(status2.pid, 0);
1473
- await new Promise((resolve) => setTimeout(resolve, interval));
2787
+ await new Promise((resolve2) => setTimeout(resolve2, interval));
1474
2788
  waited += interval;
1475
2789
  } catch {
1476
2790
  removePidFile(pidFile);
@@ -1481,7 +2795,9 @@ function createDaemonCommand() {
1481
2795
  }
1482
2796
  }
1483
2797
  try {
1484
- process.kill(status2.pid, "SIGKILL");
2798
+ if (!isWindows) {
2799
+ process.kill(status2.pid, "SIGKILL");
2800
+ }
1485
2801
  removePidFile(pidFile);
1486
2802
  return {
1487
2803
  success: true,
@@ -1511,13 +2827,9 @@ function createDaemonCommand() {
1511
2827
  message: "Daemon not running"
1512
2828
  };
1513
2829
  }
1514
- const metrics = getMetrics().getSnapshot();
1515
2830
  return {
1516
2831
  running: true,
1517
2832
  pid: daemonStatus.pid,
1518
- uptime: metrics.daemon.uptime,
1519
- sessionsWatched: metrics.daemon.sessionsWatched,
1520
- tokensSaved: metrics.optimization.totalTokensSaved,
1521
2833
  message: `Daemon running (PID ${daemonStatus.pid})`
1522
2834
  };
1523
2835
  }
@@ -1529,12 +2841,12 @@ function createDaemonCommand() {
1529
2841
  }
1530
2842
 
1531
2843
  // src/daemon/file-tracker.ts
1532
- var import_node_fs3 = require("fs");
2844
+ var import_node_fs7 = require("fs");
1533
2845
  function createFileTracker() {
1534
2846
  const positions = /* @__PURE__ */ new Map();
1535
2847
  function readNewLines(filePath) {
1536
2848
  try {
1537
- const stats = (0, import_node_fs3.statSync)(filePath);
2849
+ const stats = (0, import_node_fs7.statSync)(filePath);
1538
2850
  const currentSize = stats.size;
1539
2851
  const currentModified = stats.mtimeMs;
1540
2852
  let pos = positions.get(filePath);
@@ -1555,9 +2867,14 @@ function createFileTracker() {
1555
2867
  }
1556
2868
  return [];
1557
2869
  }
1558
- const buffer = Buffer.alloc(currentSize - pos.position);
1559
- const fd = (0, import_node_fs3.readFileSync)(filePath);
1560
- fd.copy(buffer, 0, pos.position, currentSize);
2870
+ const bytesToRead = currentSize - pos.position;
2871
+ const buffer = Buffer.alloc(bytesToRead);
2872
+ const fd = (0, import_node_fs7.openSync)(filePath, "r");
2873
+ try {
2874
+ (0, import_node_fs7.readSync)(fd, buffer, 0, bytesToRead, pos.position);
2875
+ } finally {
2876
+ (0, import_node_fs7.closeSync)(fd);
2877
+ }
1561
2878
  const newContent = (pos.partialLine + buffer.toString("utf-8")).split("\n");
1562
2879
  const partialLine = newContent.pop() || "";
1563
2880
  pos.position = currentSize;
@@ -1591,9 +2908,9 @@ function createFileTracker() {
1591
2908
  }
1592
2909
 
1593
2910
  // src/daemon/session-watcher.ts
1594
- var import_node_fs4 = require("fs");
2911
+ var import_node_fs8 = require("fs");
1595
2912
  var import_node_os = require("os");
1596
- var import_node_path2 = require("path");
2913
+ var import_node_path6 = require("path");
1597
2914
  function createSessionWatcher(config) {
1598
2915
  const { config: sparnConfig, onOptimize, onError } = config;
1599
2916
  const { realtime, decay, states } = sparnConfig;
@@ -1602,7 +2919,7 @@ function createSessionWatcher(config) {
1602
2919
  const watchers = [];
1603
2920
  const debounceTimers = /* @__PURE__ */ new Map();
1604
2921
  function getProjectsDir() {
1605
- return (0, import_node_path2.join)((0, import_node_os.homedir)(), ".claude", "projects");
2922
+ return (0, import_node_path6.join)((0, import_node_os.homedir)(), ".claude", "projects");
1606
2923
  }
1607
2924
  function getSessionId(filePath) {
1608
2925
  const filename = filePath.split(/[/\\]/).pop() || "";
@@ -1619,6 +2936,7 @@ function createSessionWatcher(config) {
1619
2936
  fullOptimizationInterval: 50
1620
2937
  // Full re-optimization every 50 incremental updates
1621
2938
  });
2939
+ loadState(sessionId, pipeline);
1622
2940
  pipelines.set(sessionId, pipeline);
1623
2941
  }
1624
2942
  return pipeline;
@@ -1660,16 +2978,16 @@ function createSessionWatcher(config) {
1660
2978
  function findJsonlFiles(dir) {
1661
2979
  const files = [];
1662
2980
  try {
1663
- const entries = (0, import_node_fs4.readdirSync)(dir);
2981
+ const entries = (0, import_node_fs8.readdirSync)(dir);
1664
2982
  for (const entry of entries) {
1665
- const fullPath = (0, import_node_path2.join)(dir, entry);
1666
- const stat = (0, import_node_fs4.statSync)(fullPath);
2983
+ const fullPath = (0, import_node_path6.join)(dir, entry);
2984
+ const stat = (0, import_node_fs8.statSync)(fullPath);
1667
2985
  if (stat.isDirectory()) {
1668
2986
  files.push(...findJsonlFiles(fullPath));
1669
2987
  } else if (entry.endsWith(".jsonl")) {
1670
2988
  const matches = realtime.watchPatterns.some((pattern) => {
1671
2989
  const regex = new RegExp(
1672
- pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/\\\\]*").replace(/\./g, "\\.")
2990
+ pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/(?<!\.)(\*)/g, "[^/\\\\]*")
1673
2991
  );
1674
2992
  return regex.test(fullPath);
1675
2993
  });
@@ -1698,34 +3016,69 @@ function createSessionWatcher(config) {
1698
3016
  async function start() {
1699
3017
  const projectsDir = getProjectsDir();
1700
3018
  const jsonlFiles = findJsonlFiles(projectsDir);
1701
- const watchedDirs = /* @__PURE__ */ new Set();
1702
- for (const file of jsonlFiles) {
1703
- const dir = (0, import_node_path2.dirname)(file);
1704
- if (!watchedDirs.has(dir)) {
1705
- const watcher = (0, import_node_fs4.watch)(dir, { recursive: false }, (_eventType, filename) => {
1706
- if (filename?.endsWith(".jsonl")) {
1707
- const fullPath = (0, import_node_path2.join)(dir, filename);
1708
- handleFileChange(fullPath);
1709
- }
1710
- });
1711
- watchers.push(watcher);
1712
- watchedDirs.add(dir);
3019
+ try {
3020
+ const projectsWatcher = (0, import_node_fs8.watch)(projectsDir, { recursive: true }, (_eventType, filename) => {
3021
+ if (filename?.endsWith(".jsonl")) {
3022
+ const fullPath = (0, import_node_path6.join)(projectsDir, filename);
3023
+ handleFileChange(fullPath);
3024
+ }
3025
+ });
3026
+ watchers.push(projectsWatcher);
3027
+ } catch {
3028
+ const watchedDirs = /* @__PURE__ */ new Set();
3029
+ for (const file of jsonlFiles) {
3030
+ const dir = (0, import_node_path6.dirname)(file);
3031
+ if (!watchedDirs.has(dir)) {
3032
+ const watcher = (0, import_node_fs8.watch)(dir, { recursive: false }, (_eventType, filename) => {
3033
+ if (filename?.endsWith(".jsonl")) {
3034
+ const fullPath = (0, import_node_path6.join)(dir, filename);
3035
+ handleFileChange(fullPath);
3036
+ }
3037
+ });
3038
+ watchers.push(watcher);
3039
+ watchedDirs.add(dir);
3040
+ }
1713
3041
  }
1714
3042
  }
1715
- const projectsWatcher = (0, import_node_fs4.watch)(projectsDir, { recursive: true }, (_eventType, filename) => {
1716
- if (filename?.endsWith(".jsonl")) {
1717
- const fullPath = (0, import_node_path2.join)(projectsDir, filename);
1718
- handleFileChange(fullPath);
1719
- }
1720
- });
1721
- watchers.push(projectsWatcher);
1722
3043
  getMetrics().updateDaemon({
1723
3044
  startTime: Date.now(),
1724
3045
  sessionsWatched: jsonlFiles.length,
1725
3046
  memoryUsage: process.memoryUsage().heapUsed
1726
3047
  });
1727
3048
  }
3049
+ function getStatePath() {
3050
+ return (0, import_node_path6.join)((0, import_node_os.homedir)(), ".sparn", "optimizer-state.json");
3051
+ }
3052
+ function saveState() {
3053
+ try {
3054
+ const stateMap = {};
3055
+ for (const [sessionId, pipeline] of pipelines.entries()) {
3056
+ stateMap[sessionId] = pipeline.serializeOptimizerState();
3057
+ }
3058
+ const statePath = getStatePath();
3059
+ const dir = (0, import_node_path6.dirname)(statePath);
3060
+ if (!(0, import_node_fs8.existsSync)(dir)) {
3061
+ (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
3062
+ }
3063
+ (0, import_node_fs8.writeFileSync)(statePath, JSON.stringify(stateMap), "utf-8");
3064
+ } catch {
3065
+ }
3066
+ }
3067
+ function loadState(sessionId, pipeline) {
3068
+ try {
3069
+ const statePath = getStatePath();
3070
+ if (!(0, import_node_fs8.existsSync)(statePath)) return;
3071
+ const raw = (0, import_node_fs8.readFileSync)(statePath, "utf-8");
3072
+ const stateMap = JSON.parse(raw);
3073
+ const sessionState = stateMap[sessionId];
3074
+ if (sessionState) {
3075
+ pipeline.deserializeOptimizerState(sessionState);
3076
+ }
3077
+ } catch {
3078
+ }
3079
+ }
1728
3080
  function stop() {
3081
+ saveState();
1729
3082
  for (const watcher of watchers) {
1730
3083
  watcher.close();
1731
3084
  }
@@ -1795,8 +3148,8 @@ var DEFAULT_CONFIG = {
1795
3148
  },
1796
3149
  autoConsolidate: null,
1797
3150
  realtime: {
1798
- tokenBudget: 5e4,
1799
- autoOptimizeThreshold: 8e4,
3151
+ tokenBudget: 4e4,
3152
+ autoOptimizeThreshold: 6e4,
1800
3153
  watchPatterns: ["**/*.jsonl"],
1801
3154
  pidFile: ".sparn/daemon.pid",
1802
3155
  logFile: ".sparn/daemon.log",
@@ -1812,11 +3165,12 @@ function createSparnMcpServer(options) {
1812
3165
  const { memory, config = DEFAULT_CONFIG } = options;
1813
3166
  const server = new import_mcp.McpServer({
1814
3167
  name: "sparn",
1815
- version: "1.1.1"
3168
+ version: "1.4.0"
1816
3169
  });
1817
3170
  registerOptimizeTool(server, memory, config);
1818
3171
  registerStatsTool(server, memory);
1819
3172
  registerConsolidateTool(server, memory);
3173
+ registerSearchTool(server, memory);
1820
3174
  return server;
1821
3175
  }
1822
3176
  function registerOptimizeTool(server, memory, config) {
@@ -1824,7 +3178,7 @@ function registerOptimizeTool(server, memory, config) {
1824
3178
  "sparn_optimize",
1825
3179
  {
1826
3180
  title: "Sparn Optimize",
1827
- 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.",
3181
+ 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.",
1828
3182
  inputSchema: {
1829
3183
  context: import_zod.z.string().describe("The context text to optimize"),
1830
3184
  dryRun: import_zod.z.boolean().optional().default(false).describe("If true, do not persist changes to the memory store"),
@@ -1954,12 +3308,58 @@ function registerStatsTool(server, memory) {
1954
3308
  }
1955
3309
  );
1956
3310
  }
3311
+ function registerSearchTool(server, memory) {
3312
+ server.registerTool(
3313
+ "sparn_search",
3314
+ {
3315
+ title: "Sparn Search",
3316
+ description: "Search memory entries using full-text search. Returns matching entries with relevance ranking, score, and state information.",
3317
+ inputSchema: {
3318
+ query: import_zod.z.string().describe("Search query text"),
3319
+ limit: import_zod.z.number().int().min(1).max(100).optional().default(10).describe("Maximum number of results (1-100, default 10)")
3320
+ }
3321
+ },
3322
+ async ({ query, limit }) => {
3323
+ try {
3324
+ const results = await memory.searchFTS(query, limit);
3325
+ const response = results.map((r) => ({
3326
+ id: r.entry.id,
3327
+ content: r.entry.content.length > 500 ? `${r.entry.content.slice(0, 500)}...` : r.entry.content,
3328
+ score: r.entry.score,
3329
+ state: r.entry.state,
3330
+ rank: r.rank,
3331
+ tags: r.entry.tags,
3332
+ isBTSP: r.entry.isBTSP
3333
+ }));
3334
+ return {
3335
+ content: [
3336
+ {
3337
+ type: "text",
3338
+ text: JSON.stringify({ results: response, total: response.length }, null, 2)
3339
+ }
3340
+ ]
3341
+ };
3342
+ } catch (error) {
3343
+ const message = error instanceof Error ? error.message : String(error);
3344
+ return {
3345
+ content: [
3346
+ {
3347
+ type: "text",
3348
+ text: JSON.stringify({ error: message })
3349
+ }
3350
+ ],
3351
+ isError: true
3352
+ };
3353
+ }
3354
+ }
3355
+ );
3356
+ }
1957
3357
  function registerConsolidateTool(server, memory) {
1958
3358
  server.registerTool(
1959
3359
  "sparn_consolidate",
1960
3360
  {
1961
3361
  title: "Sparn Consolidate",
1962
- 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."
3362
+ description: "Run memory consolidation. Removes decayed entries and merges duplicates to reclaim space."
1963
3363
  },
1964
3364
  async () => {
1965
3365
  try {
@@ -2035,6 +3435,10 @@ function createLogger(verbose = false) {
2035
3435
  // Annotate the CommonJS export names for ESM import in node:
2036
3436
  0 && (module.exports = {
2037
3437
  DEFAULT_CONFIG,
3438
+ calculateIDF,
3439
+ calculateTF,
3440
+ calculateTFIDF,
3441
+ countTokensPrecise,
2038
3442
  createBTSPEmbedder,
2039
3443
  createBudgetPruner,
2040
3444
  createBudgetPrunerFromConfig,
@@ -2042,6 +3446,9 @@ function createLogger(verbose = false) {
2042
3446
  createConfidenceStates,
2043
3447
  createContextPipeline,
2044
3448
  createDaemonCommand,
3449
+ createDebtTracker,
3450
+ createDependencyGraph,
3451
+ createDocsGenerator,
2045
3452
  createEngramScorer,
2046
3453
  createEntry,
2047
3454
  createFileTracker,
@@ -2049,13 +3456,23 @@ function createLogger(verbose = false) {
2049
3456
  createIncrementalOptimizer,
2050
3457
  createKVMemory,
2051
3458
  createLogger,
3459
+ createMetricsCollector,
3460
+ createSearchEngine,
2052
3461
  createSessionWatcher,
2053
3462
  createSleepCompressor,
2054
3463
  createSparnMcpServer,
2055
3464
  createSparsePruner,
3465
+ createTFIDFIndex,
3466
+ createWorkflowPlanner,
2056
3467
  estimateTokens,
3468
+ getMetrics,
2057
3469
  hashContent,
2058
3470
  parseClaudeCodeContext,
2059
- parseGenericContext
3471
+ parseGenericContext,
3472
+ parseJSONLContext,
3473
+ parseJSONLLine,
3474
+ scoreTFIDF,
3475
+ setPreciseTokenCounting,
3476
+ tokenize
2060
3477
  });
2061
3478
  //# sourceMappingURL=index.cjs.map