@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.
- package/PRIVACY.md +1 -1
- package/README.md +136 -642
- package/SECURITY.md +1 -1
- package/dist/cli/dashboard.cjs +3977 -0
- package/dist/cli/dashboard.cjs.map +1 -0
- package/dist/cli/dashboard.d.cts +17 -0
- package/dist/cli/dashboard.d.ts +17 -0
- package/dist/cli/dashboard.js +3932 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/index.cjs +3855 -486
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +3812 -459
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/index.cjs +411 -99
- package/dist/daemon/index.cjs.map +1 -1
- package/dist/daemon/index.js +423 -103
- package/dist/daemon/index.js.map +1 -1
- package/dist/hooks/post-tool-result.cjs +129 -225
- package/dist/hooks/post-tool-result.cjs.map +1 -1
- package/dist/hooks/post-tool-result.js +129 -225
- package/dist/hooks/post-tool-result.js.map +1 -1
- package/dist/hooks/pre-prompt.cjs +206 -242
- package/dist/hooks/pre-prompt.cjs.map +1 -1
- package/dist/hooks/pre-prompt.js +192 -243
- package/dist/hooks/pre-prompt.js.map +1 -1
- package/dist/hooks/stop-docs-refresh.cjs +123 -0
- package/dist/hooks/stop-docs-refresh.cjs.map +1 -0
- package/dist/hooks/stop-docs-refresh.d.cts +1 -0
- package/dist/hooks/stop-docs-refresh.d.ts +1 -0
- package/dist/hooks/stop-docs-refresh.js +126 -0
- package/dist/hooks/stop-docs-refresh.js.map +1 -0
- package/dist/index.cjs +1756 -339
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +540 -41
- package/dist/index.d.ts +540 -41
- package/dist/index.js +1739 -331
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.cjs +306 -73
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +310 -73
- package/dist/mcp/index.js.map +1 -1
- 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/
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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/
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
|
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 =
|
|
384
|
-
|
|
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:
|
|
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.
|
|
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
|
|
483
|
-
|
|
484
|
-
content
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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(
|
|
687
|
-
if (
|
|
688
|
-
const
|
|
689
|
-
|
|
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
|
|
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(
|
|
712
|
-
p95Latency: calculatePercentile(
|
|
713
|
-
p99Latency: calculatePercentile(
|
|
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
|
|
955
|
-
const
|
|
956
|
-
const
|
|
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
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
|
1003
|
-
var
|
|
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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
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,
|
|
1821
|
+
if ((0, import_node_fs3.existsSync)(dbPath)) {
|
|
1036
1822
|
createBackup(dbPath);
|
|
1037
1823
|
console.log("Creating new database...");
|
|
1038
1824
|
}
|
|
1039
|
-
db = new
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
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
|
|
1342
|
-
vec1[word] =
|
|
1343
|
-
|
|
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
|
|
1372
|
-
var
|
|
1373
|
-
var
|
|
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,
|
|
2642
|
+
if (!(0, import_node_fs6.existsSync)(pidFile)) {
|
|
1378
2643
|
return { running: false };
|
|
1379
2644
|
}
|
|
1380
2645
|
try {
|
|
1381
|
-
const pidStr = (0,
|
|
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,
|
|
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,
|
|
1399
|
-
if (!(0,
|
|
1400
|
-
(0,
|
|
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,
|
|
2667
|
+
(0, import_node_fs6.writeFileSync)(pidFile, String(pid), "utf-8");
|
|
1403
2668
|
}
|
|
1404
2669
|
function removePidFile(pidFile) {
|
|
1405
|
-
if ((0,
|
|
1406
|
-
(0,
|
|
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,
|
|
1423
|
-
const daemonPath = (0,
|
|
1424
|
-
const
|
|
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.
|
|
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((
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
1559
|
-
const
|
|
1560
|
-
fd
|
|
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
|
|
2911
|
+
var import_node_fs8 = require("fs");
|
|
1595
2912
|
var import_node_os = require("os");
|
|
1596
|
-
var
|
|
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,
|
|
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,
|
|
2981
|
+
const entries = (0, import_node_fs8.readdirSync)(dir);
|
|
1664
2982
|
for (const entry of entries) {
|
|
1665
|
-
const fullPath = (0,
|
|
1666
|
-
const stat = (0,
|
|
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(
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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:
|
|
1799
|
-
autoOptimizeThreshold:
|
|
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.
|
|
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
|
|
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
|
|
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
|