@ulrichc1/sparn 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/PRIVACY.md +1 -1
  2. package/README.md +136 -642
  3. package/SECURITY.md +1 -1
  4. package/dist/cli/dashboard.cjs +3977 -0
  5. package/dist/cli/dashboard.cjs.map +1 -0
  6. package/dist/cli/dashboard.d.cts +17 -0
  7. package/dist/cli/dashboard.d.ts +17 -0
  8. package/dist/cli/dashboard.js +3932 -0
  9. package/dist/cli/dashboard.js.map +1 -0
  10. package/dist/cli/index.cjs +3855 -486
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +3812 -459
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/daemon/index.cjs +411 -99
  15. package/dist/daemon/index.cjs.map +1 -1
  16. package/dist/daemon/index.js +423 -103
  17. package/dist/daemon/index.js.map +1 -1
  18. package/dist/hooks/post-tool-result.cjs +129 -225
  19. package/dist/hooks/post-tool-result.cjs.map +1 -1
  20. package/dist/hooks/post-tool-result.js +129 -225
  21. package/dist/hooks/post-tool-result.js.map +1 -1
  22. package/dist/hooks/pre-prompt.cjs +206 -242
  23. package/dist/hooks/pre-prompt.cjs.map +1 -1
  24. package/dist/hooks/pre-prompt.js +192 -243
  25. package/dist/hooks/pre-prompt.js.map +1 -1
  26. package/dist/hooks/stop-docs-refresh.cjs +123 -0
  27. package/dist/hooks/stop-docs-refresh.cjs.map +1 -0
  28. package/dist/hooks/stop-docs-refresh.d.cts +1 -0
  29. package/dist/hooks/stop-docs-refresh.d.ts +1 -0
  30. package/dist/hooks/stop-docs-refresh.js +126 -0
  31. package/dist/hooks/stop-docs-refresh.js.map +1 -0
  32. package/dist/index.cjs +1756 -339
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +540 -41
  35. package/dist/index.d.ts +540 -41
  36. package/dist/index.js +1739 -331
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp/index.cjs +306 -73
  39. package/dist/mcp/index.cjs.map +1 -1
  40. package/dist/mcp/index.js +310 -73
  41. package/dist/mcp/index.js.map +1 -1
  42. package/package.json +10 -3
@@ -0,0 +1,3932 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // node_modules/tsup/assets/esm_shims.js
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ var init_esm_shims = __esm({
15
+ "node_modules/tsup/assets/esm_shims.js"() {
16
+ "use strict";
17
+ }
18
+ });
19
+
20
+ // src/utils/hash.ts
21
+ import { createHash } from "crypto";
22
+ function hashContent(content) {
23
+ return createHash("sha256").update(content, "utf8").digest("hex");
24
+ }
25
+ var init_hash = __esm({
26
+ "src/utils/hash.ts"() {
27
+ "use strict";
28
+ init_esm_shims();
29
+ }
30
+ });
31
+
32
+ // src/core/btsp-embedder.ts
33
+ import { randomUUID } from "crypto";
34
+ function createBTSPEmbedder(config) {
35
+ const BTSP_PATTERNS = [
36
+ // Error patterns
37
+ /\b(error|exception|failure|fatal|critical|panic)\b/i,
38
+ /\b(TypeError|ReferenceError|SyntaxError|RangeError|URIError)\b/,
39
+ /\bENOENT|EACCES|ECONNREFUSED|ETIMEDOUT\b/,
40
+ // Stack trace patterns
41
+ /^\s+at\s+.*\(.*:\d+:\d+\)/m,
42
+ // JavaScript stack trace
43
+ /^\s+at\s+.*\.[a-zA-Z]+:\d+/m,
44
+ // Python/Ruby stack trace
45
+ // Git diff new files
46
+ /^new file mode \d+$/m,
47
+ /^--- \/dev\/null$/m,
48
+ // Merge conflict markers
49
+ /^<<<<<<< /m,
50
+ /^=======/m,
51
+ /^>>>>>>> /m
52
+ ];
53
+ if (config?.customPatterns) {
54
+ for (const pattern of config.customPatterns) {
55
+ try {
56
+ BTSP_PATTERNS.push(new RegExp(pattern));
57
+ } catch {
58
+ }
59
+ }
60
+ }
61
+ function detectBTSP(content) {
62
+ return BTSP_PATTERNS.some((pattern) => pattern.test(content));
63
+ }
64
+ function createBTSPEntry(content, tags = [], metadata = {}) {
65
+ return {
66
+ id: randomUUID(),
67
+ content,
68
+ hash: hashContent(content),
69
+ timestamp: Date.now(),
70
+ score: 1,
71
+ // Maximum initial score
72
+ ttl: 365 * 24 * 3600,
73
+ // 1 year in seconds (long retention)
74
+ state: "active",
75
+ // Always active
76
+ accessCount: 0,
77
+ tags: [...tags, "btsp"],
78
+ metadata,
79
+ isBTSP: true
80
+ };
81
+ }
82
+ return {
83
+ detectBTSP,
84
+ createBTSPEntry
85
+ };
86
+ }
87
+ var init_btsp_embedder = __esm({
88
+ "src/core/btsp-embedder.ts"() {
89
+ "use strict";
90
+ init_esm_shims();
91
+ init_hash();
92
+ }
93
+ });
94
+
95
+ // src/core/confidence-states.ts
96
+ function createConfidenceStates(config) {
97
+ const { activeThreshold, readyThreshold } = config;
98
+ function calculateState(entry) {
99
+ if (entry.isBTSP) {
100
+ return "active";
101
+ }
102
+ if (entry.score >= activeThreshold) {
103
+ return "active";
104
+ }
105
+ if (entry.score >= readyThreshold) {
106
+ return "ready";
107
+ }
108
+ return "silent";
109
+ }
110
+ function transition(entry) {
111
+ const newState = calculateState(entry);
112
+ return {
113
+ ...entry,
114
+ state: newState
115
+ };
116
+ }
117
+ function getDistribution(entries) {
118
+ const distribution = {
119
+ silent: 0,
120
+ ready: 0,
121
+ active: 0,
122
+ total: entries.length
123
+ };
124
+ for (const entry of entries) {
125
+ const state = calculateState(entry);
126
+ distribution[state]++;
127
+ }
128
+ return distribution;
129
+ }
130
+ return {
131
+ calculateState,
132
+ transition,
133
+ getDistribution
134
+ };
135
+ }
136
+ var init_confidence_states = __esm({
137
+ "src/core/confidence-states.ts"() {
138
+ "use strict";
139
+ init_esm_shims();
140
+ }
141
+ });
142
+
143
+ // src/core/engram-scorer.ts
144
+ function createEngramScorer(config) {
145
+ const { defaultTTL } = config;
146
+ const recencyWindowMs = (config.recencyBoostMinutes ?? 30) * 60 * 1e3;
147
+ const recencyMultiplier = config.recencyBoostMultiplier ?? 1.3;
148
+ function calculateDecay(ageInSeconds, ttlInSeconds) {
149
+ if (ttlInSeconds === 0) return 1;
150
+ if (ageInSeconds <= 0) return 0;
151
+ const ratio = ageInSeconds / ttlInSeconds;
152
+ const decay = 1 - Math.exp(-ratio);
153
+ return Math.max(0, Math.min(1, decay));
154
+ }
155
+ function calculateScore(entry, currentTime = Date.now()) {
156
+ const ageInMilliseconds = currentTime - entry.timestamp;
157
+ const ageInSeconds = Math.max(0, ageInMilliseconds / 1e3);
158
+ const decay = calculateDecay(ageInSeconds, entry.ttl);
159
+ let score = entry.score * (1 - decay);
160
+ if (entry.accessCount > 0) {
161
+ const accessBonus = Math.log(entry.accessCount + 1) * 0.1;
162
+ score = Math.min(1, score + accessBonus);
163
+ }
164
+ if (entry.isBTSP) {
165
+ score = Math.max(score, 0.9);
166
+ }
167
+ if (!entry.isBTSP && recencyWindowMs > 0) {
168
+ const ageMs = currentTime - entry.timestamp;
169
+ if (ageMs >= 0 && ageMs < recencyWindowMs) {
170
+ const boostFactor = 1 + (recencyMultiplier - 1) * (1 - ageMs / recencyWindowMs);
171
+ score = score * boostFactor;
172
+ }
173
+ }
174
+ return Math.max(0, Math.min(1, score));
175
+ }
176
+ function refreshTTL(entry) {
177
+ return {
178
+ ...entry,
179
+ ttl: defaultTTL * 3600,
180
+ // Convert hours to seconds
181
+ timestamp: Date.now()
182
+ };
183
+ }
184
+ return {
185
+ calculateScore,
186
+ refreshTTL,
187
+ calculateDecay
188
+ };
189
+ }
190
+ var init_engram_scorer = __esm({
191
+ "src/core/engram-scorer.ts"() {
192
+ "use strict";
193
+ init_esm_shims();
194
+ }
195
+ });
196
+
197
+ // src/utils/tfidf.ts
198
+ function tokenize2(text) {
199
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
200
+ }
201
+ function calculateTF(term, tokens) {
202
+ const count = tokens.filter((t) => t === term).length;
203
+ return Math.sqrt(count);
204
+ }
205
+ function createTFIDFIndex(entries) {
206
+ const documentFrequency = /* @__PURE__ */ new Map();
207
+ for (const entry of entries) {
208
+ const tokens = tokenize2(entry.content);
209
+ const uniqueTerms = new Set(tokens);
210
+ for (const term of uniqueTerms) {
211
+ documentFrequency.set(term, (documentFrequency.get(term) || 0) + 1);
212
+ }
213
+ }
214
+ return {
215
+ documentFrequency,
216
+ totalDocuments: entries.length
217
+ };
218
+ }
219
+ function scoreTFIDF(entry, index) {
220
+ const tokens = tokenize2(entry.content);
221
+ if (tokens.length === 0) return 0;
222
+ const uniqueTerms = new Set(tokens);
223
+ let totalScore = 0;
224
+ for (const term of uniqueTerms) {
225
+ const tf = calculateTF(term, tokens);
226
+ const docsWithTerm = index.documentFrequency.get(term) || 0;
227
+ if (docsWithTerm === 0) continue;
228
+ const idf = Math.log(index.totalDocuments / docsWithTerm);
229
+ totalScore += tf * idf;
230
+ }
231
+ return totalScore / tokens.length;
232
+ }
233
+ var init_tfidf = __esm({
234
+ "src/utils/tfidf.ts"() {
235
+ "use strict";
236
+ init_esm_shims();
237
+ }
238
+ });
239
+
240
+ // src/utils/tokenizer.ts
241
+ import { encode } from "gpt-tokenizer";
242
+ function estimateTokens(text) {
243
+ if (!text || text.length === 0) {
244
+ return 0;
245
+ }
246
+ if (usePrecise) {
247
+ return encode(text).length;
248
+ }
249
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
250
+ const wordCount = words.length;
251
+ const charCount = text.length;
252
+ const charEstimate = Math.ceil(charCount / 4);
253
+ const wordEstimate = Math.ceil(wordCount * 0.75);
254
+ return Math.max(wordEstimate, charEstimate);
255
+ }
256
+ var usePrecise;
257
+ var init_tokenizer = __esm({
258
+ "src/utils/tokenizer.ts"() {
259
+ "use strict";
260
+ init_esm_shims();
261
+ usePrecise = false;
262
+ }
263
+ });
264
+
265
+ // src/core/sparse-pruner.ts
266
+ function createSparsePruner(config) {
267
+ const { threshold } = config;
268
+ function scoreEntry(entry, allEntries) {
269
+ return scoreTFIDF(entry, createTFIDFIndex(allEntries));
270
+ }
271
+ function prune(entries) {
272
+ if (entries.length === 0) {
273
+ return {
274
+ kept: [],
275
+ removed: [],
276
+ originalTokens: 0,
277
+ prunedTokens: 0
278
+ };
279
+ }
280
+ const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
281
+ const tfidfIndex = createTFIDFIndex(entries);
282
+ const scored = entries.map((entry) => ({
283
+ entry,
284
+ score: scoreTFIDF(entry, tfidfIndex)
285
+ }));
286
+ scored.sort((a, b) => b.score - a.score);
287
+ const keepCount = Math.max(1, Math.ceil(entries.length * (threshold / 100)));
288
+ const kept = scored.slice(0, keepCount).map((s) => s.entry);
289
+ const removed = scored.slice(keepCount).map((s) => s.entry);
290
+ const prunedTokens = kept.reduce((sum, e) => sum + estimateTokens(e.content), 0);
291
+ return {
292
+ kept,
293
+ removed,
294
+ originalTokens,
295
+ prunedTokens
296
+ };
297
+ }
298
+ return {
299
+ prune,
300
+ scoreEntry
301
+ };
302
+ }
303
+ var init_sparse_pruner = __esm({
304
+ "src/core/sparse-pruner.ts"() {
305
+ "use strict";
306
+ init_esm_shims();
307
+ init_tfidf();
308
+ init_tokenizer();
309
+ }
310
+ });
311
+
312
+ // src/adapters/generic.ts
313
+ import { randomUUID as randomUUID2 } from "crypto";
314
+ function createGenericAdapter(memory, config) {
315
+ const pruner = createSparsePruner(config.pruning);
316
+ const scorer = createEngramScorer(config.decay);
317
+ const states = createConfidenceStates(config.states);
318
+ const btsp = createBTSPEmbedder({ customPatterns: config.btspPatterns });
319
+ async function optimize(context, options = {}) {
320
+ const startTime = Date.now();
321
+ const lines = context.split("\n").filter((line) => line.trim().length > 0);
322
+ const now = Date.now();
323
+ const entries = lines.map((content, index) => {
324
+ const isBTSP = btsp.detectBTSP(content);
325
+ return {
326
+ id: randomUUID2(),
327
+ content,
328
+ hash: hashContent(content),
329
+ timestamp: now + index,
330
+ // Unique timestamps preserve ordering
331
+ score: isBTSP ? 1 : 0.5,
332
+ ttl: config.decay.defaultTTL * 3600,
333
+ state: "ready",
334
+ accessCount: 0,
335
+ tags: [],
336
+ metadata: {},
337
+ isBTSP
338
+ };
339
+ });
340
+ const tokensBefore = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
341
+ const scoredEntries = entries.map((entry) => ({
342
+ ...entry,
343
+ score: scorer.calculateScore(entry)
344
+ }));
345
+ const statedEntries = scoredEntries.map((entry) => states.transition(entry));
346
+ const pruneResult = pruner.prune(statedEntries);
347
+ const optimizedEntries = pruneResult.kept.filter(
348
+ (e) => e.state === "active" || e.state === "ready"
349
+ );
350
+ const tokensAfter = optimizedEntries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
351
+ const optimizedContext = optimizedEntries.map((e) => e.content).join("\n");
352
+ if (!options.dryRun) {
353
+ for (const entry of optimizedEntries) {
354
+ await memory.put(entry);
355
+ }
356
+ await memory.recordOptimization({
357
+ timestamp: Date.now(),
358
+ tokens_before: tokensBefore,
359
+ tokens_after: tokensAfter,
360
+ entries_pruned: entries.length - optimizedEntries.length,
361
+ duration_ms: Date.now() - startTime
362
+ });
363
+ }
364
+ const distribution = states.getDistribution(optimizedEntries);
365
+ const result = {
366
+ optimizedContext,
367
+ tokensBefore,
368
+ tokensAfter,
369
+ reduction: tokensBefore > 0 ? (tokensBefore - tokensAfter) / tokensBefore : 0,
370
+ entriesProcessed: entries.length,
371
+ entriesKept: optimizedEntries.length,
372
+ stateDistribution: distribution,
373
+ durationMs: Date.now() - startTime
374
+ };
375
+ if (options.verbose) {
376
+ result.details = optimizedEntries.map((e) => ({
377
+ id: e.id,
378
+ score: e.score,
379
+ state: e.state,
380
+ isBTSP: e.isBTSP,
381
+ tokens: estimateTokens(e.content)
382
+ }));
383
+ }
384
+ return result;
385
+ }
386
+ return {
387
+ optimize
388
+ };
389
+ }
390
+ var init_generic = __esm({
391
+ "src/adapters/generic.ts"() {
392
+ "use strict";
393
+ init_esm_shims();
394
+ init_btsp_embedder();
395
+ init_confidence_states();
396
+ init_engram_scorer();
397
+ init_sparse_pruner();
398
+ init_hash();
399
+ init_tokenizer();
400
+ }
401
+ });
402
+
403
+ // src/types/config.ts
404
+ var DEFAULT_CONFIG;
405
+ var init_config = __esm({
406
+ "src/types/config.ts"() {
407
+ "use strict";
408
+ init_esm_shims();
409
+ DEFAULT_CONFIG = {
410
+ pruning: {
411
+ threshold: 5,
412
+ aggressiveness: 50
413
+ },
414
+ decay: {
415
+ defaultTTL: 24,
416
+ decayThreshold: 0.95
417
+ },
418
+ states: {
419
+ activeThreshold: 0.7,
420
+ readyThreshold: 0.3
421
+ },
422
+ agent: "generic",
423
+ ui: {
424
+ colors: true,
425
+ sounds: false,
426
+ verbose: false
427
+ },
428
+ autoConsolidate: null,
429
+ realtime: {
430
+ tokenBudget: 4e4,
431
+ autoOptimizeThreshold: 6e4,
432
+ watchPatterns: ["**/*.jsonl"],
433
+ pidFile: ".sparn/daemon.pid",
434
+ logFile: ".sparn/daemon.log",
435
+ debounceMs: 5e3,
436
+ incremental: true,
437
+ windowSize: 500,
438
+ consolidationInterval: null
439
+ }
440
+ };
441
+ }
442
+ });
443
+
444
+ // src/cli/commands/optimize.ts
445
+ var optimize_exports = {};
446
+ __export(optimize_exports, {
447
+ optimizeCommand: () => optimizeCommand
448
+ });
449
+ import { readFile, writeFile } from "fs/promises";
450
+ async function optimizeCommand(options) {
451
+ const { memory, dryRun = false, verbose = false } = options;
452
+ let input;
453
+ if (options.inputFile) {
454
+ input = await readFile(options.inputFile, "utf-8");
455
+ } else if (options.input) {
456
+ input = options.input;
457
+ } else {
458
+ throw new Error("No input provided. Use --input or --input-file");
459
+ }
460
+ const adapter = createGenericAdapter(memory, DEFAULT_CONFIG);
461
+ const result = await adapter.optimize(input, { dryRun, verbose });
462
+ if (options.outputFile) {
463
+ await writeFile(options.outputFile, result.optimizedContext, "utf-8");
464
+ }
465
+ return {
466
+ ...result,
467
+ output: result.optimizedContext,
468
+ outputFile: options.outputFile
469
+ };
470
+ }
471
+ var init_optimize = __esm({
472
+ "src/cli/commands/optimize.ts"() {
473
+ "use strict";
474
+ init_esm_shims();
475
+ init_generic();
476
+ init_config();
477
+ }
478
+ });
479
+
480
+ // src/cli/commands/stats.ts
481
+ var stats_exports = {};
482
+ __export(stats_exports, {
483
+ statsCommand: () => statsCommand
484
+ });
485
+ async function statsCommand(options) {
486
+ const { memory, graph, reset, confirmReset, json } = options;
487
+ if (reset) {
488
+ if (confirmReset) {
489
+ await memory.clearOptimizationStats();
490
+ return {
491
+ totalCommands: 0,
492
+ totalTokensSaved: 0,
493
+ averageReduction: 0,
494
+ resetConfirmed: true
495
+ };
496
+ }
497
+ }
498
+ const stats = await memory.getOptimizationStats();
499
+ const totalCommands = stats.length;
500
+ const totalTokensSaved = stats.reduce((sum, s) => sum + (s.tokens_before - s.tokens_after), 0);
501
+ const averageReduction = totalCommands > 0 ? stats.reduce((sum, s) => {
502
+ const reduction = s.tokens_before > 0 ? (s.tokens_before - s.tokens_after) / s.tokens_before : 0;
503
+ return sum + reduction;
504
+ }, 0) / totalCommands : 0;
505
+ const result = {
506
+ totalCommands,
507
+ totalTokensSaved,
508
+ averageReduction
509
+ };
510
+ if (graph && totalCommands > 0) {
511
+ result.graph = generateBarChart(stats);
512
+ }
513
+ if (json) {
514
+ result.json = JSON.stringify(
515
+ {
516
+ totalCommands,
517
+ totalTokensSaved,
518
+ averageReduction: Math.round(averageReduction * 1e3) / 10,
519
+ // Convert to percentage
520
+ optimizations: stats.map((s) => ({
521
+ timestamp: s.timestamp,
522
+ tokensBefore: s.tokens_before,
523
+ tokensAfter: s.tokens_after,
524
+ entriesPruned: s.entries_pruned,
525
+ durationMs: s.duration_ms,
526
+ reduction: Math.round((s.tokens_before - s.tokens_after) / s.tokens_before * 1e3) / 10
527
+ }))
528
+ },
529
+ null,
530
+ 2
531
+ );
532
+ }
533
+ return result;
534
+ }
535
+ function generateBarChart(stats) {
536
+ const maxBars = 20;
537
+ const recentStats = stats.slice(0, maxBars);
538
+ const lines = [];
539
+ lines.push("\nOptimization History (most recent first):\n");
540
+ const maxReduction = Math.max(...recentStats.map((s) => s.tokens_before - s.tokens_after));
541
+ for (let i = 0; i < recentStats.length; i++) {
542
+ const s = recentStats[i];
543
+ if (!s) continue;
544
+ const reduction = s.tokens_before - s.tokens_after;
545
+ const reductionPct = s.tokens_before > 0 ? reduction / s.tokens_before * 100 : 0;
546
+ const barLength = Math.round(reduction / maxReduction * 40);
547
+ const bar = "\u2588".repeat(barLength);
548
+ const date = new Date(s.timestamp);
549
+ const timeStr = date.toLocaleTimeString();
550
+ lines.push(`${timeStr} \u2502 ${bar} ${reductionPct.toFixed(1)}%`);
551
+ }
552
+ return lines.join("\n");
553
+ }
554
+ var init_stats = __esm({
555
+ "src/cli/commands/stats.ts"() {
556
+ "use strict";
557
+ init_esm_shims();
558
+ }
559
+ });
560
+
561
+ // src/core/sleep-compressor.ts
562
+ function createSleepCompressor() {
563
+ const scorer = createEngramScorer({ defaultTTL: 24, decayThreshold: 0.95 });
564
+ function consolidate(entries) {
565
+ const startTime = Date.now();
566
+ const originalCount = entries.length;
567
+ const now = Date.now();
568
+ const nonDecayed = entries.filter((entry) => {
569
+ const ageInSeconds = (now - entry.timestamp) / 1e3;
570
+ const decay = scorer.calculateDecay(ageInSeconds, entry.ttl);
571
+ return decay < 0.95;
572
+ });
573
+ const decayedRemoved = originalCount - nonDecayed.length;
574
+ const duplicateGroups = findDuplicates(nonDecayed);
575
+ const merged = mergeDuplicates(duplicateGroups);
576
+ const duplicateIds = new Set(duplicateGroups.flatMap((g) => g.entries.map((e) => e.id)));
577
+ const nonDuplicates = nonDecayed.filter((e) => !duplicateIds.has(e.id));
578
+ const kept = [...merged, ...nonDuplicates];
579
+ const removed = entries.filter((e) => !kept.some((k) => k.id === e.id));
580
+ const duplicatesRemoved = duplicateGroups.reduce((sum, g) => sum + (g.entries.length - 1), 0);
581
+ return {
582
+ kept,
583
+ removed,
584
+ entriesBefore: originalCount,
585
+ entriesAfter: kept.length,
586
+ decayedRemoved,
587
+ duplicatesRemoved,
588
+ compressionRatio: originalCount > 0 ? kept.length / originalCount : 0,
589
+ durationMs: Date.now() - startTime
590
+ };
591
+ }
592
+ function findDuplicates(entries) {
593
+ const groups = [];
594
+ const processed = /* @__PURE__ */ new Set();
595
+ for (let i = 0; i < entries.length; i++) {
596
+ const entry = entries[i];
597
+ if (!entry || processed.has(entry.id)) continue;
598
+ const duplicates = entries.filter((e, idx) => idx !== i && e.hash === entry.hash);
599
+ if (duplicates.length > 0) {
600
+ const group = {
601
+ entries: [entry, ...duplicates],
602
+ similarity: 1
603
+ // Exact match
604
+ };
605
+ groups.push(group);
606
+ processed.add(entry.id);
607
+ for (const dup of duplicates) {
608
+ processed.add(dup.id);
609
+ }
610
+ }
611
+ }
612
+ for (let i = 0; i < entries.length; i++) {
613
+ const entryI = entries[i];
614
+ if (!entryI || processed.has(entryI.id)) continue;
615
+ for (let j = i + 1; j < entries.length; j++) {
616
+ const entryJ = entries[j];
617
+ if (!entryJ || processed.has(entryJ.id)) continue;
618
+ const similarity = cosineSimilarity(entryI.content, entryJ.content);
619
+ if (similarity >= 0.85) {
620
+ const group = {
621
+ entries: [entryI, entryJ],
622
+ similarity
623
+ };
624
+ groups.push(group);
625
+ processed.add(entryI.id);
626
+ processed.add(entryJ.id);
627
+ break;
628
+ }
629
+ }
630
+ }
631
+ return groups;
632
+ }
633
+ function mergeDuplicates(groups) {
634
+ const merged = [];
635
+ for (const group of groups) {
636
+ const sorted = [...group.entries].sort((a, b) => b.score - a.score);
637
+ const best = sorted[0];
638
+ if (!best) continue;
639
+ const totalAccessCount = group.entries.reduce((sum, e) => sum + e.accessCount, 0);
640
+ const allTags = new Set(group.entries.flatMap((e) => e.tags));
641
+ merged.push({
642
+ ...best,
643
+ accessCount: totalAccessCount,
644
+ tags: Array.from(allTags)
645
+ });
646
+ }
647
+ return merged;
648
+ }
649
+ function cosineSimilarity(text1, text2) {
650
+ const words1 = tokenize2(text1);
651
+ const words2 = tokenize2(text2);
652
+ const vec1 = {};
653
+ const vec2 = {};
654
+ for (const word of words1) {
655
+ vec1[word] = (vec1[word] ?? 0) + 1;
656
+ }
657
+ for (const word of words2) {
658
+ vec2[word] = (vec2[word] ?? 0) + 1;
659
+ }
660
+ const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
661
+ let dotProduct = 0;
662
+ let mag1 = 0;
663
+ let mag2 = 0;
664
+ for (const word of vocab) {
665
+ const count1 = vec1[word] ?? 0;
666
+ const count2 = vec2[word] ?? 0;
667
+ dotProduct += count1 * count2;
668
+ mag1 += count1 * count1;
669
+ mag2 += count2 * count2;
670
+ }
671
+ mag1 = Math.sqrt(mag1);
672
+ mag2 = Math.sqrt(mag2);
673
+ if (mag1 === 0 || mag2 === 0) return 0;
674
+ return dotProduct / (mag1 * mag2);
675
+ }
676
+ return {
677
+ consolidate,
678
+ findDuplicates,
679
+ mergeDuplicates
680
+ };
681
+ }
682
+ var init_sleep_compressor = __esm({
683
+ "src/core/sleep-compressor.ts"() {
684
+ "use strict";
685
+ init_esm_shims();
686
+ init_tfidf();
687
+ init_engram_scorer();
688
+ }
689
+ });
690
+
691
+ // src/cli/commands/consolidate.ts
692
+ var consolidate_exports = {};
693
+ __export(consolidate_exports, {
694
+ consolidateCommand: () => consolidateCommand
695
+ });
696
+ async function consolidateCommand(options) {
697
+ const { memory } = options;
698
+ const allIds = await memory.list();
699
+ const allEntries = await Promise.all(
700
+ allIds.map(async (id) => {
701
+ const entry = await memory.get(id);
702
+ return entry;
703
+ })
704
+ );
705
+ const entries = allEntries.filter((e) => e !== null);
706
+ const compressor = createSleepCompressor();
707
+ const result = compressor.consolidate(entries);
708
+ for (const removed of result.removed) {
709
+ await memory.delete(removed.id);
710
+ }
711
+ for (const kept of result.kept) {
712
+ await memory.put(kept);
713
+ }
714
+ await memory.compact();
715
+ return {
716
+ entriesBefore: result.entriesBefore,
717
+ entriesAfter: result.entriesAfter,
718
+ decayedRemoved: result.decayedRemoved,
719
+ duplicatesRemoved: result.duplicatesRemoved,
720
+ compressionRatio: result.compressionRatio,
721
+ durationMs: result.durationMs,
722
+ vacuumCompleted: true
723
+ };
724
+ }
725
+ var init_consolidate = __esm({
726
+ "src/cli/commands/consolidate.ts"() {
727
+ "use strict";
728
+ init_esm_shims();
729
+ init_sleep_compressor();
730
+ }
731
+ });
732
+
733
+ // src/core/search-engine.ts
734
+ import { execFileSync, execSync } from "child_process";
735
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
736
+ import { extname, join, relative } from "path";
737
+ import Database from "better-sqlite3";
738
+ function hasRipgrep() {
739
+ try {
740
+ execSync("rg --version", { stdio: "pipe" });
741
+ return true;
742
+ } catch {
743
+ return false;
744
+ }
745
+ }
746
+ function ripgrepSearch(query, projectRoot, opts = {}) {
747
+ try {
748
+ const args = ["--line-number", "--no-heading", "--color=never"];
749
+ if (opts.fileGlob) {
750
+ args.push("--glob", opts.fileGlob);
751
+ }
752
+ args.push(
753
+ "--glob",
754
+ "!node_modules",
755
+ "--glob",
756
+ "!dist",
757
+ "--glob",
758
+ "!.git",
759
+ "--glob",
760
+ "!.sparn",
761
+ "--glob",
762
+ "!coverage"
763
+ );
764
+ const maxResults = opts.maxResults || 20;
765
+ args.push("--max-count", String(maxResults));
766
+ if (opts.includeContext) {
767
+ args.push("-C", "2");
768
+ }
769
+ args.push("--", query, projectRoot);
770
+ const output = execFileSync("rg", args, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
771
+ const results = [];
772
+ const lines = output.split("\n").filter(Boolean);
773
+ for (const line of lines) {
774
+ const match = line.match(/^(.+?):(\d+):(.*)/);
775
+ if (match) {
776
+ const filePath = relative(projectRoot, match[1] || "").replace(/\\/g, "/");
777
+ const lineNumber = Number.parseInt(match[2] || "0", 10);
778
+ const content = (match[3] || "").trim();
779
+ results.push({
780
+ filePath,
781
+ lineNumber,
782
+ content,
783
+ score: 0.8,
784
+ // Exact match gets high base score
785
+ context: [],
786
+ engram_score: 0
787
+ });
788
+ }
789
+ }
790
+ return results.slice(0, maxResults);
791
+ } catch {
792
+ return [];
793
+ }
794
+ }
795
+ function collectIndexableFiles(dir, projectRoot, ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"], exts = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".yaml", ".yml"]) {
796
+ const files = [];
797
+ try {
798
+ const entries = readdirSync(dir, { withFileTypes: true });
799
+ for (const entry of entries) {
800
+ const fullPath = join(dir, entry.name);
801
+ if (entry.isDirectory()) {
802
+ if (!ignoreDirs.includes(entry.name)) {
803
+ files.push(...collectIndexableFiles(fullPath, projectRoot, ignoreDirs, exts));
804
+ }
805
+ } else if (entry.isFile() && exts.includes(extname(entry.name))) {
806
+ try {
807
+ const stat = statSync(fullPath);
808
+ if (stat.size < 100 * 1024) {
809
+ files.push(relative(projectRoot, fullPath).replace(/\\/g, "/"));
810
+ }
811
+ } catch {
812
+ }
813
+ }
814
+ }
815
+ } catch {
816
+ }
817
+ return files;
818
+ }
819
+ function createSearchEngine(dbPath) {
820
+ let db = null;
821
+ let projectRoot = "";
822
+ const rgAvailable = hasRipgrep();
823
+ async function init(root) {
824
+ if (db) {
825
+ try {
826
+ db.close();
827
+ } catch {
828
+ }
829
+ }
830
+ projectRoot = root;
831
+ db = new Database(dbPath);
832
+ db.pragma("journal_mode = WAL");
833
+ db.exec(`
834
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
835
+ filepath, line_number, content, tokenize='porter'
836
+ );
837
+ `);
838
+ db.exec(`
839
+ CREATE TABLE IF NOT EXISTS search_meta (
840
+ filepath TEXT PRIMARY KEY,
841
+ mtime INTEGER NOT NULL,
842
+ indexed_at INTEGER NOT NULL
843
+ );
844
+ `);
845
+ }
846
+ async function index(paths) {
847
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
848
+ const startTime = Date.now();
849
+ const filesToIndex = paths || collectIndexableFiles(projectRoot, projectRoot);
850
+ let filesIndexed = 0;
851
+ let totalLines = 0;
852
+ const insertStmt = db.prepare(
853
+ "INSERT INTO search_index(filepath, line_number, content) VALUES (?, ?, ?)"
854
+ );
855
+ const metaStmt = db.prepare(
856
+ "INSERT OR REPLACE INTO search_meta(filepath, mtime, indexed_at) VALUES (?, ?, ?)"
857
+ );
858
+ const checkMeta = db.prepare("SELECT mtime FROM search_meta WHERE filepath = ?");
859
+ const deleteFile = db.prepare("DELETE FROM search_index WHERE filepath = ?");
860
+ const transaction = db.transaction(() => {
861
+ for (const filePath of filesToIndex) {
862
+ const fullPath = join(projectRoot, filePath);
863
+ if (!existsSync(fullPath)) continue;
864
+ try {
865
+ const stat = statSync(fullPath);
866
+ const existing = checkMeta.get(filePath);
867
+ if (existing && existing.mtime >= stat.mtimeMs) {
868
+ continue;
869
+ }
870
+ deleteFile.run(filePath);
871
+ const content = readFileSync(fullPath, "utf-8");
872
+ const lines = content.split("\n");
873
+ for (let i = 0; i < lines.length; i++) {
874
+ const line = lines[i];
875
+ if (line && line.trim().length > 0) {
876
+ insertStmt.run(filePath, i + 1, line);
877
+ totalLines++;
878
+ }
879
+ }
880
+ metaStmt.run(filePath, stat.mtimeMs, Date.now());
881
+ filesIndexed++;
882
+ } catch {
883
+ }
884
+ }
885
+ });
886
+ transaction();
887
+ return {
888
+ filesIndexed,
889
+ totalLines,
890
+ duration: Date.now() - startTime
891
+ };
892
+ }
893
+ async function search(query, opts = {}) {
894
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
895
+ const maxResults = opts.maxResults || 10;
896
+ const results = [];
897
+ const seen = /* @__PURE__ */ new Set();
898
+ try {
899
+ const ftsQuery = query.replace(/['"*(){}[\]^~\\:]/g, " ").trim().split(/\s+/).filter((w) => w.length > 0).map((w) => `content:${w}`).join(" ");
900
+ if (ftsQuery.length > 0) {
901
+ let sql = `
902
+ SELECT filepath, line_number, content, rank
903
+ FROM search_index
904
+ WHERE search_index MATCH ?
905
+ `;
906
+ const params = [ftsQuery];
907
+ if (opts.fileGlob) {
908
+ const likePattern = opts.fileGlob.replace(/\*/g, "%").replace(/\?/g, "_");
909
+ sql += " AND filepath LIKE ?";
910
+ params.push(likePattern);
911
+ }
912
+ sql += " ORDER BY rank LIMIT ?";
913
+ params.push(maxResults * 2);
914
+ const rows = db.prepare(sql).all(...params);
915
+ for (const row of rows) {
916
+ const key = `${row.filepath}:${row.line_number}`;
917
+ if (!seen.has(key)) {
918
+ seen.add(key);
919
+ const score = Math.min(1, Math.max(0, 1 + row.rank / 10));
920
+ const context = [];
921
+ if (opts.includeContext) {
922
+ const contextRows = db.prepare(
923
+ `SELECT content FROM search_index WHERE filepath = ? AND CAST(line_number AS INTEGER) BETWEEN ? AND ? ORDER BY CAST(line_number AS INTEGER)`
924
+ ).all(row.filepath, row.line_number - 2, row.line_number + 2);
925
+ context.push(...contextRows.map((r) => r.content));
926
+ }
927
+ results.push({
928
+ filePath: row.filepath,
929
+ lineNumber: row.line_number,
930
+ content: row.content,
931
+ score,
932
+ context,
933
+ engram_score: 0
934
+ });
935
+ }
936
+ }
937
+ }
938
+ } catch {
939
+ }
940
+ if (rgAvailable && opts.useRipgrep !== false) {
941
+ const rgResults = ripgrepSearch(query, projectRoot, opts);
942
+ for (const result of rgResults) {
943
+ const key = `${result.filePath}:${result.lineNumber}`;
944
+ if (!seen.has(key)) {
945
+ seen.add(key);
946
+ results.push(result);
947
+ }
948
+ }
949
+ }
950
+ results.sort((a, b) => b.score - a.score);
951
+ return results.slice(0, maxResults);
952
+ }
953
+ async function refresh() {
954
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
955
+ db.exec("DELETE FROM search_index");
956
+ db.exec("DELETE FROM search_meta");
957
+ return index();
958
+ }
959
+ async function close() {
960
+ if (db) {
961
+ db.close();
962
+ db = null;
963
+ }
964
+ }
965
+ return {
966
+ init,
967
+ index,
968
+ search,
969
+ refresh,
970
+ close
971
+ };
972
+ }
973
+ var init_search_engine = __esm({
974
+ "src/core/search-engine.ts"() {
975
+ "use strict";
976
+ init_esm_shims();
977
+ }
978
+ });
979
+
980
+ // src/cli/commands/search.ts
981
+ var search_exports = {};
982
+ __export(search_exports, {
983
+ searchCommand: () => searchCommand
984
+ });
985
+ import { resolve } from "path";
986
+ async function searchCommand(options) {
987
+ const projectRoot = resolve(process.cwd());
988
+ const dbPath = resolve(projectRoot, ".sparn", "search.db");
989
+ const engine = createSearchEngine(dbPath);
990
+ try {
991
+ await engine.init(projectRoot);
992
+ if (options.subcommand === "init" || options.subcommand === "refresh") {
993
+ const stats = options.subcommand === "refresh" ? await engine.refresh() : await engine.index();
994
+ const result = {
995
+ indexStats: stats,
996
+ message: `Indexed ${stats.filesIndexed} files (${stats.totalLines} lines) in ${stats.duration}ms`
997
+ };
998
+ if (options.json) {
999
+ result.json = JSON.stringify(stats, null, 2);
1000
+ }
1001
+ return result;
1002
+ }
1003
+ if (!options.query) {
1004
+ return { message: 'No search query provided. Usage: sparn search "query"' };
1005
+ }
1006
+ const results = await engine.search(options.query, {
1007
+ maxResults: options.maxResults || 10,
1008
+ fileGlob: options.glob,
1009
+ includeContext: true
1010
+ });
1011
+ const cmdResult = { results };
1012
+ if (options.json) {
1013
+ cmdResult.json = JSON.stringify(results, null, 2);
1014
+ }
1015
+ return cmdResult;
1016
+ } finally {
1017
+ await engine.close();
1018
+ }
1019
+ }
1020
+ var init_search = __esm({
1021
+ "src/cli/commands/search.ts"() {
1022
+ "use strict";
1023
+ init_esm_shims();
1024
+ init_search_engine();
1025
+ }
1026
+ });
1027
+
1028
+ // src/core/dependency-graph.ts
1029
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
1030
+ import { extname as extname2, join as join2, relative as relative2, resolve as resolve2 } from "path";
1031
+ function parseImports(content, filePath) {
1032
+ const edges = [];
1033
+ const seen = /* @__PURE__ */ new Set();
1034
+ for (const pattern of IMPORT_PATTERNS) {
1035
+ const regex = new RegExp(pattern.source, pattern.flags);
1036
+ const matches = [...content.matchAll(regex)];
1037
+ for (const match of matches) {
1038
+ if (pattern.source.includes("from")) {
1039
+ const symbolsRaw = match[1] || "";
1040
+ const target = match[2] || "";
1041
+ if (!target || target.startsWith(".") === false && !target.startsWith("/")) {
1042
+ continue;
1043
+ }
1044
+ const symbols = symbolsRaw.split(",").map(
1045
+ (s) => s.trim().split(/\s+as\s+/)[0]?.trim() || ""
1046
+ ).filter(Boolean);
1047
+ const key = `${filePath}->${target}`;
1048
+ if (!seen.has(key)) {
1049
+ seen.add(key);
1050
+ edges.push({ source: filePath, target, symbols });
1051
+ }
1052
+ } else if (pattern.source.includes("require")) {
1053
+ const target = match[1] || "";
1054
+ if (!target || !target.startsWith(".") && !target.startsWith("/")) {
1055
+ continue;
1056
+ }
1057
+ const key = `${filePath}->${target}`;
1058
+ if (!seen.has(key)) {
1059
+ seen.add(key);
1060
+ edges.push({ source: filePath, target, symbols: [] });
1061
+ }
1062
+ } else {
1063
+ const target = match[1] || "";
1064
+ if (!target || !target.startsWith(".") && !target.startsWith("/")) {
1065
+ continue;
1066
+ }
1067
+ const key = `${filePath}->${target}`;
1068
+ if (!seen.has(key)) {
1069
+ seen.add(key);
1070
+ edges.push({ source: filePath, target, symbols: [] });
1071
+ }
1072
+ }
1073
+ }
1074
+ }
1075
+ return edges;
1076
+ }
1077
+ function parseExports(content) {
1078
+ const exportsList = [];
1079
+ for (const pattern of EXPORT_PATTERNS) {
1080
+ const regex = new RegExp(pattern.source, pattern.flags);
1081
+ const matches = [...content.matchAll(regex)];
1082
+ for (const match of matches) {
1083
+ if (match[1]) {
1084
+ const symbols = match[1].split(",").map(
1085
+ (s) => s.trim().split(/\s+as\s+/)[0]?.trim() || ""
1086
+ ).filter(Boolean);
1087
+ exportsList.push(...symbols);
1088
+ } else {
1089
+ exportsList.push("default");
1090
+ }
1091
+ }
1092
+ }
1093
+ return [...new Set(exportsList)];
1094
+ }
1095
+ function resolveImportPath(importPath, fromFile, projectRoot, extensions) {
1096
+ const cleanImport = importPath.replace(/\.(js|ts|tsx|jsx)$/, "");
1097
+ const baseDir = join2(projectRoot, fromFile, "..");
1098
+ const candidates = [
1099
+ ...extensions.map((ext) => resolve2(baseDir, `${cleanImport}${ext}`)),
1100
+ ...extensions.map((ext) => resolve2(baseDir, cleanImport, `index${ext}`))
1101
+ ];
1102
+ for (const candidate of candidates) {
1103
+ if (existsSync2(candidate)) {
1104
+ return relative2(projectRoot, candidate).replace(/\\/g, "/");
1105
+ }
1106
+ }
1107
+ return null;
1108
+ }
1109
+ function collectFiles(dir, projectRoot, extensions, ignoreDirs) {
1110
+ const files = [];
1111
+ try {
1112
+ const entries = readdirSync2(dir, { withFileTypes: true });
1113
+ for (const entry of entries) {
1114
+ const fullPath = join2(dir, entry.name);
1115
+ if (entry.isDirectory()) {
1116
+ if (!ignoreDirs.includes(entry.name)) {
1117
+ files.push(...collectFiles(fullPath, projectRoot, extensions, ignoreDirs));
1118
+ }
1119
+ } else if (entry.isFile() && extensions.includes(extname2(entry.name))) {
1120
+ files.push(relative2(projectRoot, fullPath).replace(/\\/g, "/"));
1121
+ }
1122
+ }
1123
+ } catch {
1124
+ }
1125
+ return files;
1126
+ }
1127
+ function createDependencyGraph(config) {
1128
+ const {
1129
+ projectRoot,
1130
+ maxDepth = 50,
1131
+ extensions = [".ts", ".tsx", ".js", ".jsx"],
1132
+ ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"]
1133
+ } = config;
1134
+ const nodes = /* @__PURE__ */ new Map();
1135
+ let built = false;
1136
+ async function build() {
1137
+ nodes.clear();
1138
+ const files = collectFiles(projectRoot, projectRoot, extensions, ignoreDirs);
1139
+ for (const filePath of files) {
1140
+ const fullPath = join2(projectRoot, filePath);
1141
+ try {
1142
+ const content = readFileSync2(fullPath, "utf-8");
1143
+ const stat = statSync2(fullPath);
1144
+ const exports = parseExports(content);
1145
+ const imports = parseImports(content, filePath);
1146
+ const resolvedImports = [];
1147
+ for (const imp of imports) {
1148
+ const resolved = resolveImportPath(imp.target, filePath, projectRoot, extensions);
1149
+ if (resolved) {
1150
+ resolvedImports.push({ ...imp, target: resolved });
1151
+ }
1152
+ }
1153
+ nodes.set(filePath, {
1154
+ filePath,
1155
+ exports,
1156
+ imports: resolvedImports,
1157
+ callers: [],
1158
+ // Populated in second pass
1159
+ engram_score: 0,
1160
+ lastModified: stat.mtimeMs,
1161
+ tokenEstimate: estimateTokens(content)
1162
+ });
1163
+ } catch {
1164
+ }
1165
+ }
1166
+ for (const [filePath, node] of nodes) {
1167
+ for (const imp of node.imports) {
1168
+ const targetNode = nodes.get(imp.target);
1169
+ if (targetNode && !targetNode.callers.includes(filePath)) {
1170
+ targetNode.callers.push(filePath);
1171
+ }
1172
+ }
1173
+ }
1174
+ built = true;
1175
+ return nodes;
1176
+ }
1177
+ async function analyze() {
1178
+ if (!built) await build();
1179
+ const entryPoints = [];
1180
+ const orphans = [];
1181
+ const callerCounts = /* @__PURE__ */ new Map();
1182
+ for (const [filePath, node] of nodes) {
1183
+ if (node.callers.length === 0 && node.imports.length > 0) {
1184
+ entryPoints.push(filePath);
1185
+ }
1186
+ if (node.callers.length === 0 && node.imports.length === 0) {
1187
+ orphans.push(filePath);
1188
+ }
1189
+ callerCounts.set(filePath, node.callers.length);
1190
+ }
1191
+ const sortedByCallers = [...callerCounts.entries()].sort((a, b) => b[1] - a[1]).filter(([, count]) => count > 0);
1192
+ const hotPaths = sortedByCallers.slice(0, 10).map(([path2]) => path2);
1193
+ const totalTokens = [...nodes.values()].reduce((sum, n) => sum + n.tokenEstimate, 0);
1194
+ const hotPathTokens = hotPaths.reduce(
1195
+ (sum, path2) => sum + (nodes.get(path2)?.tokenEstimate || 0),
1196
+ 0
1197
+ );
1198
+ return {
1199
+ entryPoints,
1200
+ hotPaths,
1201
+ orphans,
1202
+ totalTokens,
1203
+ optimizedTokens: hotPathTokens
1204
+ };
1205
+ }
1206
+ async function focus(pattern) {
1207
+ if (!built) await build();
1208
+ const matching = /* @__PURE__ */ new Map();
1209
+ const lowerPattern = pattern.toLowerCase();
1210
+ for (const [filePath, node] of nodes) {
1211
+ if (filePath.toLowerCase().includes(lowerPattern)) {
1212
+ matching.set(filePath, node);
1213
+ for (const imp of node.imports) {
1214
+ const depNode = nodes.get(imp.target);
1215
+ if (depNode) {
1216
+ matching.set(imp.target, depNode);
1217
+ }
1218
+ }
1219
+ for (const caller of node.callers) {
1220
+ const callerNode = nodes.get(caller);
1221
+ if (callerNode) {
1222
+ matching.set(caller, callerNode);
1223
+ }
1224
+ }
1225
+ }
1226
+ }
1227
+ return matching;
1228
+ }
1229
+ async function getFilesFromEntry(entryPoint, depth = maxDepth) {
1230
+ if (!built) await build();
1231
+ const visited = /* @__PURE__ */ new Set();
1232
+ const queue = [{ path: entryPoint, depth: 0 }];
1233
+ while (queue.length > 0) {
1234
+ const item = queue.shift();
1235
+ if (!item) break;
1236
+ if (visited.has(item.path) || item.depth > depth) continue;
1237
+ visited.add(item.path);
1238
+ const node = nodes.get(item.path);
1239
+ if (node) {
1240
+ for (const imp of node.imports) {
1241
+ if (!visited.has(imp.target)) {
1242
+ queue.push({ path: imp.target, depth: item.depth + 1 });
1243
+ }
1244
+ }
1245
+ }
1246
+ }
1247
+ return [...visited];
1248
+ }
1249
+ function getNodes() {
1250
+ return nodes;
1251
+ }
1252
+ return {
1253
+ build,
1254
+ analyze,
1255
+ focus,
1256
+ getFilesFromEntry,
1257
+ getNodes
1258
+ };
1259
+ }
1260
+ var IMPORT_PATTERNS, EXPORT_PATTERNS;
1261
+ var init_dependency_graph = __esm({
1262
+ "src/core/dependency-graph.ts"() {
1263
+ "use strict";
1264
+ init_esm_shims();
1265
+ init_tokenizer();
1266
+ IMPORT_PATTERNS = [
1267
+ // import { Foo, Bar } from './module'
1268
+ /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g,
1269
+ // import Foo from './module'
1270
+ /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
1271
+ // import * as Foo from './module'
1272
+ /import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
1273
+ // import './module' (side-effect)
1274
+ /import\s+['"]([^'"]+)['"]/g,
1275
+ // require('./module')
1276
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
1277
+ ];
1278
+ EXPORT_PATTERNS = [
1279
+ // export { Foo, Bar }
1280
+ /export\s+\{([^}]+)\}/g,
1281
+ // export function/class/const/let/var/type/interface
1282
+ /export\s+(?:default\s+)?(?:function|class|const|let|var|type|interface|enum)\s+(\w+)/g,
1283
+ // export default
1284
+ /export\s+default\s+/g
1285
+ ];
1286
+ }
1287
+ });
1288
+
1289
+ // src/cli/commands/graph.ts
1290
+ var graph_exports = {};
1291
+ __export(graph_exports, {
1292
+ graphCommand: () => graphCommand
1293
+ });
1294
+ import { resolve as resolve3 } from "path";
1295
+ async function graphCommand(options) {
1296
+ const projectRoot = resolve3(process.cwd());
1297
+ const graph = createDependencyGraph({
1298
+ projectRoot,
1299
+ maxDepth: options.depth
1300
+ });
1301
+ await graph.build();
1302
+ let nodes = graph.getNodes();
1303
+ if (options.focus) {
1304
+ nodes = await graph.focus(options.focus);
1305
+ }
1306
+ if (options.entry) {
1307
+ const allNodes = graph.getNodes();
1308
+ if (!allNodes.has(options.entry)) {
1309
+ throw new Error(
1310
+ `Entry point not found in graph: ${options.entry}. Available: ${[...allNodes.keys()].slice(0, 5).join(", ")}${allNodes.size > 5 ? "..." : ""}`
1311
+ );
1312
+ }
1313
+ const files = await graph.getFilesFromEntry(options.entry, options.depth);
1314
+ const entryNodes = /* @__PURE__ */ new Map();
1315
+ for (const f of files) {
1316
+ const node = nodes.get(f);
1317
+ if (node) entryNodes.set(f, node);
1318
+ }
1319
+ nodes = entryNodes;
1320
+ }
1321
+ const analysis = await graph.analyze();
1322
+ const result = {
1323
+ analysis,
1324
+ nodeCount: nodes.size
1325
+ };
1326
+ if (options.json) {
1327
+ result.json = JSON.stringify(
1328
+ {
1329
+ analysis,
1330
+ nodeCount: nodes.size,
1331
+ nodes: Object.fromEntries(
1332
+ [...nodes.entries()].map(([k, v]) => [
1333
+ k,
1334
+ {
1335
+ exports: v.exports,
1336
+ imports: v.imports.map((i) => i.target),
1337
+ callers: v.callers,
1338
+ tokens: v.tokenEstimate
1339
+ }
1340
+ ])
1341
+ )
1342
+ },
1343
+ null,
1344
+ 2
1345
+ );
1346
+ }
1347
+ return result;
1348
+ }
1349
+ var init_graph = __esm({
1350
+ "src/cli/commands/graph.ts"() {
1351
+ "use strict";
1352
+ init_esm_shims();
1353
+ init_dependency_graph();
1354
+ }
1355
+ });
1356
+
1357
+ // src/core/workflow-planner.ts
1358
+ import { randomUUID as randomUUID3 } from "crypto";
1359
+ import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
1360
+ import { join as join3 } from "path";
1361
+ function createWorkflowPlanner(projectRoot) {
1362
+ const plansDir = join3(projectRoot, ".sparn", "plans");
1363
+ if (!existsSync3(plansDir)) {
1364
+ mkdirSync(plansDir, { recursive: true });
1365
+ }
1366
+ function sanitizeId(id) {
1367
+ return id.replace(/[/\\:.]/g, "").replace(/\.\./g, "");
1368
+ }
1369
+ function planPath(planId) {
1370
+ const safeId = sanitizeId(planId);
1371
+ if (!safeId) throw new Error("Invalid plan ID");
1372
+ return join3(plansDir, `plan-${safeId}.json`);
1373
+ }
1374
+ async function createPlan(taskDescription, filesNeeded, searchQueries, steps, tokenBudget = { planning: 0, estimated_execution: 0, max_file_reads: 5 }) {
1375
+ const id = randomUUID3().split("-")[0] || "plan";
1376
+ const plan = {
1377
+ id,
1378
+ created_at: Date.now(),
1379
+ task_description: taskDescription,
1380
+ steps: steps.map((s) => ({ ...s, status: "pending" })),
1381
+ token_budget: tokenBudget,
1382
+ files_needed: filesNeeded,
1383
+ search_queries: searchQueries,
1384
+ status: "draft"
1385
+ };
1386
+ writeFileSync(planPath(id), JSON.stringify(plan, null, 2), "utf-8");
1387
+ return plan;
1388
+ }
1389
+ async function loadPlan(planId) {
1390
+ const path2 = planPath(planId);
1391
+ if (!existsSync3(path2)) return null;
1392
+ try {
1393
+ return JSON.parse(readFileSync3(path2, "utf-8"));
1394
+ } catch {
1395
+ return null;
1396
+ }
1397
+ }
1398
+ async function listPlans() {
1399
+ if (!existsSync3(plansDir)) return [];
1400
+ const files = readdirSync3(plansDir).filter((f) => f.startsWith("plan-") && f.endsWith(".json"));
1401
+ const plans = [];
1402
+ for (const file of files) {
1403
+ try {
1404
+ const plan = JSON.parse(readFileSync3(join3(plansDir, file), "utf-8"));
1405
+ plans.push({
1406
+ id: plan.id,
1407
+ task: plan.task_description,
1408
+ status: plan.status,
1409
+ created: plan.created_at
1410
+ });
1411
+ } catch {
1412
+ }
1413
+ }
1414
+ return plans.sort((a, b) => b.created - a.created);
1415
+ }
1416
+ async function updateStep(planId, stepOrder, status, result) {
1417
+ const plan = await loadPlan(planId);
1418
+ if (!plan) throw new Error(`Plan ${planId} not found`);
1419
+ const step = plan.steps.find((s) => s.order === stepOrder);
1420
+ if (!step) throw new Error(`Step ${stepOrder} not found in plan ${planId}`);
1421
+ step.status = status;
1422
+ if (result !== void 0) {
1423
+ step.result = result;
1424
+ }
1425
+ writeFileSync(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
1426
+ }
1427
+ async function startExec(planId) {
1428
+ const plan = await loadPlan(planId);
1429
+ if (!plan) throw new Error(`Plan ${planId} not found`);
1430
+ plan.status = "executing";
1431
+ writeFileSync(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
1432
+ return {
1433
+ maxFileReads: plan.token_budget.max_file_reads,
1434
+ tokenBudget: plan.token_budget.estimated_execution,
1435
+ allowReplan: false
1436
+ };
1437
+ }
1438
+ async function verify(planId) {
1439
+ const plan = await loadPlan(planId);
1440
+ if (!plan) throw new Error(`Plan ${planId} not found`);
1441
+ let stepsCompleted = 0;
1442
+ let stepsFailed = 0;
1443
+ let stepsSkipped = 0;
1444
+ let tokensUsed = 0;
1445
+ const details = [];
1446
+ for (const step of plan.steps) {
1447
+ switch (step.status) {
1448
+ case "completed":
1449
+ stepsCompleted++;
1450
+ tokensUsed += step.estimated_tokens;
1451
+ break;
1452
+ case "failed":
1453
+ stepsFailed++;
1454
+ break;
1455
+ case "skipped":
1456
+ stepsSkipped++;
1457
+ break;
1458
+ default:
1459
+ break;
1460
+ }
1461
+ details.push({
1462
+ step: step.order,
1463
+ action: step.action,
1464
+ target: step.target,
1465
+ status: step.status
1466
+ });
1467
+ }
1468
+ const totalSteps = plan.steps.length;
1469
+ const success = stepsFailed === 0 && stepsCompleted === totalSteps;
1470
+ const hasInProgress = plan.steps.some(
1471
+ (s) => s.status === "pending" || s.status === "in_progress"
1472
+ );
1473
+ if (!hasInProgress) {
1474
+ plan.status = success ? "completed" : "failed";
1475
+ writeFileSync(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
1476
+ }
1477
+ return {
1478
+ planId,
1479
+ stepsCompleted,
1480
+ stepsFailed,
1481
+ stepsSkipped,
1482
+ totalSteps,
1483
+ tokensBudgeted: plan.token_budget.estimated_execution,
1484
+ tokensUsed,
1485
+ success,
1486
+ details
1487
+ };
1488
+ }
1489
+ function getPlansDir() {
1490
+ return plansDir;
1491
+ }
1492
+ return {
1493
+ createPlan,
1494
+ loadPlan,
1495
+ listPlans,
1496
+ updateStep,
1497
+ startExec,
1498
+ verify,
1499
+ getPlansDir
1500
+ };
1501
+ }
1502
+ var init_workflow_planner = __esm({
1503
+ "src/core/workflow-planner.ts"() {
1504
+ "use strict";
1505
+ init_esm_shims();
1506
+ }
1507
+ });
1508
+
1509
+ // src/cli/commands/plan.ts
1510
+ var plan_exports = {};
1511
+ __export(plan_exports, {
1512
+ planCommand: () => planCommand,
1513
+ planListCommand: () => planListCommand
1514
+ });
1515
+ import { resolve as resolve4 } from "path";
1516
+ async function planCommand(options) {
1517
+ const projectRoot = resolve4(process.cwd());
1518
+ const planner = createWorkflowPlanner(projectRoot);
1519
+ const steps = [];
1520
+ let order = 1;
1521
+ if (options.searches) {
1522
+ for (const query of options.searches) {
1523
+ steps.push({
1524
+ order: order++,
1525
+ action: "search",
1526
+ target: query,
1527
+ description: `Search for: ${query}`,
1528
+ dependencies: [],
1529
+ estimated_tokens: 500
1530
+ });
1531
+ }
1532
+ }
1533
+ if (options.files) {
1534
+ for (const file of options.files) {
1535
+ steps.push({
1536
+ order: order++,
1537
+ action: "read",
1538
+ target: file,
1539
+ description: `Read file: ${file}`,
1540
+ dependencies: [],
1541
+ estimated_tokens: 1e3
1542
+ });
1543
+ }
1544
+ }
1545
+ steps.push({
1546
+ order: order++,
1547
+ action: "verify",
1548
+ target: "tests",
1549
+ description: "Run tests to verify changes",
1550
+ dependencies: steps.map((s) => s.order),
1551
+ estimated_tokens: 200
1552
+ });
1553
+ const plan = await planner.createPlan(
1554
+ options.task,
1555
+ options.files || [],
1556
+ options.searches || [],
1557
+ steps,
1558
+ {
1559
+ planning: 0,
1560
+ estimated_execution: steps.reduce((sum, s) => sum + s.estimated_tokens, 0),
1561
+ max_file_reads: options.maxReads || 5
1562
+ }
1563
+ );
1564
+ const result = {
1565
+ plan,
1566
+ message: `Plan ${plan.id} created with ${plan.steps.length} steps`
1567
+ };
1568
+ if (options.json) {
1569
+ result.json = JSON.stringify(plan, null, 2);
1570
+ }
1571
+ return result;
1572
+ }
1573
+ async function planListCommand(options) {
1574
+ const projectRoot = resolve4(process.cwd());
1575
+ const planner = createWorkflowPlanner(projectRoot);
1576
+ const plans = await planner.listPlans();
1577
+ const result = { plans };
1578
+ if (options.json) {
1579
+ result.json = JSON.stringify(plans, null, 2);
1580
+ }
1581
+ return result;
1582
+ }
1583
+ var init_plan = __esm({
1584
+ "src/cli/commands/plan.ts"() {
1585
+ "use strict";
1586
+ init_esm_shims();
1587
+ init_workflow_planner();
1588
+ }
1589
+ });
1590
+
1591
+ // src/cli/commands/exec.ts
1592
+ var exec_exports = {};
1593
+ __export(exec_exports, {
1594
+ execCommand: () => execCommand
1595
+ });
1596
+ import { resolve as resolve5 } from "path";
1597
+ async function execCommand(options) {
1598
+ const projectRoot = resolve5(process.cwd());
1599
+ const planner = createWorkflowPlanner(projectRoot);
1600
+ const plan = await planner.loadPlan(options.planId);
1601
+ if (!plan) {
1602
+ throw new Error(`Plan ${options.planId} not found`);
1603
+ }
1604
+ const constraints = await planner.startExec(options.planId);
1605
+ const updatedPlan = await planner.loadPlan(options.planId) || plan;
1606
+ const result = {
1607
+ plan: updatedPlan,
1608
+ constraints,
1609
+ message: `Executing plan ${options.planId}: max ${constraints.maxFileReads} reads, ${constraints.tokenBudget} token budget`
1610
+ };
1611
+ if (options.json) {
1612
+ result.json = JSON.stringify({ plan: updatedPlan, constraints }, null, 2);
1613
+ }
1614
+ return result;
1615
+ }
1616
+ var init_exec = __esm({
1617
+ "src/cli/commands/exec.ts"() {
1618
+ "use strict";
1619
+ init_esm_shims();
1620
+ init_workflow_planner();
1621
+ }
1622
+ });
1623
+
1624
+ // src/cli/commands/verify.ts
1625
+ var verify_exports = {};
1626
+ __export(verify_exports, {
1627
+ verifyCommand: () => verifyCommand
1628
+ });
1629
+ import { resolve as resolve6 } from "path";
1630
+ async function verifyCommand(options) {
1631
+ const projectRoot = resolve6(process.cwd());
1632
+ const planner = createWorkflowPlanner(projectRoot);
1633
+ const plan = await planner.loadPlan(options.planId);
1634
+ if (!plan) {
1635
+ throw new Error(`Plan ${options.planId} not found`);
1636
+ }
1637
+ const verification = await planner.verify(options.planId);
1638
+ const status = verification.success ? "PASSED" : "FAILED";
1639
+ const message = `Plan ${options.planId} verification: ${status} (${verification.stepsCompleted}/${verification.totalSteps} steps completed)`;
1640
+ const result = {
1641
+ verification,
1642
+ message
1643
+ };
1644
+ if (options.json) {
1645
+ result.json = JSON.stringify(verification, null, 2);
1646
+ }
1647
+ return result;
1648
+ }
1649
+ var init_verify = __esm({
1650
+ "src/cli/commands/verify.ts"() {
1651
+ "use strict";
1652
+ init_esm_shims();
1653
+ init_workflow_planner();
1654
+ }
1655
+ });
1656
+
1657
+ // src/core/docs-generator.ts
1658
+ import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync4 } from "fs";
1659
+ import { extname as extname3, join as join4, relative as relative3 } from "path";
1660
+ function detectEntryPoints(projectRoot) {
1661
+ const entries = [];
1662
+ const candidates = [
1663
+ { path: "src/index.ts", desc: "Library API" },
1664
+ { path: "src/cli/index.ts", desc: "CLI entry point" },
1665
+ { path: "src/daemon/index.ts", desc: "Daemon process" },
1666
+ { path: "src/mcp/index.ts", desc: "MCP server" },
1667
+ { path: "src/main.ts", desc: "Main entry" },
1668
+ { path: "src/app.ts", desc: "App entry" },
1669
+ { path: "src/server.ts", desc: "Server entry" },
1670
+ { path: "index.ts", desc: "Root entry" },
1671
+ { path: "index.js", desc: "Root entry" }
1672
+ ];
1673
+ for (const c of candidates) {
1674
+ if (existsSync4(join4(projectRoot, c.path))) {
1675
+ entries.push({ path: c.path, description: c.desc });
1676
+ }
1677
+ }
1678
+ return entries;
1679
+ }
1680
+ function scanModules(dir, projectRoot, ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"]) {
1681
+ const modules = [];
1682
+ try {
1683
+ const entries = readdirSync4(dir, { withFileTypes: true });
1684
+ for (const entry of entries) {
1685
+ const fullPath = join4(dir, entry.name);
1686
+ if (entry.isDirectory() && !ignoreDirs.includes(entry.name)) {
1687
+ modules.push(...scanModules(fullPath, projectRoot, ignoreDirs));
1688
+ } else if (entry.isFile() && [".ts", ".tsx", ".js", ".jsx"].includes(extname3(entry.name))) {
1689
+ try {
1690
+ const content = readFileSync4(fullPath, "utf-8");
1691
+ modules.push({
1692
+ path: relative3(projectRoot, fullPath).replace(/\\/g, "/"),
1693
+ lines: content.split("\n").length
1694
+ });
1695
+ } catch {
1696
+ }
1697
+ }
1698
+ }
1699
+ } catch {
1700
+ }
1701
+ return modules;
1702
+ }
1703
+ function readPackageJson(projectRoot) {
1704
+ const pkgPath = join4(projectRoot, "package.json");
1705
+ if (!existsSync4(pkgPath)) return null;
1706
+ try {
1707
+ return JSON.parse(readFileSync4(pkgPath, "utf-8"));
1708
+ } catch {
1709
+ return null;
1710
+ }
1711
+ }
1712
+ function detectStack(projectRoot) {
1713
+ const stack = [];
1714
+ const pkg = readPackageJson(projectRoot);
1715
+ if (!pkg) return stack;
1716
+ const allDeps = {
1717
+ ...pkg.dependencies,
1718
+ ...pkg.devDependencies
1719
+ };
1720
+ if (allDeps["typescript"]) stack.push("TypeScript");
1721
+ if (allDeps["vitest"]) stack.push("Vitest");
1722
+ if (allDeps["@biomejs/biome"]) stack.push("Biome");
1723
+ if (allDeps["eslint"]) stack.push("ESLint");
1724
+ if (allDeps["prettier"]) stack.push("Prettier");
1725
+ if (allDeps["react"]) stack.push("React");
1726
+ if (allDeps["next"]) stack.push("Next.js");
1727
+ if (allDeps["express"]) stack.push("Express");
1728
+ if (allDeps["commander"]) stack.push("Commander.js CLI");
1729
+ if (allDeps["better-sqlite3"]) stack.push("SQLite (better-sqlite3)");
1730
+ if (allDeps["zod"]) stack.push("Zod validation");
1731
+ return stack;
1732
+ }
1733
+ function createDocsGenerator(config) {
1734
+ const { projectRoot, includeGraph = true } = config;
1735
+ async function generate(graph) {
1736
+ const lines = [];
1737
+ const pkg = readPackageJson(projectRoot);
1738
+ const projectName = pkg?.name || "Project";
1739
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1740
+ lines.push(`# ${projectName} \u2014 Developer Guide`);
1741
+ lines.push(`<!-- Auto-generated by Sparn v1.3.0 \u2014 ${now} -->`);
1742
+ lines.push("");
1743
+ const stack = detectStack(projectRoot);
1744
+ if (stack.length > 0) {
1745
+ lines.push(`**Stack**: ${stack.join(", ")}`);
1746
+ lines.push("");
1747
+ }
1748
+ if (pkg?.scripts) {
1749
+ lines.push("## Commands");
1750
+ lines.push("");
1751
+ const important = ["build", "dev", "test", "lint", "typecheck", "start"];
1752
+ for (const cmd of important) {
1753
+ if (pkg.scripts[cmd]) {
1754
+ lines.push(`- \`npm run ${cmd}\` \u2014 \`${pkg.scripts[cmd]}\``);
1755
+ }
1756
+ }
1757
+ lines.push("");
1758
+ }
1759
+ const entryPoints = detectEntryPoints(projectRoot);
1760
+ if (entryPoints.length > 0) {
1761
+ lines.push("## Entry Points");
1762
+ lines.push("");
1763
+ for (const ep of entryPoints) {
1764
+ lines.push(`- \`${ep.path}\` \u2014 ${ep.description}`);
1765
+ }
1766
+ lines.push("");
1767
+ }
1768
+ const srcDir = join4(projectRoot, "src");
1769
+ if (existsSync4(srcDir)) {
1770
+ const modules = scanModules(srcDir, projectRoot);
1771
+ const dirGroups = /* @__PURE__ */ new Map();
1772
+ for (const mod of modules) {
1773
+ const parts = mod.path.split("/");
1774
+ const dir = parts.length > 2 ? parts.slice(0, 2).join("/") : parts[0] || "";
1775
+ if (!dirGroups.has(dir)) {
1776
+ dirGroups.set(dir, []);
1777
+ }
1778
+ dirGroups.get(dir)?.push({
1779
+ file: parts[parts.length - 1] || mod.path,
1780
+ lines: mod.lines
1781
+ });
1782
+ }
1783
+ lines.push("## Structure");
1784
+ lines.push("");
1785
+ for (const [dir, files] of dirGroups) {
1786
+ lines.push(`### ${dir}/ (${files.length} files)`);
1787
+ const shown = files.slice(0, 8);
1788
+ for (const f of shown) {
1789
+ lines.push(`- \`${f.file}\` (${f.lines}L)`);
1790
+ }
1791
+ if (files.length > 8) {
1792
+ lines.push(`- ... and ${files.length - 8} more`);
1793
+ }
1794
+ lines.push("");
1795
+ }
1796
+ }
1797
+ if (includeGraph && graph) {
1798
+ try {
1799
+ const analysis = await graph.analyze();
1800
+ lines.push("## Hot Dependencies (most imported)");
1801
+ lines.push("");
1802
+ for (const path2 of analysis.hotPaths.slice(0, 5)) {
1803
+ const node = graph.getNodes().get(path2);
1804
+ const callerCount = node?.callers.length || 0;
1805
+ lines.push(`- \`${path2}\` (imported by ${callerCount} modules)`);
1806
+ }
1807
+ lines.push("");
1808
+ if (analysis.orphans.length > 0) {
1809
+ lines.push(
1810
+ `**Orphaned files** (${analysis.orphans.length}): ${analysis.orphans.slice(0, 3).join(", ")}${analysis.orphans.length > 3 ? "..." : ""}`
1811
+ );
1812
+ lines.push("");
1813
+ }
1814
+ lines.push(
1815
+ `**Total tokens**: ${analysis.totalTokens.toLocaleString()} | **Hot path tokens**: ${analysis.optimizedTokens.toLocaleString()}`
1816
+ );
1817
+ lines.push("");
1818
+ } catch {
1819
+ }
1820
+ }
1821
+ const sparnConfigPath = join4(projectRoot, ".sparn", "config.yaml");
1822
+ if (existsSync4(sparnConfigPath)) {
1823
+ lines.push("## Sparn Optimization");
1824
+ lines.push("");
1825
+ lines.push("Sparn is active in this project. Key features:");
1826
+ lines.push("- Context optimization (60-70% token reduction)");
1827
+ lines.push("- Use `sparn search` before reading files");
1828
+ lines.push("- Use `sparn graph` to understand dependencies");
1829
+ lines.push("- Use `sparn plan` for planning, `sparn exec` for execution");
1830
+ lines.push("");
1831
+ }
1832
+ if (config.customSections) {
1833
+ for (const section of config.customSections) {
1834
+ lines.push(section);
1835
+ lines.push("");
1836
+ }
1837
+ }
1838
+ return lines.join("\n");
1839
+ }
1840
+ return { generate };
1841
+ }
1842
+ var init_docs_generator = __esm({
1843
+ "src/core/docs-generator.ts"() {
1844
+ "use strict";
1845
+ init_esm_shims();
1846
+ }
1847
+ });
1848
+
1849
+ // src/cli/commands/docs.ts
1850
+ var docs_exports = {};
1851
+ __export(docs_exports, {
1852
+ docsCommand: () => docsCommand
1853
+ });
1854
+ import { writeFileSync as writeFileSync2 } from "fs";
1855
+ import { resolve as resolve7 } from "path";
1856
+ async function docsCommand(options) {
1857
+ const projectRoot = resolve7(process.cwd());
1858
+ const generator = createDocsGenerator({
1859
+ projectRoot,
1860
+ includeGraph: options.includeGraph !== false
1861
+ });
1862
+ let graph;
1863
+ if (options.includeGraph !== false) {
1864
+ graph = createDependencyGraph({ projectRoot });
1865
+ await graph.build();
1866
+ }
1867
+ const content = await generator.generate(graph);
1868
+ if (options.json) {
1869
+ return {
1870
+ content,
1871
+ message: `CLAUDE.md generated (${content.split("\n").length} lines)`
1872
+ };
1873
+ }
1874
+ const outputPath = options.output || resolve7(projectRoot, "CLAUDE.md");
1875
+ writeFileSync2(outputPath, content, "utf-8");
1876
+ return {
1877
+ content,
1878
+ outputPath,
1879
+ message: `CLAUDE.md generated at ${outputPath} (${content.split("\n").length} lines)`
1880
+ };
1881
+ }
1882
+ var init_docs = __esm({
1883
+ "src/cli/commands/docs.ts"() {
1884
+ "use strict";
1885
+ init_esm_shims();
1886
+ init_dependency_graph();
1887
+ init_docs_generator();
1888
+ }
1889
+ });
1890
+
1891
+ // src/core/debt-tracker.ts
1892
+ import { randomUUID as randomUUID4 } from "crypto";
1893
+ import Database2 from "better-sqlite3";
1894
+ function createDebtTracker(dbPath) {
1895
+ const db = new Database2(dbPath);
1896
+ db.pragma("journal_mode = WAL");
1897
+ db.exec(`
1898
+ CREATE TABLE IF NOT EXISTS tech_debt (
1899
+ id TEXT PRIMARY KEY NOT NULL,
1900
+ description TEXT NOT NULL,
1901
+ created_at INTEGER NOT NULL,
1902
+ repayment_date INTEGER NOT NULL,
1903
+ severity TEXT NOT NULL CHECK(severity IN ('P0', 'P1', 'P2')),
1904
+ token_cost INTEGER NOT NULL DEFAULT 0,
1905
+ files_affected TEXT NOT NULL DEFAULT '[]',
1906
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'in_progress', 'resolved')),
1907
+ resolution_tokens INTEGER,
1908
+ resolved_at INTEGER
1909
+ );
1910
+ `);
1911
+ db.exec(`
1912
+ CREATE INDEX IF NOT EXISTS idx_debt_status ON tech_debt(status);
1913
+ CREATE INDEX IF NOT EXISTS idx_debt_severity ON tech_debt(severity);
1914
+ CREATE INDEX IF NOT EXISTS idx_debt_repayment ON tech_debt(repayment_date);
1915
+ `);
1916
+ const insertStmt = db.prepare(`
1917
+ INSERT INTO tech_debt (id, description, created_at, repayment_date, severity, token_cost, files_affected, status)
1918
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'open')
1919
+ `);
1920
+ const getStmt = db.prepare("SELECT * FROM tech_debt WHERE id = ?");
1921
+ function rowToDebt(row) {
1922
+ return {
1923
+ id: row["id"],
1924
+ description: row["description"],
1925
+ created_at: row["created_at"],
1926
+ repayment_date: row["repayment_date"],
1927
+ severity: row["severity"],
1928
+ token_cost: row["token_cost"],
1929
+ files_affected: JSON.parse(row["files_affected"] || "[]"),
1930
+ status: row["status"],
1931
+ resolution_tokens: row["resolution_tokens"],
1932
+ resolved_at: row["resolved_at"]
1933
+ };
1934
+ }
1935
+ async function add(debt) {
1936
+ const id = randomUUID4().split("-")[0] || "debt";
1937
+ const created_at = Date.now();
1938
+ insertStmt.run(
1939
+ id,
1940
+ debt.description,
1941
+ created_at,
1942
+ debt.repayment_date,
1943
+ debt.severity,
1944
+ debt.token_cost,
1945
+ JSON.stringify(debt.files_affected)
1946
+ );
1947
+ return {
1948
+ id,
1949
+ created_at,
1950
+ status: "open",
1951
+ description: debt.description,
1952
+ repayment_date: debt.repayment_date,
1953
+ severity: debt.severity,
1954
+ token_cost: debt.token_cost,
1955
+ files_affected: debt.files_affected
1956
+ };
1957
+ }
1958
+ async function list(filter = {}) {
1959
+ let sql = "SELECT * FROM tech_debt WHERE 1=1";
1960
+ const params = [];
1961
+ if (filter.status) {
1962
+ sql += " AND status = ?";
1963
+ params.push(filter.status);
1964
+ }
1965
+ if (filter.severity) {
1966
+ sql += " AND severity = ?";
1967
+ params.push(filter.severity);
1968
+ }
1969
+ if (filter.overdue) {
1970
+ sql += " AND repayment_date < ? AND status != ?";
1971
+ params.push(Date.now());
1972
+ params.push("resolved");
1973
+ }
1974
+ sql += " ORDER BY severity ASC, repayment_date ASC";
1975
+ const rows = db.prepare(sql).all(...params);
1976
+ return rows.map(rowToDebt);
1977
+ }
1978
+ async function get(id) {
1979
+ const row = getStmt.get(id);
1980
+ if (!row) return null;
1981
+ return rowToDebt(row);
1982
+ }
1983
+ async function resolve9(id, resolutionTokens) {
1984
+ const result = db.prepare(
1985
+ "UPDATE tech_debt SET status = ?, resolution_tokens = ?, resolved_at = ? WHERE id = ?"
1986
+ ).run("resolved", resolutionTokens ?? null, Date.now(), id);
1987
+ if (result.changes === 0) {
1988
+ throw new Error(`Debt not found: ${id}`);
1989
+ }
1990
+ }
1991
+ async function start(id) {
1992
+ const result = db.prepare("UPDATE tech_debt SET status = ? WHERE id = ?").run("in_progress", id);
1993
+ if (result.changes === 0) {
1994
+ throw new Error(`Debt not found: ${id}`);
1995
+ }
1996
+ }
1997
+ async function remove(id) {
1998
+ db.prepare("DELETE FROM tech_debt WHERE id = ?").run(id);
1999
+ }
2000
+ async function stats() {
2001
+ const all = db.prepare("SELECT * FROM tech_debt").all();
2002
+ const debts = all.map(rowToDebt);
2003
+ const now = Date.now();
2004
+ const open = debts.filter((d) => d.status === "open");
2005
+ const inProgress = debts.filter((d) => d.status === "in_progress");
2006
+ const resolved = debts.filter((d) => d.status === "resolved");
2007
+ const overdue = debts.filter((d) => d.status !== "resolved" && d.repayment_date < now);
2008
+ const totalTokenCost = debts.reduce((sum, d) => sum + d.token_cost, 0);
2009
+ const resolvedTokenCost = resolved.reduce(
2010
+ (sum, d) => sum + (d.resolution_tokens || d.token_cost),
2011
+ 0
2012
+ );
2013
+ const resolvedOnTime = resolved.filter(
2014
+ (d) => d.resolved_at && d.resolved_at <= d.repayment_date
2015
+ ).length;
2016
+ const repaymentRate = resolved.length > 0 ? resolvedOnTime / resolved.length : 0;
2017
+ return {
2018
+ total: debts.length,
2019
+ open: open.length,
2020
+ in_progress: inProgress.length,
2021
+ resolved: resolved.length,
2022
+ overdue: overdue.length,
2023
+ totalTokenCost,
2024
+ resolvedTokenCost,
2025
+ repaymentRate
2026
+ };
2027
+ }
2028
+ async function getCritical() {
2029
+ const rows = db.prepare(
2030
+ "SELECT * FROM tech_debt WHERE severity = 'P0' AND status != 'resolved' ORDER BY repayment_date ASC"
2031
+ ).all();
2032
+ return rows.map(rowToDebt);
2033
+ }
2034
+ async function close() {
2035
+ db.close();
2036
+ }
2037
+ return {
2038
+ add,
2039
+ list,
2040
+ get,
2041
+ resolve: resolve9,
2042
+ start,
2043
+ remove,
2044
+ stats,
2045
+ getCritical,
2046
+ close
2047
+ };
2048
+ }
2049
+ var init_debt_tracker = __esm({
2050
+ "src/core/debt-tracker.ts"() {
2051
+ "use strict";
2052
+ init_esm_shims();
2053
+ }
2054
+ });
2055
+
2056
+ // src/cli/commands/debt.ts
2057
+ var debt_exports = {};
2058
+ __export(debt_exports, {
2059
+ debtCommand: () => debtCommand
2060
+ });
2061
+ import { resolve as resolve8 } from "path";
2062
+ async function debtCommand(options) {
2063
+ const projectRoot = resolve8(process.cwd());
2064
+ const dbPath = resolve8(projectRoot, ".sparn", "memory.db");
2065
+ const tracker = createDebtTracker(dbPath);
2066
+ try {
2067
+ switch (options.subcommand) {
2068
+ case "add": {
2069
+ if (!options.description) {
2070
+ throw new Error("Description is required");
2071
+ }
2072
+ const severity = options.severity || "P1";
2073
+ if (!["P0", "P1", "P2"].includes(severity)) {
2074
+ throw new Error("Severity must be P0, P1, or P2");
2075
+ }
2076
+ let repaymentDate;
2077
+ if (options.due) {
2078
+ repaymentDate = new Date(options.due).getTime();
2079
+ if (Number.isNaN(repaymentDate)) {
2080
+ throw new Error(`Invalid date: ${options.due}`);
2081
+ }
2082
+ } else {
2083
+ repaymentDate = Date.now() + 14 * 24 * 60 * 60 * 1e3;
2084
+ }
2085
+ const debt = await tracker.add({
2086
+ description: options.description,
2087
+ repayment_date: repaymentDate,
2088
+ severity,
2089
+ token_cost: options.tokenCost || 0,
2090
+ files_affected: options.files || []
2091
+ });
2092
+ const result = {
2093
+ debt,
2094
+ message: `Debt ${debt.id} added (${severity}): ${options.description}`
2095
+ };
2096
+ if (options.json) {
2097
+ result.json = JSON.stringify(debt, null, 2);
2098
+ }
2099
+ return result;
2100
+ }
2101
+ case "list": {
2102
+ const debts = await tracker.list({
2103
+ overdue: options.overdue
2104
+ });
2105
+ const result = {
2106
+ debts,
2107
+ message: `${debts.length} debt item(s)`
2108
+ };
2109
+ if (options.json) {
2110
+ result.json = JSON.stringify(debts, null, 2);
2111
+ }
2112
+ return result;
2113
+ }
2114
+ case "resolve": {
2115
+ if (!options.id) {
2116
+ throw new Error("Debt ID is required");
2117
+ }
2118
+ await tracker.resolve(options.id, options.tokenCost);
2119
+ return {
2120
+ message: `Debt ${options.id} resolved`
2121
+ };
2122
+ }
2123
+ case "start": {
2124
+ if (!options.id) {
2125
+ throw new Error("Debt ID is required");
2126
+ }
2127
+ await tracker.start(options.id);
2128
+ return {
2129
+ message: `Debt ${options.id} started`
2130
+ };
2131
+ }
2132
+ case "stats": {
2133
+ const stats = await tracker.stats();
2134
+ const result = {
2135
+ stats,
2136
+ message: `${stats.total} total, ${stats.open} open, ${stats.overdue} overdue, ${(stats.repaymentRate * 100).toFixed(0)}% on-time rate`
2137
+ };
2138
+ if (options.json) {
2139
+ result.json = JSON.stringify(stats, null, 2);
2140
+ }
2141
+ return result;
2142
+ }
2143
+ default:
2144
+ throw new Error(`Unknown subcommand: ${options.subcommand}`);
2145
+ }
2146
+ } finally {
2147
+ await tracker.close();
2148
+ }
2149
+ }
2150
+ var init_debt = __esm({
2151
+ "src/cli/commands/debt.ts"() {
2152
+ "use strict";
2153
+ init_esm_shims();
2154
+ init_debt_tracker();
2155
+ }
2156
+ });
2157
+
2158
+ // src/daemon/daemon-process.ts
2159
+ var daemon_process_exports = {};
2160
+ __export(daemon_process_exports, {
2161
+ createDaemonCommand: () => createDaemonCommand
2162
+ });
2163
+ import { spawn } from "child_process";
2164
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
2165
+ import { dirname, join as join5 } from "path";
2166
+ import { fileURLToPath as fileURLToPath2 } from "url";
2167
+ function createDaemonCommand() {
2168
+ function isDaemonRunning(pidFile) {
2169
+ if (!existsSync6(pidFile)) {
2170
+ return { running: false };
2171
+ }
2172
+ try {
2173
+ const pidStr = readFileSync5(pidFile, "utf-8").trim();
2174
+ const pid = Number.parseInt(pidStr, 10);
2175
+ if (Number.isNaN(pid)) {
2176
+ return { running: false };
2177
+ }
2178
+ try {
2179
+ process.kill(pid, 0);
2180
+ return { running: true, pid };
2181
+ } catch {
2182
+ unlinkSync(pidFile);
2183
+ return { running: false };
2184
+ }
2185
+ } catch {
2186
+ return { running: false };
2187
+ }
2188
+ }
2189
+ function writePidFile(pidFile, pid) {
2190
+ const dir = dirname(pidFile);
2191
+ if (!existsSync6(dir)) {
2192
+ mkdirSync2(dir, { recursive: true });
2193
+ }
2194
+ writeFileSync3(pidFile, String(pid), "utf-8");
2195
+ }
2196
+ function removePidFile(pidFile) {
2197
+ if (existsSync6(pidFile)) {
2198
+ unlinkSync(pidFile);
2199
+ }
2200
+ }
2201
+ async function start(config) {
2202
+ const { pidFile, logFile } = config.realtime;
2203
+ const status2 = isDaemonRunning(pidFile);
2204
+ if (status2.running) {
2205
+ return {
2206
+ success: false,
2207
+ pid: status2.pid,
2208
+ message: `Daemon already running (PID ${status2.pid})`,
2209
+ error: "Already running"
2210
+ };
2211
+ }
2212
+ try {
2213
+ const __filename2 = fileURLToPath2(import.meta.url);
2214
+ const __dirname2 = dirname(__filename2);
2215
+ const daemonPath = join5(__dirname2, "..", "daemon", "index.js");
2216
+ const isWindows = process.platform === "win32";
2217
+ const childEnv = {
2218
+ ...process.env,
2219
+ SPARN_CONFIG: JSON.stringify(config),
2220
+ SPARN_PID_FILE: pidFile,
2221
+ SPARN_LOG_FILE: logFile
2222
+ };
2223
+ if (isWindows) {
2224
+ const configFile = join5(dirname(pidFile), "daemon-config.json");
2225
+ writeFileSync3(configFile, JSON.stringify({ config, pidFile, logFile }), "utf-8");
2226
+ const launcherFile = join5(dirname(pidFile), "daemon-launcher.mjs");
2227
+ const launcherCode = [
2228
+ `import { readFileSync } from 'node:fs';`,
2229
+ `const cfg = JSON.parse(readFileSync(${JSON.stringify(configFile)}, 'utf-8'));`,
2230
+ `process.env.SPARN_CONFIG = JSON.stringify(cfg.config);`,
2231
+ `process.env.SPARN_PID_FILE = cfg.pidFile;`,
2232
+ `process.env.SPARN_LOG_FILE = cfg.logFile;`,
2233
+ `await import(${JSON.stringify(`file:///${daemonPath.replace(/\\/g, "/")}`)});`
2234
+ ].join("\n");
2235
+ writeFileSync3(launcherFile, launcherCode, "utf-8");
2236
+ const ps = spawn(
2237
+ "powershell.exe",
2238
+ [
2239
+ "-NoProfile",
2240
+ "-WindowStyle",
2241
+ "Hidden",
2242
+ "-Command",
2243
+ `Start-Process -FilePath '${process.execPath}' -ArgumentList '${launcherFile}' -WindowStyle Hidden`
2244
+ ],
2245
+ { stdio: "ignore", windowsHide: true }
2246
+ );
2247
+ ps.unref();
2248
+ await new Promise((resolve9) => setTimeout(resolve9, 2e3));
2249
+ if (existsSync6(pidFile)) {
2250
+ const pid = Number.parseInt(readFileSync5(pidFile, "utf-8").trim(), 10);
2251
+ if (!Number.isNaN(pid)) {
2252
+ return {
2253
+ success: true,
2254
+ pid,
2255
+ message: `Daemon started (PID ${pid})`
2256
+ };
2257
+ }
2258
+ }
2259
+ return {
2260
+ success: false,
2261
+ message: "Daemon failed to start (no PID file written)",
2262
+ error: "Timeout waiting for daemon PID"
2263
+ };
2264
+ }
2265
+ const child = spawn(process.execPath, [daemonPath], {
2266
+ detached: true,
2267
+ stdio: "ignore",
2268
+ env: childEnv
2269
+ });
2270
+ child.unref();
2271
+ if (child.pid) {
2272
+ writePidFile(pidFile, child.pid);
2273
+ return {
2274
+ success: true,
2275
+ pid: child.pid,
2276
+ message: `Daemon started (PID ${child.pid})`
2277
+ };
2278
+ }
2279
+ return {
2280
+ success: false,
2281
+ message: "Failed to start daemon (no PID)",
2282
+ error: "No PID"
2283
+ };
2284
+ } catch (error) {
2285
+ return {
2286
+ success: false,
2287
+ message: "Failed to start daemon",
2288
+ error: error instanceof Error ? error.message : String(error)
2289
+ };
2290
+ }
2291
+ }
2292
+ async function stop(config) {
2293
+ const { pidFile } = config.realtime;
2294
+ const status2 = isDaemonRunning(pidFile);
2295
+ if (!status2.running || !status2.pid) {
2296
+ return {
2297
+ success: true,
2298
+ message: "Daemon not running"
2299
+ };
2300
+ }
2301
+ try {
2302
+ const isWindows = process.platform === "win32";
2303
+ if (isWindows) {
2304
+ process.kill(status2.pid);
2305
+ } else {
2306
+ process.kill(status2.pid, "SIGTERM");
2307
+ }
2308
+ const maxWait = 5e3;
2309
+ const interval = 100;
2310
+ let waited = 0;
2311
+ while (waited < maxWait) {
2312
+ try {
2313
+ process.kill(status2.pid, 0);
2314
+ await new Promise((resolve9) => setTimeout(resolve9, interval));
2315
+ waited += interval;
2316
+ } catch {
2317
+ removePidFile(pidFile);
2318
+ return {
2319
+ success: true,
2320
+ message: `Daemon stopped (PID ${status2.pid})`
2321
+ };
2322
+ }
2323
+ }
2324
+ try {
2325
+ if (!isWindows) {
2326
+ process.kill(status2.pid, "SIGKILL");
2327
+ }
2328
+ removePidFile(pidFile);
2329
+ return {
2330
+ success: true,
2331
+ message: `Daemon force killed (PID ${status2.pid})`
2332
+ };
2333
+ } catch {
2334
+ removePidFile(pidFile);
2335
+ return {
2336
+ success: true,
2337
+ message: `Daemon stopped (PID ${status2.pid})`
2338
+ };
2339
+ }
2340
+ } catch (error) {
2341
+ return {
2342
+ success: false,
2343
+ message: "Failed to stop daemon",
2344
+ error: error instanceof Error ? error.message : String(error)
2345
+ };
2346
+ }
2347
+ }
2348
+ async function status(config) {
2349
+ const { pidFile } = config.realtime;
2350
+ const daemonStatus = isDaemonRunning(pidFile);
2351
+ if (!daemonStatus.running || !daemonStatus.pid) {
2352
+ return {
2353
+ running: false,
2354
+ message: "Daemon not running"
2355
+ };
2356
+ }
2357
+ return {
2358
+ running: true,
2359
+ pid: daemonStatus.pid,
2360
+ message: `Daemon running (PID ${daemonStatus.pid})`
2361
+ };
2362
+ }
2363
+ return {
2364
+ start,
2365
+ stop,
2366
+ status
2367
+ };
2368
+ }
2369
+ var init_daemon_process = __esm({
2370
+ "src/daemon/daemon-process.ts"() {
2371
+ "use strict";
2372
+ init_esm_shims();
2373
+ }
2374
+ });
2375
+
2376
+ // src/cli/dashboard/app.tsx
2377
+ init_esm_shims();
2378
+ import { Box as Box7, render, Text as Text7, useApp, useInput } from "ink";
2379
+ import { useEffect as useEffect2, useState as useState4 } from "react";
2380
+
2381
+ // src/cli/dashboard/components/command-input.tsx
2382
+ init_esm_shims();
2383
+ import { Box, Text } from "ink";
2384
+ import TextInput from "ink-text-input";
2385
+ import { useState } from "react";
2386
+
2387
+ // src/cli/dashboard/theme.ts
2388
+ init_esm_shims();
2389
+ var theme = {
2390
+ neuralCyan: "#00D4AA",
2391
+ synapseViolet: "#7B61FF",
2392
+ errorRed: "#FF6B6B",
2393
+ brainPink: "#FF6B9D",
2394
+ dimGray: "#555555",
2395
+ white: "#FFFFFF"
2396
+ };
2397
+
2398
+ // src/cli/dashboard/components/command-input.tsx
2399
+ import { jsx, jsxs } from "react/jsx-runtime";
2400
+ function CommandInput({
2401
+ onSubmit,
2402
+ isRunning,
2403
+ runningCommand,
2404
+ active
2405
+ }) {
2406
+ const [value, setValue] = useState("");
2407
+ const handleSubmit = (input) => {
2408
+ if (input.trim().length === 0) return;
2409
+ onSubmit(input.trim());
2410
+ setValue("");
2411
+ };
2412
+ if (isRunning) {
2413
+ return /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { color: theme.synapseViolet, children: [
2414
+ "Running ",
2415
+ runningCommand || "command",
2416
+ "..."
2417
+ ] }) });
2418
+ }
2419
+ if (!active) {
2420
+ return /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: theme.dimGray, children: "Press : to enter command mode" }) });
2421
+ }
2422
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
2423
+ /* @__PURE__ */ jsxs(Text, { color: theme.neuralCyan, children: [
2424
+ "\u276F",
2425
+ " "
2426
+ ] }),
2427
+ /* @__PURE__ */ jsx(
2428
+ TextInput,
2429
+ {
2430
+ value,
2431
+ onChange: setValue,
2432
+ onSubmit: handleSubmit,
2433
+ focus: active,
2434
+ showCursor: true
2435
+ }
2436
+ ),
2437
+ /* @__PURE__ */ jsx(Text, { color: theme.dimGray, children: " [Esc: monitor mode]" })
2438
+ ] });
2439
+ }
2440
+
2441
+ // src/cli/dashboard/components/output-panel.tsx
2442
+ init_esm_shims();
2443
+ import { Box as Box2, Text as Text2 } from "ink";
2444
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
2445
+ function OutputPanel({
2446
+ lines,
2447
+ scrollOffset,
2448
+ focused,
2449
+ maxVisibleLines = 8
2450
+ }) {
2451
+ const borderColor = focused ? theme.neuralCyan : theme.dimGray;
2452
+ if (lines.length === 0) {
2453
+ return /* @__PURE__ */ jsxs2(
2454
+ Box2,
2455
+ {
2456
+ flexDirection: "column",
2457
+ borderStyle: "round",
2458
+ borderColor,
2459
+ paddingX: 1,
2460
+ width: "100%",
2461
+ children: [
2462
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.dimGray, children: "Output" }),
2463
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dimGray, children: "Type : then a command. Try help for available commands." })
2464
+ ]
2465
+ }
2466
+ );
2467
+ }
2468
+ const visible = lines.slice(scrollOffset, scrollOffset + maxVisibleLines);
2469
+ const hasMore = lines.length > maxVisibleLines;
2470
+ return /* @__PURE__ */ jsxs2(
2471
+ Box2,
2472
+ {
2473
+ flexDirection: "column",
2474
+ borderStyle: "round",
2475
+ borderColor,
2476
+ paddingX: 1,
2477
+ width: "100%",
2478
+ children: [
2479
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.dimGray, children: "Output" }),
2480
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: visible.map((line, i) => /* @__PURE__ */ jsx2(Text2, { color: line.color, children: line.text }, `${scrollOffset + i}`)) }),
2481
+ hasMore && /* @__PURE__ */ jsxs2(Text2, { color: theme.dimGray, children: [
2482
+ "[",
2483
+ scrollOffset + 1,
2484
+ "-",
2485
+ Math.min(scrollOffset + maxVisibleLines, lines.length),
2486
+ "/",
2487
+ lines.length,
2488
+ "]",
2489
+ focused ? " \u2191\u2193 scroll" : ""
2490
+ ] })
2491
+ ]
2492
+ }
2493
+ );
2494
+ }
2495
+
2496
+ // src/cli/dashboard/hooks/use-command-executor.ts
2497
+ init_esm_shims();
2498
+ import { useCallback, useState as useState2 } from "react";
2499
+
2500
+ // src/cli/dashboard/command-parser.ts
2501
+ init_esm_shims();
2502
+ function tokenize(input) {
2503
+ const tokens = [];
2504
+ let current = "";
2505
+ let inQuote = null;
2506
+ for (const ch of input) {
2507
+ if (inQuote) {
2508
+ if (ch === inQuote) {
2509
+ inQuote = null;
2510
+ } else {
2511
+ current += ch;
2512
+ }
2513
+ } else if (ch === '"' || ch === "'") {
2514
+ inQuote = ch;
2515
+ } else if (ch === " " || ch === " ") {
2516
+ if (current.length > 0) {
2517
+ tokens.push(current);
2518
+ current = "";
2519
+ }
2520
+ } else {
2521
+ current += ch;
2522
+ }
2523
+ }
2524
+ if (current.length > 0) {
2525
+ tokens.push(current);
2526
+ }
2527
+ return tokens;
2528
+ }
2529
+ function parseCommand(input) {
2530
+ const trimmed = input.trim();
2531
+ if (trimmed.length === 0) return null;
2532
+ const tokens = tokenize(trimmed);
2533
+ const first = tokens[0];
2534
+ if (!first) return null;
2535
+ const name = first.toLowerCase();
2536
+ const positionals = [];
2537
+ const flags = {};
2538
+ let i = 1;
2539
+ while (i < tokens.length) {
2540
+ const token = tokens[i];
2541
+ if (!token) {
2542
+ i++;
2543
+ continue;
2544
+ }
2545
+ if (token.startsWith("--")) {
2546
+ const key = token.slice(2);
2547
+ const next = tokens[i + 1];
2548
+ if (next !== void 0 && !next.startsWith("-")) {
2549
+ flags[key] = next;
2550
+ i += 2;
2551
+ } else {
2552
+ flags[key] = true;
2553
+ i++;
2554
+ }
2555
+ } else if (token.startsWith("-") && token.length === 2) {
2556
+ const key = token.slice(1);
2557
+ const next = tokens[i + 1];
2558
+ if (next !== void 0 && !next.startsWith("-")) {
2559
+ flags[key] = next;
2560
+ i += 2;
2561
+ } else {
2562
+ flags[key] = true;
2563
+ i++;
2564
+ }
2565
+ } else {
2566
+ positionals.push(token);
2567
+ i++;
2568
+ }
2569
+ }
2570
+ return { name, positionals, flags };
2571
+ }
2572
+
2573
+ // src/cli/dashboard/formatters.ts
2574
+ init_esm_shims();
2575
+ function formatOptimize(result) {
2576
+ const pct = (result.reduction * 100).toFixed(1);
2577
+ return [
2578
+ { text: `\u2713 Optimization complete in ${result.durationMs}ms`, color: theme.neuralCyan },
2579
+ {
2580
+ text: ` Tokens: ${result.tokensBefore} \u2192 ${result.tokensAfter} (${pct}% reduction)`
2581
+ },
2582
+ {
2583
+ text: ` Entries: processed=${result.entriesProcessed} kept=${result.entriesKept}`
2584
+ },
2585
+ ...result.outputFile ? [{ text: ` Output: ${result.outputFile}`, color: theme.dimGray }] : []
2586
+ ];
2587
+ }
2588
+ function formatStats(result) {
2589
+ if (result.resetConfirmed) {
2590
+ return [{ text: "\u2713 Statistics reset", color: theme.neuralCyan }];
2591
+ }
2592
+ const avgPct = (result.averageReduction * 100).toFixed(1);
2593
+ const lines = [
2594
+ { text: "Optimization Statistics", color: theme.neuralCyan },
2595
+ { text: ` Total commands: ${result.totalCommands}` },
2596
+ { text: ` Tokens saved: ${result.totalTokensSaved}` },
2597
+ { text: ` Avg reduction: ${avgPct}%` }
2598
+ ];
2599
+ if (result.graph) {
2600
+ for (const line of result.graph.split("\n")) {
2601
+ lines.push({ text: line, color: theme.dimGray });
2602
+ }
2603
+ }
2604
+ return lines;
2605
+ }
2606
+ function formatConsolidate(result) {
2607
+ const pct = (result.compressionRatio * 100).toFixed(1);
2608
+ return [
2609
+ { text: `\u2713 Consolidation complete in ${result.durationMs}ms`, color: theme.neuralCyan },
2610
+ { text: ` Entries: ${result.entriesBefore} \u2192 ${result.entriesAfter}` },
2611
+ { text: ` Decayed removed: ${result.decayedRemoved}` },
2612
+ { text: ` Duplicates removed: ${result.duplicatesRemoved}` },
2613
+ { text: ` Compression: ${pct}%` }
2614
+ ];
2615
+ }
2616
+ function formatSearch(result) {
2617
+ if (result.indexStats) {
2618
+ const s = result.indexStats;
2619
+ return [
2620
+ {
2621
+ text: `\u2713 Indexed ${s.filesIndexed} files (${s.totalLines} lines) in ${s.duration}ms`,
2622
+ color: theme.neuralCyan
2623
+ }
2624
+ ];
2625
+ }
2626
+ if (!result.results || result.results.length === 0) {
2627
+ return [{ text: result.message || "No results found", color: theme.dimGray }];
2628
+ }
2629
+ const lines = [
2630
+ { text: `${result.results.length} result(s):`, color: theme.neuralCyan }
2631
+ ];
2632
+ for (const r of result.results) {
2633
+ lines.push({
2634
+ text: ` ${r.filePath}:${r.lineNumber} (score: ${r.score.toFixed(2)})`,
2635
+ color: theme.synapseViolet
2636
+ });
2637
+ lines.push({ text: ` ${r.content.trim()}` });
2638
+ }
2639
+ return lines;
2640
+ }
2641
+ function formatGraph(result) {
2642
+ const a = result.analysis;
2643
+ const lines = [
2644
+ { text: `Graph: ${result.nodeCount} nodes`, color: theme.neuralCyan },
2645
+ { text: ` Tokens: ${a.totalTokens} (optimized: ${a.optimizedTokens})` }
2646
+ ];
2647
+ if (a.hotPaths.length > 0) {
2648
+ lines.push({
2649
+ text: ` Hot paths: ${a.hotPaths.slice(0, 5).join(", ")}`,
2650
+ color: theme.brainPink
2651
+ });
2652
+ }
2653
+ if (a.orphans.length > 0) {
2654
+ lines.push({ text: ` Orphans: ${a.orphans.length}`, color: theme.dimGray });
2655
+ }
2656
+ return lines;
2657
+ }
2658
+ function formatPlan(result) {
2659
+ return [{ text: `\u2713 ${result.message}`, color: theme.neuralCyan }];
2660
+ }
2661
+ function formatPlanList(result) {
2662
+ if (result.plans.length === 0) {
2663
+ return [{ text: "No plans found", color: theme.dimGray }];
2664
+ }
2665
+ const lines = [
2666
+ { text: `${result.plans.length} plan(s):`, color: theme.neuralCyan }
2667
+ ];
2668
+ for (const p of result.plans) {
2669
+ const statusColor = p.status === "completed" ? theme.neuralCyan : theme.synapseViolet;
2670
+ lines.push({ text: ` ${p.id} [${p.status}] ${p.task}`, color: statusColor });
2671
+ }
2672
+ return lines;
2673
+ }
2674
+ function formatExec(result) {
2675
+ return [
2676
+ { text: `\u2713 ${result.message}`, color: theme.neuralCyan },
2677
+ {
2678
+ text: ` Max reads: ${result.constraints.maxFileReads}, Budget: ${result.constraints.tokenBudget} tokens`
2679
+ }
2680
+ ];
2681
+ }
2682
+ function formatVerify(result) {
2683
+ const v = result.verification;
2684
+ const icon = v.success ? "\u2713" : "\u2717";
2685
+ const color = v.success ? theme.neuralCyan : theme.errorRed;
2686
+ return [
2687
+ { text: `${icon} ${result.message}`, color },
2688
+ { text: ` Steps: ${v.stepsCompleted}/${v.totalSteps} completed` }
2689
+ ];
2690
+ }
2691
+ function formatDocs(result) {
2692
+ return [{ text: `\u2713 ${result.message}`, color: theme.neuralCyan }];
2693
+ }
2694
+ function formatDebt(result) {
2695
+ if (result.stats) {
2696
+ const s = result.stats;
2697
+ return [
2698
+ { text: "Debt Stats", color: theme.neuralCyan },
2699
+ { text: ` Total: ${s.total}, Open: ${s.open}, Overdue: ${s.overdue}` },
2700
+ { text: ` Repayment rate: ${(s.repaymentRate * 100).toFixed(0)}%` }
2701
+ ];
2702
+ }
2703
+ if (result.debts) {
2704
+ if (result.debts.length === 0) {
2705
+ return [{ text: "No debt items", color: theme.dimGray }];
2706
+ }
2707
+ const lines = [
2708
+ { text: `${result.debts.length} debt item(s):`, color: theme.neuralCyan }
2709
+ ];
2710
+ for (const d of result.debts) {
2711
+ const sevColor = d.severity === "P0" ? theme.errorRed : d.severity === "P1" ? theme.synapseViolet : theme.dimGray;
2712
+ const desc = d.description.length > 40 ? `${d.description.substring(0, 37)}...` : d.description;
2713
+ lines.push({
2714
+ text: ` [${d.severity}] ${d.id.slice(0, 8)} ${desc} (${d.status})`,
2715
+ color: sevColor
2716
+ });
2717
+ }
2718
+ return lines;
2719
+ }
2720
+ return [{ text: `\u2713 ${result.message}`, color: theme.neuralCyan }];
2721
+ }
2722
+ function formatError(err) {
2723
+ return [{ text: `Error: ${err.message}`, color: theme.errorRed }];
2724
+ }
2725
+ function formatHelp() {
2726
+ return [
2727
+ { text: "Available commands:", color: theme.neuralCyan },
2728
+ { text: " optimize --input <text> [--input-file <path>] [--output-file <path>] [--dry-run]" },
2729
+ { text: " stats [--graph] [--reset --confirm-reset]" },
2730
+ { text: " consolidate" },
2731
+ { text: " search <query> [--glob <pattern>] [--max-results <n>]" },
2732
+ { text: " search init | search refresh" },
2733
+ { text: " graph [--entry <file>] [--depth <n>] [--focus <file>]" },
2734
+ { text: " plan <task> [--files <f1,f2>] [--searches <q1,q2>]" },
2735
+ { text: " plan list" },
2736
+ { text: " exec <planId>" },
2737
+ { text: " verify <planId>" },
2738
+ { text: " docs [--output <path>] [--no-graph]" },
2739
+ { text: " debt add <desc> [--severity P0|P1|P2] [--due <date>] [--token-cost <n>]" },
2740
+ { text: " debt list [--overdue]" },
2741
+ { text: " debt resolve <id> [--token-cost <n>]" },
2742
+ { text: " debt start <id>" },
2743
+ { text: " debt stats" },
2744
+ { text: " clear Clear output" },
2745
+ { text: " help Show this help" },
2746
+ { text: "" },
2747
+ { text: "Keybinds:", color: theme.neuralCyan },
2748
+ { text: " : Enter command mode" },
2749
+ { text: " Escape Exit command mode" },
2750
+ { text: " Tab Cycle panel focus" },
2751
+ { text: " q Quit (in monitor mode)" }
2752
+ ];
2753
+ }
2754
+
2755
+ // src/cli/dashboard/hooks/use-command-executor.ts
2756
+ var MAX_OUTPUT_LINES = 200;
2757
+ function useCommandExecutor({
2758
+ getMemory,
2759
+ forceRefresh,
2760
+ dbPath,
2761
+ projectRoot
2762
+ }) {
2763
+ const [outputLines, setOutputLines] = useState2([]);
2764
+ const [isRunning, setIsRunning] = useState2(false);
2765
+ const [runningCommand, setRunningCommand] = useState2();
2766
+ const appendLines = useCallback((lines) => {
2767
+ setOutputLines((prev) => {
2768
+ const next = [...prev, ...lines];
2769
+ return next.length > MAX_OUTPUT_LINES ? next.slice(-MAX_OUTPUT_LINES) : next;
2770
+ });
2771
+ }, []);
2772
+ const clearOutput = useCallback(() => {
2773
+ setOutputLines([]);
2774
+ }, []);
2775
+ const execute = useCallback(
2776
+ (rawInput) => {
2777
+ const parsed = parseCommand(rawInput);
2778
+ if (!parsed) return;
2779
+ if (parsed.name === "clear") {
2780
+ clearOutput();
2781
+ return;
2782
+ }
2783
+ if (parsed.name === "help") {
2784
+ appendLines(formatHelp());
2785
+ return;
2786
+ }
2787
+ setIsRunning(true);
2788
+ setRunningCommand(parsed.name);
2789
+ void (async () => {
2790
+ try {
2791
+ const lines = await executeCommand(parsed.name, parsed.positionals, parsed.flags, {
2792
+ getMemory,
2793
+ dbPath,
2794
+ projectRoot
2795
+ });
2796
+ appendLines(lines);
2797
+ forceRefresh();
2798
+ } catch (err) {
2799
+ appendLines(formatError(err instanceof Error ? err : new Error(String(err))));
2800
+ } finally {
2801
+ setIsRunning(false);
2802
+ setRunningCommand(void 0);
2803
+ }
2804
+ })();
2805
+ },
2806
+ [getMemory, forceRefresh, dbPath, projectRoot, appendLines, clearOutput]
2807
+ );
2808
+ return { execute, outputLines, isRunning, runningCommand, clearOutput };
2809
+ }
2810
+ async function executeCommand(name, positionals, flags, ctx) {
2811
+ switch (name) {
2812
+ case "optimize": {
2813
+ const memory = ctx.getMemory();
2814
+ if (!memory) throw new Error("Memory not initialized");
2815
+ const { optimizeCommand: optimizeCommand2 } = await Promise.resolve().then(() => (init_optimize(), optimize_exports));
2816
+ const result = await optimizeCommand2({
2817
+ memory,
2818
+ input: typeof flags["input"] === "string" ? flags["input"] : void 0,
2819
+ inputFile: typeof flags["input-file"] === "string" ? flags["input-file"] : void 0,
2820
+ outputFile: typeof flags["output-file"] === "string" ? flags["output-file"] : void 0,
2821
+ dryRun: flags["dry-run"] === true,
2822
+ verbose: flags["verbose"] === true || flags["v"] === true
2823
+ });
2824
+ return formatOptimize(result);
2825
+ }
2826
+ case "stats": {
2827
+ const memory = ctx.getMemory();
2828
+ if (!memory) throw new Error("Memory not initialized");
2829
+ const { statsCommand: statsCommand2 } = await Promise.resolve().then(() => (init_stats(), stats_exports));
2830
+ const result = await statsCommand2({
2831
+ memory,
2832
+ graph: flags["graph"] === true || flags["g"] === true,
2833
+ reset: flags["reset"] === true,
2834
+ confirmReset: flags["confirm-reset"] === true
2835
+ });
2836
+ return formatStats(result);
2837
+ }
2838
+ case "consolidate": {
2839
+ const memory = ctx.getMemory();
2840
+ if (!memory) throw new Error("Memory not initialized");
2841
+ const { consolidateCommand: consolidateCommand2 } = await Promise.resolve().then(() => (init_consolidate(), consolidate_exports));
2842
+ const result = await consolidateCommand2({ memory });
2843
+ return formatConsolidate(result);
2844
+ }
2845
+ case "search": {
2846
+ const { searchCommand: searchCommand2 } = await Promise.resolve().then(() => (init_search(), search_exports));
2847
+ const subcommand = positionals[0] === "init" || positionals[0] === "refresh" ? positionals[0] : void 0;
2848
+ const query = subcommand ? void 0 : positionals.join(" ") || void 0;
2849
+ const result = await searchCommand2({
2850
+ query,
2851
+ subcommand,
2852
+ glob: typeof flags["glob"] === "string" ? flags["glob"] : void 0,
2853
+ maxResults: typeof flags["max-results"] === "string" ? Number.parseInt(flags["max-results"], 10) : void 0
2854
+ });
2855
+ return formatSearch(result);
2856
+ }
2857
+ case "graph": {
2858
+ const { graphCommand: graphCommand2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
2859
+ const result = await graphCommand2({
2860
+ entry: typeof flags["entry"] === "string" ? flags["entry"] : void 0,
2861
+ depth: typeof flags["depth"] === "string" ? Number.parseInt(flags["depth"], 10) : void 0,
2862
+ focus: typeof flags["focus"] === "string" ? flags["focus"] : void 0
2863
+ });
2864
+ return formatGraph(result);
2865
+ }
2866
+ case "plan": {
2867
+ if (positionals[0] === "list") {
2868
+ const { planListCommand: planListCommand2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
2869
+ const result2 = await planListCommand2({});
2870
+ return formatPlanList(result2);
2871
+ }
2872
+ const { planCommand: planCommand2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
2873
+ const task = positionals.join(" ");
2874
+ if (!task) throw new Error("Task description required: plan <task>");
2875
+ const files = typeof flags["files"] === "string" ? flags["files"].split(",") : void 0;
2876
+ const searches = typeof flags["searches"] === "string" ? flags["searches"].split(",") : void 0;
2877
+ const result = await planCommand2({ task, files, searches });
2878
+ return formatPlan(result);
2879
+ }
2880
+ case "exec": {
2881
+ const planId = positionals[0];
2882
+ if (!planId) throw new Error("Plan ID required: exec <planId>");
2883
+ const { execCommand: execCommand2 } = await Promise.resolve().then(() => (init_exec(), exec_exports));
2884
+ const result = await execCommand2({ planId });
2885
+ return formatExec(result);
2886
+ }
2887
+ case "verify": {
2888
+ const planId = positionals[0];
2889
+ if (!planId) throw new Error("Plan ID required: verify <planId>");
2890
+ const { verifyCommand: verifyCommand2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
2891
+ const result = await verifyCommand2({ planId });
2892
+ return formatVerify(result);
2893
+ }
2894
+ case "docs": {
2895
+ const { docsCommand: docsCommand2 } = await Promise.resolve().then(() => (init_docs(), docs_exports));
2896
+ const result = await docsCommand2({
2897
+ output: typeof flags["output"] === "string" ? flags["output"] : void 0,
2898
+ includeGraph: flags["no-graph"] !== true
2899
+ });
2900
+ return formatDocs(result);
2901
+ }
2902
+ case "debt": {
2903
+ const subcommand = positionals[0];
2904
+ if (!subcommand || !["add", "list", "resolve", "stats", "start"].includes(subcommand)) {
2905
+ throw new Error("Subcommand required: debt add|list|resolve|start|stats");
2906
+ }
2907
+ const { debtCommand: debtCommand2 } = await Promise.resolve().then(() => (init_debt(), debt_exports));
2908
+ const result = await debtCommand2({
2909
+ subcommand,
2910
+ description: subcommand === "add" ? positionals.slice(1).join(" ") || void 0 : void 0,
2911
+ severity: typeof flags["severity"] === "string" ? flags["severity"] : void 0,
2912
+ due: typeof flags["due"] === "string" ? flags["due"] : void 0,
2913
+ tokenCost: typeof flags["token-cost"] === "string" ? Number.parseInt(flags["token-cost"], 10) : void 0,
2914
+ id: subcommand === "resolve" || subcommand === "start" ? positionals[1] : void 0,
2915
+ overdue: flags["overdue"] === true
2916
+ });
2917
+ return formatDebt(result);
2918
+ }
2919
+ default:
2920
+ throw new Error(`Unknown command: ${name}. Type help for available commands.`);
2921
+ }
2922
+ }
2923
+
2924
+ // src/cli/dashboard/hooks/use-data.ts
2925
+ init_esm_shims();
2926
+ init_debt_tracker();
2927
+ init_dependency_graph();
2928
+ import { statSync as statSync3 } from "fs";
2929
+ import { useCallback as useCallback2, useEffect, useRef, useState as useState3 } from "react";
2930
+
2931
+ // src/core/kv-memory.ts
2932
+ init_esm_shims();
2933
+ import { copyFileSync, existsSync as existsSync5 } from "fs";
2934
+ import Database3 from "better-sqlite3";
2935
+ function createBackup(dbPath) {
2936
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2937
+ const backupPath = `${dbPath}.backup-${timestamp}`;
2938
+ try {
2939
+ copyFileSync(dbPath, backupPath);
2940
+ console.log(`\u2713 Database backed up to: ${backupPath}`);
2941
+ return backupPath;
2942
+ } catch (error) {
2943
+ console.error(`Warning: Could not create backup: ${error}`);
2944
+ return "";
2945
+ }
2946
+ }
2947
+ async function createKVMemory(dbPath) {
2948
+ let db;
2949
+ try {
2950
+ db = new Database3(dbPath);
2951
+ const integrityCheck = db.pragma("quick_check", { simple: true });
2952
+ if (integrityCheck !== "ok") {
2953
+ console.error("\u26A0 Database corruption detected!");
2954
+ db.close();
2955
+ if (existsSync5(dbPath)) {
2956
+ const backupPath = createBackup(dbPath);
2957
+ if (backupPath) {
2958
+ console.log(`Backup created at: ${backupPath}`);
2959
+ }
2960
+ }
2961
+ console.log("Attempting database recovery...");
2962
+ db = new Database3(dbPath);
2963
+ }
2964
+ } catch (error) {
2965
+ console.error("\u26A0 Database error detected:", error);
2966
+ if (existsSync5(dbPath)) {
2967
+ createBackup(dbPath);
2968
+ console.log("Creating new database...");
2969
+ }
2970
+ db = new Database3(dbPath);
2971
+ }
2972
+ db.pragma("journal_mode = WAL");
2973
+ db.pragma("foreign_keys = ON");
2974
+ db.exec(`
2975
+ CREATE TABLE IF NOT EXISTS entries_index (
2976
+ id TEXT PRIMARY KEY NOT NULL,
2977
+ hash TEXT UNIQUE NOT NULL,
2978
+ timestamp INTEGER NOT NULL,
2979
+ score REAL NOT NULL DEFAULT 0.0 CHECK(score >= 0.0 AND score <= 1.0),
2980
+ ttl INTEGER NOT NULL CHECK(ttl >= 0),
2981
+ state TEXT NOT NULL CHECK(state IN ('silent', 'ready', 'active')),
2982
+ accessCount INTEGER NOT NULL DEFAULT 0 CHECK(accessCount >= 0),
2983
+ isBTSP INTEGER NOT NULL DEFAULT 0 CHECK(isBTSP IN (0, 1)),
2984
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
2985
+ );
2986
+ `);
2987
+ db.exec(`
2988
+ CREATE TABLE IF NOT EXISTS entries_value (
2989
+ id TEXT PRIMARY KEY NOT NULL,
2990
+ content TEXT NOT NULL,
2991
+ tags TEXT,
2992
+ metadata TEXT,
2993
+ FOREIGN KEY (id) REFERENCES entries_index(id) ON DELETE CASCADE
2994
+ );
2995
+ `);
2996
+ db.exec(`
2997
+ CREATE TABLE IF NOT EXISTS optimization_stats (
2998
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2999
+ timestamp INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
3000
+ tokens_before INTEGER NOT NULL,
3001
+ tokens_after INTEGER NOT NULL,
3002
+ entries_pruned INTEGER NOT NULL,
3003
+ duration_ms INTEGER NOT NULL
3004
+ );
3005
+ `);
3006
+ db.exec(`
3007
+ CREATE INDEX IF NOT EXISTS idx_entries_state ON entries_index(state);
3008
+ CREATE INDEX IF NOT EXISTS idx_entries_score ON entries_index(score DESC);
3009
+ CREATE INDEX IF NOT EXISTS idx_entries_hash ON entries_index(hash);
3010
+ CREATE INDEX IF NOT EXISTS idx_entries_timestamp ON entries_index(timestamp DESC);
3011
+ CREATE INDEX IF NOT EXISTS idx_stats_timestamp ON optimization_stats(timestamp DESC);
3012
+ `);
3013
+ db.exec(`
3014
+ CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(id, content, tokenize='porter');
3015
+ `);
3016
+ db.exec(`
3017
+ CREATE TRIGGER IF NOT EXISTS entries_fts_insert
3018
+ AFTER INSERT ON entries_value
3019
+ BEGIN
3020
+ INSERT OR REPLACE INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
3021
+ END;
3022
+ `);
3023
+ db.exec(`
3024
+ CREATE TRIGGER IF NOT EXISTS entries_fts_delete
3025
+ AFTER DELETE ON entries_value
3026
+ BEGIN
3027
+ DELETE FROM entries_fts WHERE id = OLD.id;
3028
+ END;
3029
+ `);
3030
+ db.exec(`
3031
+ CREATE TRIGGER IF NOT EXISTS entries_fts_update
3032
+ AFTER UPDATE ON entries_value
3033
+ BEGIN
3034
+ DELETE FROM entries_fts WHERE id = OLD.id;
3035
+ INSERT INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
3036
+ END;
3037
+ `);
3038
+ db.exec(`
3039
+ INSERT OR IGNORE INTO entries_fts(id, content)
3040
+ SELECT id, content FROM entries_value
3041
+ WHERE id NOT IN (SELECT id FROM entries_fts);
3042
+ `);
3043
+ const putIndexStmt = db.prepare(`
3044
+ INSERT OR REPLACE INTO entries_index
3045
+ (id, hash, timestamp, score, ttl, state, accessCount, isBTSP)
3046
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3047
+ `);
3048
+ const putValueStmt = db.prepare(`
3049
+ INSERT OR REPLACE INTO entries_value
3050
+ (id, content, tags, metadata)
3051
+ VALUES (?, ?, ?, ?)
3052
+ `);
3053
+ const getStmt = db.prepare(`
3054
+ SELECT
3055
+ i.id, i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
3056
+ v.content, v.tags, v.metadata
3057
+ FROM entries_index i
3058
+ JOIN entries_value v ON i.id = v.id
3059
+ WHERE i.id = ?
3060
+ `);
3061
+ const deleteIndexStmt = db.prepare("DELETE FROM entries_index WHERE id = ?");
3062
+ const deleteValueStmt = db.prepare("DELETE FROM entries_value WHERE id = ?");
3063
+ return {
3064
+ async put(entry) {
3065
+ const transaction = db.transaction(() => {
3066
+ putIndexStmt.run(
3067
+ entry.id,
3068
+ entry.hash,
3069
+ entry.timestamp,
3070
+ entry.score,
3071
+ entry.ttl,
3072
+ entry.state,
3073
+ entry.accessCount,
3074
+ entry.isBTSP ? 1 : 0
3075
+ );
3076
+ putValueStmt.run(
3077
+ entry.id,
3078
+ entry.content,
3079
+ JSON.stringify(entry.tags),
3080
+ JSON.stringify(entry.metadata)
3081
+ );
3082
+ });
3083
+ transaction();
3084
+ },
3085
+ async get(id) {
3086
+ const row = getStmt.get(id);
3087
+ if (!row) {
3088
+ return null;
3089
+ }
3090
+ const r = row;
3091
+ return {
3092
+ id: r.id,
3093
+ content: r.content,
3094
+ hash: r.hash,
3095
+ timestamp: r.timestamp,
3096
+ score: r.score,
3097
+ ttl: r.ttl,
3098
+ state: r.state,
3099
+ accessCount: r.accessCount,
3100
+ tags: r.tags ? JSON.parse(r.tags) : [],
3101
+ metadata: r.metadata ? JSON.parse(r.metadata) : {},
3102
+ isBTSP: r.isBTSP === 1
3103
+ };
3104
+ },
3105
+ async query(filters) {
3106
+ let sql = `
3107
+ SELECT
3108
+ i.id, i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
3109
+ v.content, v.tags, v.metadata
3110
+ FROM entries_index i
3111
+ JOIN entries_value v ON i.id = v.id
3112
+ WHERE 1=1
3113
+ `;
3114
+ const params = [];
3115
+ if (filters.state) {
3116
+ sql += " AND i.state = ?";
3117
+ params.push(filters.state);
3118
+ }
3119
+ if (filters.minScore !== void 0) {
3120
+ sql += " AND i.score >= ?";
3121
+ params.push(filters.minScore);
3122
+ }
3123
+ if (filters.maxScore !== void 0) {
3124
+ sql += " AND i.score <= ?";
3125
+ params.push(filters.maxScore);
3126
+ }
3127
+ if (filters.isBTSP !== void 0) {
3128
+ sql += " AND i.isBTSP = ?";
3129
+ params.push(filters.isBTSP ? 1 : 0);
3130
+ }
3131
+ if (filters.tags && filters.tags.length > 0) {
3132
+ for (const tag of filters.tags) {
3133
+ sql += " AND v.tags LIKE ?";
3134
+ params.push(`%"${tag}"%`);
3135
+ }
3136
+ }
3137
+ sql += " ORDER BY i.score DESC";
3138
+ if (filters.limit) {
3139
+ sql += " LIMIT ?";
3140
+ params.push(filters.limit);
3141
+ if (filters.offset) {
3142
+ sql += " OFFSET ?";
3143
+ params.push(filters.offset);
3144
+ }
3145
+ }
3146
+ const stmt = db.prepare(sql);
3147
+ const rows = stmt.all(...params);
3148
+ return rows.map((row) => {
3149
+ const r = row;
3150
+ return {
3151
+ id: r.id,
3152
+ content: r.content,
3153
+ hash: r.hash,
3154
+ timestamp: r.timestamp,
3155
+ score: r.score,
3156
+ ttl: r.ttl,
3157
+ state: r.state,
3158
+ accessCount: r.accessCount,
3159
+ tags: r.tags ? JSON.parse(r.tags) : [],
3160
+ metadata: r.metadata ? JSON.parse(r.metadata) : {},
3161
+ isBTSP: r.isBTSP === 1
3162
+ };
3163
+ });
3164
+ },
3165
+ async delete(id) {
3166
+ const transaction = db.transaction(() => {
3167
+ deleteIndexStmt.run(id);
3168
+ deleteValueStmt.run(id);
3169
+ });
3170
+ transaction();
3171
+ },
3172
+ async list() {
3173
+ const stmt = db.prepare("SELECT id FROM entries_index");
3174
+ const rows = stmt.all();
3175
+ return rows.map((r) => r.id);
3176
+ },
3177
+ async compact() {
3178
+ const before = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
3179
+ const now = Date.now();
3180
+ db.prepare("DELETE FROM entries_index WHERE isBTSP = 0 AND (timestamp + ttl * 1000) < ?").run(
3181
+ now
3182
+ );
3183
+ db.exec("DELETE FROM entries_index WHERE isBTSP = 0 AND ttl <= 0");
3184
+ const candidates = db.prepare("SELECT id, timestamp, ttl FROM entries_index WHERE isBTSP = 0").all();
3185
+ for (const row of candidates) {
3186
+ const ageSeconds = Math.max(0, (now - row.timestamp) / 1e3);
3187
+ const ttlSeconds = row.ttl;
3188
+ if (ttlSeconds <= 0) continue;
3189
+ const decay = 1 - Math.exp(-ageSeconds / ttlSeconds);
3190
+ if (decay >= 0.95) {
3191
+ db.prepare("DELETE FROM entries_index WHERE id = ?").run(row.id);
3192
+ }
3193
+ }
3194
+ db.exec("DELETE FROM entries_value WHERE id NOT IN (SELECT id FROM entries_index)");
3195
+ db.exec("VACUUM");
3196
+ const after = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
3197
+ return before.count - after.count;
3198
+ },
3199
+ async close() {
3200
+ db.close();
3201
+ },
3202
+ async recordOptimization(stats) {
3203
+ const stmt = db.prepare(`
3204
+ INSERT INTO optimization_stats (timestamp, tokens_before, tokens_after, entries_pruned, duration_ms)
3205
+ VALUES (?, ?, ?, ?, ?)
3206
+ `);
3207
+ stmt.run(
3208
+ stats.timestamp,
3209
+ stats.tokens_before,
3210
+ stats.tokens_after,
3211
+ stats.entries_pruned,
3212
+ stats.duration_ms
3213
+ );
3214
+ db.prepare(
3215
+ "DELETE FROM optimization_stats WHERE id NOT IN (SELECT id FROM optimization_stats ORDER BY timestamp DESC LIMIT 1000)"
3216
+ ).run();
3217
+ },
3218
+ async getOptimizationStats() {
3219
+ const stmt = db.prepare(`
3220
+ SELECT id, timestamp, tokens_before, tokens_after, entries_pruned, duration_ms
3221
+ FROM optimization_stats
3222
+ ORDER BY timestamp DESC
3223
+ `);
3224
+ const rows = stmt.all();
3225
+ return rows;
3226
+ },
3227
+ async clearOptimizationStats() {
3228
+ db.exec("DELETE FROM optimization_stats");
3229
+ },
3230
+ async searchFTS(query, limit = 10) {
3231
+ if (!query || query.trim().length === 0) return [];
3232
+ const sanitized = query.replace(/[{}()[\]"':*^~]/g, " ").trim();
3233
+ if (sanitized.length === 0) return [];
3234
+ const stmt = db.prepare(`
3235
+ SELECT
3236
+ f.id, f.content, rank,
3237
+ i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
3238
+ v.tags, v.metadata
3239
+ FROM entries_fts f
3240
+ JOIN entries_index i ON f.id = i.id
3241
+ JOIN entries_value v ON f.id = v.id
3242
+ WHERE entries_fts MATCH ?
3243
+ ORDER BY rank
3244
+ LIMIT ?
3245
+ `);
3246
+ try {
3247
+ const rows = stmt.all(sanitized, limit);
3248
+ return rows.map((r) => ({
3249
+ entry: {
3250
+ id: r.id,
3251
+ content: r.content,
3252
+ hash: r.hash,
3253
+ timestamp: r.timestamp,
3254
+ score: r.score,
3255
+ ttl: r.ttl,
3256
+ state: r.state,
3257
+ accessCount: r.accessCount,
3258
+ tags: r.tags ? JSON.parse(r.tags) : [],
3259
+ metadata: r.metadata ? JSON.parse(r.metadata) : {},
3260
+ isBTSP: r.isBTSP === 1
3261
+ },
3262
+ rank: r.rank
3263
+ }));
3264
+ } catch {
3265
+ return [];
3266
+ }
3267
+ }
3268
+ };
3269
+ }
3270
+
3271
+ // src/cli/dashboard/hooks/use-data.ts
3272
+ var NOOP = () => {
3273
+ };
3274
+ var EMPTY_STATE = {
3275
+ optimizationStats: [],
3276
+ totalEntries: 0,
3277
+ stateDistribution: { active: 0, ready: 0, silent: 0 },
3278
+ debts: [],
3279
+ debtStats: null,
3280
+ graphAnalysis: null,
3281
+ graphNodes: /* @__PURE__ */ new Map(),
3282
+ daemon: null,
3283
+ dbSizeBytes: 0,
3284
+ loading: true,
3285
+ error: null,
3286
+ lastRefresh: /* @__PURE__ */ new Date(),
3287
+ getMemory: () => null,
3288
+ forceRefresh: NOOP
3289
+ };
3290
+ function useDashboardData(dbPath, projectRoot, refreshInterval = 2e3) {
3291
+ const [data, setData] = useState3(EMPTY_STATE);
3292
+ const memoryRef = useRef(null);
3293
+ const mountedRef = useRef(true);
3294
+ const dbPathRef = useRef(dbPath);
3295
+ dbPathRef.current = dbPath;
3296
+ const getMemory = useCallback2(() => memoryRef.current, []);
3297
+ const doRefreshFast = useCallback2(async () => {
3298
+ const memory = memoryRef.current;
3299
+ if (!memory) return;
3300
+ await refreshFast(memory, dbPathRef.current, projectRoot, mountedRef, setData);
3301
+ }, [projectRoot]);
3302
+ const forceRefresh = useCallback2(() => {
3303
+ void doRefreshFast();
3304
+ }, [doRefreshFast]);
3305
+ useEffect(() => {
3306
+ let timer = null;
3307
+ async function init() {
3308
+ try {
3309
+ const memory = await createKVMemory(dbPath);
3310
+ if (!mountedRef.current) {
3311
+ await memory.close();
3312
+ return;
3313
+ }
3314
+ memoryRef.current = memory;
3315
+ await refreshFast(memory, dbPath, projectRoot, mountedRef, setData);
3316
+ await refreshGraph(projectRoot, mountedRef, setData);
3317
+ timer = setInterval(() => {
3318
+ if (memoryRef.current) {
3319
+ void refreshFast(memoryRef.current, dbPath, projectRoot, mountedRef, setData);
3320
+ }
3321
+ }, refreshInterval);
3322
+ } catch (err) {
3323
+ if (mountedRef.current) {
3324
+ setData((prev) => ({
3325
+ ...prev,
3326
+ loading: false,
3327
+ error: err instanceof Error ? err.message : String(err)
3328
+ }));
3329
+ }
3330
+ }
3331
+ }
3332
+ void init();
3333
+ return () => {
3334
+ mountedRef.current = false;
3335
+ if (timer) clearInterval(timer);
3336
+ if (memoryRef.current) {
3337
+ void memoryRef.current.close();
3338
+ memoryRef.current = null;
3339
+ }
3340
+ };
3341
+ }, [dbPath, projectRoot, refreshInterval]);
3342
+ return { ...data, getMemory, forceRefresh };
3343
+ }
3344
+ async function refreshFast(memory, path2, projectRoot, mountedRef, setData) {
3345
+ try {
3346
+ const [stats, ids, activeEntries, readyEntries, silentEntries] = await Promise.all([
3347
+ memory.getOptimizationStats(),
3348
+ memory.list(),
3349
+ memory.query({ state: "active" }),
3350
+ memory.query({ state: "ready" }),
3351
+ memory.query({ state: "silent" })
3352
+ ]);
3353
+ let debts = [];
3354
+ let debtStats = null;
3355
+ try {
3356
+ const tracker = createDebtTracker(path2);
3357
+ debts = await tracker.list();
3358
+ debtStats = await tracker.stats();
3359
+ await tracker.close();
3360
+ } catch {
3361
+ }
3362
+ let daemon = null;
3363
+ try {
3364
+ const { createDaemonCommand: createDaemonCommand2 } = await Promise.resolve().then(() => (init_daemon_process(), daemon_process_exports));
3365
+ const { readFileSync: readFileSync6 } = await import("fs");
3366
+ const { resolve: resolve9 } = await import("path");
3367
+ const { load: parseYAML } = await import("js-yaml");
3368
+ const configPath = resolve9(projectRoot, ".sparn/config.yaml");
3369
+ const configContent = readFileSync6(configPath, "utf-8");
3370
+ const config = parseYAML(configContent);
3371
+ daemon = await createDaemonCommand2().status(config);
3372
+ } catch {
3373
+ }
3374
+ let dbSizeBytes = 0;
3375
+ try {
3376
+ dbSizeBytes = statSync3(path2).size;
3377
+ } catch {
3378
+ }
3379
+ if (mountedRef.current) {
3380
+ setData((prev) => ({
3381
+ ...prev,
3382
+ optimizationStats: stats,
3383
+ totalEntries: ids.length,
3384
+ stateDistribution: {
3385
+ active: activeEntries.length,
3386
+ ready: readyEntries.length,
3387
+ silent: silentEntries.length
3388
+ },
3389
+ debts,
3390
+ debtStats,
3391
+ daemon,
3392
+ dbSizeBytes,
3393
+ loading: false,
3394
+ error: null,
3395
+ lastRefresh: /* @__PURE__ */ new Date()
3396
+ }));
3397
+ }
3398
+ } catch (err) {
3399
+ if (mountedRef.current) {
3400
+ setData((prev) => ({
3401
+ ...prev,
3402
+ loading: false,
3403
+ error: err instanceof Error ? err.message : String(err)
3404
+ }));
3405
+ }
3406
+ }
3407
+ }
3408
+ async function refreshGraph(projectRoot, mountedRef, setData) {
3409
+ try {
3410
+ const graph = createDependencyGraph({ projectRoot });
3411
+ const analysis = await graph.analyze();
3412
+ const nodes = graph.getNodes();
3413
+ if (mountedRef.current) {
3414
+ setData((prev) => ({
3415
+ ...prev,
3416
+ graphAnalysis: analysis,
3417
+ graphNodes: nodes
3418
+ }));
3419
+ }
3420
+ } catch {
3421
+ }
3422
+ }
3423
+
3424
+ // src/cli/dashboard/panels/debt-tracker.tsx
3425
+ init_esm_shims();
3426
+ import { Box as Box3, Text as Text3 } from "ink";
3427
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
3428
+ function severityColor(severity) {
3429
+ switch (severity) {
3430
+ case "P0":
3431
+ return theme.errorRed;
3432
+ case "P1":
3433
+ return theme.synapseViolet;
3434
+ default:
3435
+ return theme.dimGray;
3436
+ }
3437
+ }
3438
+ function formatDate(ts) {
3439
+ const d = new Date(ts);
3440
+ return `${d.getMonth() + 1}/${d.getDate()}`;
3441
+ }
3442
+ function DebtTrackerPanel({
3443
+ debts,
3444
+ debtStats,
3445
+ focused,
3446
+ scrollOffset
3447
+ }) {
3448
+ const borderColor = focused ? theme.neuralCyan : theme.dimGray;
3449
+ const now = Date.now();
3450
+ const openDebts = debts.filter((d) => d.status !== "resolved");
3451
+ const visibleDebts = openDebts.slice(scrollOffset, scrollOffset + 8);
3452
+ return /* @__PURE__ */ jsxs3(
3453
+ Box3,
3454
+ {
3455
+ flexDirection: "column",
3456
+ borderStyle: "round",
3457
+ borderColor,
3458
+ paddingX: 1,
3459
+ width: "50%",
3460
+ flexGrow: 1,
3461
+ children: [
3462
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.errorRed, children: "Tech Debt" }),
3463
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
3464
+ visibleDebts.length === 0 && /* @__PURE__ */ jsx3(Text3, { color: theme.dimGray, children: "No open debts" }),
3465
+ visibleDebts.map((d) => {
3466
+ const isOverdue = d.repayment_date < now && d.status !== "resolved";
3467
+ const color = severityColor(d.severity);
3468
+ const desc = d.description.length > 30 ? `${d.description.substring(0, 27)}...` : d.description;
3469
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
3470
+ /* @__PURE__ */ jsxs3(Text3, { color, children: [
3471
+ "[",
3472
+ d.severity,
3473
+ "]"
3474
+ ] }),
3475
+ " ",
3476
+ desc,
3477
+ " ",
3478
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.dimGray, children: [
3479
+ "(due: ",
3480
+ formatDate(d.repayment_date),
3481
+ ")"
3482
+ ] }),
3483
+ isOverdue && /* @__PURE__ */ jsx3(Text3, { color: theme.errorRed, children: " OVERDUE" })
3484
+ ] }, d.id);
3485
+ }),
3486
+ openDebts.length > 8 && /* @__PURE__ */ jsxs3(Text3, { color: theme.dimGray, children: [
3487
+ "[",
3488
+ scrollOffset + 1,
3489
+ "-",
3490
+ Math.min(scrollOffset + 8, openDebts.length),
3491
+ "/",
3492
+ openDebts.length,
3493
+ "]",
3494
+ focused ? " \u2191\u2193 scroll" : ""
3495
+ ] })
3496
+ ] }),
3497
+ debtStats && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: theme.dimGray, children: [
3498
+ "Open:",
3499
+ debtStats.open,
3500
+ " InProg:",
3501
+ debtStats.in_progress,
3502
+ " Resolved:",
3503
+ debtStats.resolved,
3504
+ " ",
3505
+ "Overdue:",
3506
+ debtStats.overdue
3507
+ ] }) })
3508
+ ]
3509
+ }
3510
+ );
3511
+ }
3512
+
3513
+ // src/cli/dashboard/panels/graph-view.tsx
3514
+ init_esm_shims();
3515
+ import { Box as Box4, Text as Text4 } from "ink";
3516
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3517
+ function formatTokens(n) {
3518
+ if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
3519
+ return String(n);
3520
+ }
3521
+ function basename(filePath) {
3522
+ const parts = filePath.replace(/\\/g, "/").split("/");
3523
+ return parts[parts.length - 1] || filePath;
3524
+ }
3525
+ function buildTreeLines(nodes) {
3526
+ const lines = [];
3527
+ const sorted = [...nodes.values()].filter((n) => n.callers.length > 0).sort((a, b) => b.callers.length - a.callers.length).slice(0, 8);
3528
+ for (const node of sorted) {
3529
+ const name = basename(node.filePath);
3530
+ const tok = formatTokens(node.tokenEstimate);
3531
+ lines.push({
3532
+ text: `${name} (${node.callers.length} callers, ${tok}tok)`,
3533
+ color: theme.neuralCyan
3534
+ });
3535
+ const callerNames = node.callers.slice(0, 3).map(basename);
3536
+ const remaining = node.callers.length - callerNames.length;
3537
+ for (let i = 0; i < callerNames.length; i++) {
3538
+ const isLast = i === callerNames.length - 1 && remaining === 0;
3539
+ const prefix = isLast ? " \u2514\u2500\u2500 " : " \u251C\u2500\u2500 ";
3540
+ lines.push({
3541
+ text: `${prefix}${callerNames[i]}`,
3542
+ color: theme.dimGray
3543
+ });
3544
+ }
3545
+ if (remaining > 0) {
3546
+ lines.push({
3547
+ text: ` \u2514\u2500\u2500 ... ${remaining} more`,
3548
+ color: theme.dimGray
3549
+ });
3550
+ }
3551
+ }
3552
+ return lines;
3553
+ }
3554
+ function GraphViewPanel({
3555
+ analysis,
3556
+ nodes,
3557
+ focused,
3558
+ scrollOffset
3559
+ }) {
3560
+ const borderColor = focused ? theme.neuralCyan : theme.dimGray;
3561
+ if (!analysis) {
3562
+ return /* @__PURE__ */ jsxs4(
3563
+ Box4,
3564
+ {
3565
+ flexDirection: "column",
3566
+ borderStyle: "round",
3567
+ borderColor,
3568
+ paddingX: 1,
3569
+ width: "50%",
3570
+ flexGrow: 1,
3571
+ children: [
3572
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.synapseViolet, children: "Dependency Graph" }),
3573
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dimGray, children: "Loading graph..." })
3574
+ ]
3575
+ }
3576
+ );
3577
+ }
3578
+ const lines = buildTreeLines(nodes);
3579
+ const visibleLines = lines.slice(scrollOffset, scrollOffset + 12);
3580
+ return /* @__PURE__ */ jsxs4(
3581
+ Box4,
3582
+ {
3583
+ flexDirection: "column",
3584
+ borderStyle: "round",
3585
+ borderColor,
3586
+ paddingX: 1,
3587
+ width: "50%",
3588
+ flexGrow: 1,
3589
+ children: [
3590
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.synapseViolet, children: "Dependency Graph" }),
3591
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dimGray, children: [
3592
+ analysis.entryPoints.length,
3593
+ " entries | ",
3594
+ analysis.orphans.length,
3595
+ " orphans |",
3596
+ " ",
3597
+ formatTokens(analysis.totalTokens),
3598
+ "tok"
3599
+ ] }),
3600
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
3601
+ visibleLines.map((line) => /* @__PURE__ */ jsx4(Text4, { color: line.color, children: line.text }, line.text)),
3602
+ lines.length > 12 && /* @__PURE__ */ jsxs4(Text4, { color: theme.dimGray, children: [
3603
+ "[",
3604
+ scrollOffset + 1,
3605
+ "-",
3606
+ Math.min(scrollOffset + 12, lines.length),
3607
+ "/",
3608
+ lines.length,
3609
+ "]",
3610
+ focused ? " \u2191\u2193 scroll" : ""
3611
+ ] })
3612
+ ] })
3613
+ ]
3614
+ }
3615
+ );
3616
+ }
3617
+
3618
+ // src/cli/dashboard/panels/memory-status.tsx
3619
+ init_esm_shims();
3620
+ import { Box as Box5, Text as Text5 } from "ink";
3621
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
3622
+ function formatSize(bytes) {
3623
+ if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`;
3624
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
3625
+ return `${bytes} B`;
3626
+ }
3627
+ function formatTokens2(n) {
3628
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
3629
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
3630
+ return String(n);
3631
+ }
3632
+ function MemoryStatusPanel({
3633
+ totalEntries,
3634
+ dbSizeBytes,
3635
+ stateDistribution,
3636
+ daemon,
3637
+ focused
3638
+ }) {
3639
+ const borderColor = focused ? theme.neuralCyan : theme.dimGray;
3640
+ const daemonRunning = daemon?.running ?? false;
3641
+ const daemonPid = daemon?.pid;
3642
+ const sessions = daemon?.sessionsWatched;
3643
+ const tokensSaved = daemon?.tokensSaved;
3644
+ return /* @__PURE__ */ jsxs5(
3645
+ Box5,
3646
+ {
3647
+ flexDirection: "column",
3648
+ borderStyle: "round",
3649
+ borderColor,
3650
+ paddingX: 1,
3651
+ width: "50%",
3652
+ flexGrow: 1,
3653
+ children: [
3654
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: theme.brainPink, children: "Memory / Daemon" }),
3655
+ /* @__PURE__ */ jsxs5(Text5, { children: [
3656
+ "Entries: ",
3657
+ /* @__PURE__ */ jsx5(Text5, { color: theme.white, children: totalEntries }),
3658
+ " | Size:",
3659
+ " ",
3660
+ /* @__PURE__ */ jsx5(Text5, { color: theme.white, children: formatSize(dbSizeBytes) })
3661
+ ] }),
3662
+ /* @__PURE__ */ jsxs5(Text5, { children: [
3663
+ "Active: ",
3664
+ /* @__PURE__ */ jsx5(Text5, { color: theme.neuralCyan, children: stateDistribution.active }),
3665
+ " Ready:",
3666
+ " ",
3667
+ /* @__PURE__ */ jsx5(Text5, { color: theme.synapseViolet, children: stateDistribution.ready }),
3668
+ " Silent:",
3669
+ " ",
3670
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dimGray, children: stateDistribution.silent })
3671
+ ] }),
3672
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: daemonRunning ? /* @__PURE__ */ jsxs5(Text5, { children: [
3673
+ "Daemon: ",
3674
+ /* @__PURE__ */ jsxs5(Text5, { color: theme.neuralCyan, children: [
3675
+ "\u25CF",
3676
+ " Running"
3677
+ ] }),
3678
+ daemonPid != null ? ` (PID ${daemonPid})` : ""
3679
+ ] }) : /* @__PURE__ */ jsxs5(Text5, { children: [
3680
+ "Daemon: ",
3681
+ /* @__PURE__ */ jsxs5(Text5, { color: theme.errorRed, children: [
3682
+ "\u25CF",
3683
+ " Stopped"
3684
+ ] })
3685
+ ] }) }),
3686
+ daemonRunning && /* @__PURE__ */ jsxs5(Text5, { children: [
3687
+ "Sessions: ",
3688
+ /* @__PURE__ */ jsx5(Text5, { color: theme.white, children: sessions ?? 0 }),
3689
+ " | Saved:",
3690
+ " ",
3691
+ /* @__PURE__ */ jsx5(Text5, { color: theme.white, children: formatTokens2(tokensSaved ?? 0) })
3692
+ ] })
3693
+ ]
3694
+ }
3695
+ );
3696
+ }
3697
+
3698
+ // src/cli/dashboard/panels/optimization.tsx
3699
+ init_esm_shims();
3700
+ import { Box as Box6, Text as Text6 } from "ink";
3701
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
3702
+ function formatTime(ts) {
3703
+ const d = new Date(ts);
3704
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
3705
+ }
3706
+ function formatTokens3(n) {
3707
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
3708
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
3709
+ return String(n);
3710
+ }
3711
+ function makeBar(pct, width) {
3712
+ const filled = Math.round(pct / 100 * width);
3713
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
3714
+ }
3715
+ function OptimizationPanel({ stats, focused }) {
3716
+ const totalOpts = stats.length;
3717
+ const totalSaved = stats.reduce((s, r) => s + (r.tokens_before - r.tokens_after), 0);
3718
+ let avgReduction = 0;
3719
+ if (totalOpts > 0) {
3720
+ const totalBefore = stats.reduce((s, r) => s + r.tokens_before, 0);
3721
+ avgReduction = totalBefore > 0 ? (totalBefore - (totalBefore - totalSaved)) / totalBefore * 100 : 0;
3722
+ }
3723
+ const recent = stats.slice(-5).reverse();
3724
+ const borderColor = focused ? theme.neuralCyan : theme.dimGray;
3725
+ return /* @__PURE__ */ jsxs6(
3726
+ Box6,
3727
+ {
3728
+ flexDirection: "column",
3729
+ borderStyle: "round",
3730
+ borderColor,
3731
+ paddingX: 1,
3732
+ width: "50%",
3733
+ flexGrow: 1,
3734
+ children: [
3735
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: theme.neuralCyan, children: "Optimization Stats" }),
3736
+ /* @__PURE__ */ jsxs6(Text6, { children: [
3737
+ "Total: ",
3738
+ /* @__PURE__ */ jsx6(Text6, { color: theme.white, children: totalOpts }),
3739
+ " optimizations"
3740
+ ] }),
3741
+ /* @__PURE__ */ jsxs6(Text6, { children: [
3742
+ "Saved: ",
3743
+ /* @__PURE__ */ jsx6(Text6, { color: theme.white, children: formatTokens3(totalSaved) }),
3744
+ " tokens"
3745
+ ] }),
3746
+ /* @__PURE__ */ jsxs6(Text6, { children: [
3747
+ "Avg: ",
3748
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.white, children: [
3749
+ avgReduction.toFixed(1),
3750
+ "%"
3751
+ ] }),
3752
+ " ",
3753
+ /* @__PURE__ */ jsx6(Text6, { color: theme.neuralCyan, children: makeBar(avgReduction, 10) })
3754
+ ] }),
3755
+ recent.length > 0 && /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
3756
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dimGray, children: "Recent:" }),
3757
+ recent.map((r) => {
3758
+ const pct = r.tokens_before > 0 ? ((r.tokens_before - r.tokens_after) / r.tokens_before * 100).toFixed(0) : "0";
3759
+ return /* @__PURE__ */ jsxs6(Text6, { children: [
3760
+ " ",
3761
+ formatTime(r.timestamp),
3762
+ " | ",
3763
+ formatTokens3(r.tokens_before),
3764
+ "\u2192",
3765
+ formatTokens3(r.tokens_after),
3766
+ " | ",
3767
+ pct,
3768
+ "%"
3769
+ ] }, r.id);
3770
+ })
3771
+ ] })
3772
+ ]
3773
+ }
3774
+ );
3775
+ }
3776
+
3777
+ // src/cli/dashboard/app.tsx
3778
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
3779
+ var PANELS = ["optimization", "graph", "memory", "debt", "output"];
3780
+ function Dashboard({ dbPath, projectRoot, refreshInterval }) {
3781
+ const { exit } = useApp();
3782
+ const data = useDashboardData(dbPath, projectRoot, refreshInterval);
3783
+ const [mode, setMode] = useState4("monitor");
3784
+ const [focusIndex, setFocusIndex] = useState4(0);
3785
+ const [graphScroll, setGraphScroll] = useState4(0);
3786
+ const [debtScroll, setDebtScroll] = useState4(0);
3787
+ const [outputScroll, setOutputScroll] = useState4(0);
3788
+ const focusedPanel = PANELS[focusIndex] ?? "optimization";
3789
+ const executor = useCommandExecutor({
3790
+ getMemory: data.getMemory,
3791
+ forceRefresh: data.forceRefresh,
3792
+ dbPath,
3793
+ projectRoot
3794
+ });
3795
+ useInput(
3796
+ (input, key) => {
3797
+ if (mode === "command") {
3798
+ if (key.escape) {
3799
+ setMode("monitor");
3800
+ }
3801
+ return;
3802
+ }
3803
+ if (input === "q") {
3804
+ exit();
3805
+ return;
3806
+ }
3807
+ if (input === ":") {
3808
+ setMode("command");
3809
+ return;
3810
+ }
3811
+ if (key.tab) {
3812
+ setFocusIndex((prev) => (prev + 1) % PANELS.length);
3813
+ return;
3814
+ }
3815
+ if (key.upArrow) {
3816
+ if (focusedPanel === "graph") setGraphScroll((prev) => Math.max(0, prev - 1));
3817
+ if (focusedPanel === "debt") setDebtScroll((prev) => Math.max(0, prev - 1));
3818
+ if (focusedPanel === "output") setOutputScroll((prev) => Math.max(0, prev - 1));
3819
+ }
3820
+ if (key.downArrow) {
3821
+ if (focusedPanel === "graph") setGraphScroll((prev) => prev + 1);
3822
+ if (focusedPanel === "debt") setDebtScroll((prev) => prev + 1);
3823
+ if (focusedPanel === "output") setOutputScroll((prev) => prev + 1);
3824
+ }
3825
+ },
3826
+ { isActive: mode === "monitor" || mode === "command" }
3827
+ );
3828
+ const outputMaxVisible = 8;
3829
+ useEffect2(() => {
3830
+ setOutputScroll(Math.max(0, executor.outputLines.length - outputMaxVisible));
3831
+ }, [executor.outputLines.length]);
3832
+ if (data.loading) {
3833
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
3834
+ /* @__PURE__ */ jsx7(Text7, { color: theme.neuralCyan, children: "Sparn Dashboard" }),
3835
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dimGray, children: "Loading data..." })
3836
+ ] });
3837
+ }
3838
+ if (data.error) {
3839
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
3840
+ /* @__PURE__ */ jsx7(Text7, { color: theme.neuralCyan, children: "Sparn Dashboard" }),
3841
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.errorRed, children: [
3842
+ "Error: ",
3843
+ data.error
3844
+ ] }),
3845
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dimGray, children: "Press q to quit" })
3846
+ ] });
3847
+ }
3848
+ const timeStr = data.lastRefresh.toLocaleTimeString();
3849
+ const modeHint = mode === "command" ? "Esc:monitor Enter:run" : "q:quit Tab:focus \u2191\u2193:scroll ::cmd";
3850
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
3851
+ /* @__PURE__ */ jsxs7(Box7, { justifyContent: "space-between", paddingX: 1, children: [
3852
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: theme.neuralCyan, children: "Sparn Dashboard" }),
3853
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dimGray, children: [
3854
+ modeHint,
3855
+ " | ",
3856
+ timeStr
3857
+ ] })
3858
+ ] }),
3859
+ /* @__PURE__ */ jsxs7(Box7, { children: [
3860
+ /* @__PURE__ */ jsx7(
3861
+ OptimizationPanel,
3862
+ {
3863
+ stats: data.optimizationStats,
3864
+ focused: focusedPanel === "optimization"
3865
+ }
3866
+ ),
3867
+ /* @__PURE__ */ jsx7(
3868
+ GraphViewPanel,
3869
+ {
3870
+ analysis: data.graphAnalysis,
3871
+ nodes: data.graphNodes,
3872
+ focused: focusedPanel === "graph",
3873
+ scrollOffset: graphScroll
3874
+ }
3875
+ )
3876
+ ] }),
3877
+ /* @__PURE__ */ jsxs7(Box7, { children: [
3878
+ /* @__PURE__ */ jsx7(
3879
+ MemoryStatusPanel,
3880
+ {
3881
+ totalEntries: data.totalEntries,
3882
+ dbSizeBytes: data.dbSizeBytes,
3883
+ stateDistribution: data.stateDistribution,
3884
+ daemon: data.daemon,
3885
+ focused: focusedPanel === "memory"
3886
+ }
3887
+ ),
3888
+ /* @__PURE__ */ jsx7(
3889
+ DebtTrackerPanel,
3890
+ {
3891
+ debts: data.debts,
3892
+ debtStats: data.debtStats,
3893
+ focused: focusedPanel === "debt",
3894
+ scrollOffset: debtScroll
3895
+ }
3896
+ )
3897
+ ] }),
3898
+ /* @__PURE__ */ jsx7(
3899
+ OutputPanel,
3900
+ {
3901
+ lines: executor.outputLines,
3902
+ scrollOffset: outputScroll,
3903
+ focused: focusedPanel === "output"
3904
+ }
3905
+ ),
3906
+ /* @__PURE__ */ jsx7(
3907
+ CommandInput,
3908
+ {
3909
+ onSubmit: executor.execute,
3910
+ isRunning: executor.isRunning,
3911
+ runningCommand: executor.runningCommand,
3912
+ active: mode === "command"
3913
+ }
3914
+ )
3915
+ ] });
3916
+ }
3917
+ function renderDashboard(options) {
3918
+ render(
3919
+ /* @__PURE__ */ jsx7(
3920
+ Dashboard,
3921
+ {
3922
+ dbPath: options.dbPath,
3923
+ projectRoot: options.projectRoot,
3924
+ refreshInterval: options.refreshInterval ?? 2e3
3925
+ }
3926
+ )
3927
+ );
3928
+ }
3929
+ export {
3930
+ renderDashboard
3931
+ };
3932
+ //# sourceMappingURL=dashboard.js.map