@ulrichc1/sparn 1.2.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/PRIVACY.md +1 -1
  2. package/README.md +136 -642
  3. package/SECURITY.md +1 -1
  4. package/dist/cli/dashboard.cjs +3977 -0
  5. package/dist/cli/dashboard.cjs.map +1 -0
  6. package/dist/cli/dashboard.d.cts +17 -0
  7. package/dist/cli/dashboard.d.ts +17 -0
  8. package/dist/cli/dashboard.js +3932 -0
  9. package/dist/cli/dashboard.js.map +1 -0
  10. package/dist/cli/index.cjs +3853 -484
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +3810 -457
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/daemon/index.cjs +411 -99
  15. package/dist/daemon/index.cjs.map +1 -1
  16. package/dist/daemon/index.js +423 -103
  17. package/dist/daemon/index.js.map +1 -1
  18. package/dist/hooks/post-tool-result.cjs +115 -266
  19. package/dist/hooks/post-tool-result.cjs.map +1 -1
  20. package/dist/hooks/post-tool-result.js +115 -266
  21. package/dist/hooks/post-tool-result.js.map +1 -1
  22. package/dist/hooks/pre-prompt.cjs +197 -268
  23. package/dist/hooks/pre-prompt.cjs.map +1 -1
  24. package/dist/hooks/pre-prompt.js +182 -268
  25. package/dist/hooks/pre-prompt.js.map +1 -1
  26. package/dist/hooks/stop-docs-refresh.cjs +123 -0
  27. package/dist/hooks/stop-docs-refresh.cjs.map +1 -0
  28. package/dist/hooks/stop-docs-refresh.d.cts +1 -0
  29. package/dist/hooks/stop-docs-refresh.d.ts +1 -0
  30. package/dist/hooks/stop-docs-refresh.js +126 -0
  31. package/dist/hooks/stop-docs-refresh.js.map +1 -0
  32. package/dist/index.cjs +1754 -337
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +539 -40
  35. package/dist/index.d.ts +539 -40
  36. package/dist/index.js +1737 -329
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp/index.cjs +304 -71
  39. package/dist/mcp/index.cjs.map +1 -1
  40. package/dist/mcp/index.js +308 -71
  41. package/dist/mcp/index.js.map +1 -1
  42. package/package.json +10 -3
package/dist/cli/index.js CHANGED
@@ -18,6 +18,34 @@ var init_esm_shims = __esm({
18
18
  }
19
19
  });
20
20
 
21
+ // src/utils/tokenizer.ts
22
+ import { encode } from "gpt-tokenizer";
23
+ function setPreciseTokenCounting(enabled) {
24
+ usePrecise = enabled;
25
+ }
26
+ function estimateTokens(text) {
27
+ if (!text || text.length === 0) {
28
+ return 0;
29
+ }
30
+ if (usePrecise) {
31
+ return encode(text).length;
32
+ }
33
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
34
+ const wordCount = words.length;
35
+ const charCount = text.length;
36
+ const charEstimate = Math.ceil(charCount / 4);
37
+ const wordEstimate = Math.ceil(wordCount * 0.75);
38
+ return Math.max(wordEstimate, charEstimate);
39
+ }
40
+ var usePrecise;
41
+ var init_tokenizer = __esm({
42
+ "src/utils/tokenizer.ts"() {
43
+ "use strict";
44
+ init_esm_shims();
45
+ usePrecise = false;
46
+ }
47
+ });
48
+
21
49
  // src/cli/ui/colors.ts
22
50
  var colors_exports = {};
23
51
  __export(colors_exports, {
@@ -64,7 +92,7 @@ var init_banner = __esm({
64
92
  ___/ / ____/ __ _/ _, _/ /| /
65
93
  /____/_/ /_/ |_/_/ |_/_/ |_/
66
94
  `;
67
- TAGLINE = "\u{1F9E0} Neuroscience-inspired context optimization";
95
+ TAGLINE = "\u{1F9E0} Context optimization for AI coding agents";
68
96
  }
69
97
  });
70
98
 
@@ -73,7 +101,7 @@ var kv_memory_exports = {};
73
101
  __export(kv_memory_exports, {
74
102
  createKVMemory: () => createKVMemory
75
103
  });
76
- import { copyFileSync, existsSync } from "fs";
104
+ import { copyFileSync, existsSync as existsSync2 } from "fs";
77
105
  import Database from "better-sqlite3";
78
106
  function createBackup(dbPath) {
79
107
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
@@ -94,25 +122,26 @@ async function createKVMemory(dbPath) {
94
122
  const integrityCheck = db.pragma("quick_check", { simple: true });
95
123
  if (integrityCheck !== "ok") {
96
124
  console.error("\u26A0 Database corruption detected!");
97
- if (existsSync(dbPath)) {
125
+ db.close();
126
+ if (existsSync2(dbPath)) {
98
127
  const backupPath = createBackup(dbPath);
99
128
  if (backupPath) {
100
129
  console.log(`Backup created at: ${backupPath}`);
101
130
  }
102
131
  }
103
132
  console.log("Attempting database recovery...");
104
- db.close();
105
133
  db = new Database(dbPath);
106
134
  }
107
135
  } catch (error) {
108
136
  console.error("\u26A0 Database error detected:", error);
109
- if (existsSync(dbPath)) {
137
+ if (existsSync2(dbPath)) {
110
138
  createBackup(dbPath);
111
139
  console.log("Creating new database...");
112
140
  }
113
141
  db = new Database(dbPath);
114
142
  }
115
143
  db.pragma("journal_mode = WAL");
144
+ db.pragma("foreign_keys = ON");
116
145
  db.exec(`
117
146
  CREATE TABLE IF NOT EXISTS entries_index (
118
147
  id TEXT PRIMARY KEY NOT NULL,
@@ -152,6 +181,36 @@ async function createKVMemory(dbPath) {
152
181
  CREATE INDEX IF NOT EXISTS idx_entries_timestamp ON entries_index(timestamp DESC);
153
182
  CREATE INDEX IF NOT EXISTS idx_stats_timestamp ON optimization_stats(timestamp DESC);
154
183
  `);
184
+ db.exec(`
185
+ CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(id, content, tokenize='porter');
186
+ `);
187
+ db.exec(`
188
+ CREATE TRIGGER IF NOT EXISTS entries_fts_insert
189
+ AFTER INSERT ON entries_value
190
+ BEGIN
191
+ INSERT OR REPLACE INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
192
+ END;
193
+ `);
194
+ db.exec(`
195
+ CREATE TRIGGER IF NOT EXISTS entries_fts_delete
196
+ AFTER DELETE ON entries_value
197
+ BEGIN
198
+ DELETE FROM entries_fts WHERE id = OLD.id;
199
+ END;
200
+ `);
201
+ db.exec(`
202
+ CREATE TRIGGER IF NOT EXISTS entries_fts_update
203
+ AFTER UPDATE ON entries_value
204
+ BEGIN
205
+ DELETE FROM entries_fts WHERE id = OLD.id;
206
+ INSERT INTO entries_fts(id, content) VALUES (NEW.id, NEW.content);
207
+ END;
208
+ `);
209
+ db.exec(`
210
+ INSERT OR IGNORE INTO entries_fts(id, content)
211
+ SELECT id, content FROM entries_value
212
+ WHERE id NOT IN (SELECT id FROM entries_fts);
213
+ `);
155
214
  const putIndexStmt = db.prepare(`
156
215
  INSERT OR REPLACE INTO entries_index
157
216
  (id, hash, timestamp, score, ttl, state, accessCount, isBTSP)
@@ -240,14 +299,20 @@ async function createKVMemory(dbPath) {
240
299
  sql += " AND i.isBTSP = ?";
241
300
  params.push(filters.isBTSP ? 1 : 0);
242
301
  }
302
+ if (filters.tags && filters.tags.length > 0) {
303
+ for (const tag of filters.tags) {
304
+ sql += " AND v.tags LIKE ?";
305
+ params.push(`%"${tag}"%`);
306
+ }
307
+ }
243
308
  sql += " ORDER BY i.score DESC";
244
309
  if (filters.limit) {
245
310
  sql += " LIMIT ?";
246
311
  params.push(filters.limit);
247
- }
248
- if (filters.offset) {
249
- sql += " OFFSET ?";
250
- params.push(filters.offset);
312
+ if (filters.offset) {
313
+ sql += " OFFSET ?";
314
+ params.push(filters.offset);
315
+ }
251
316
  }
252
317
  const stmt = db.prepare(sql);
253
318
  const rows = stmt.all(...params);
@@ -282,7 +347,22 @@ async function createKVMemory(dbPath) {
282
347
  },
283
348
  async compact() {
284
349
  const before = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
285
- db.exec("DELETE FROM entries_index WHERE ttl <= 0");
350
+ const now = Date.now();
351
+ db.prepare("DELETE FROM entries_index WHERE isBTSP = 0 AND (timestamp + ttl * 1000) < ?").run(
352
+ now
353
+ );
354
+ db.exec("DELETE FROM entries_index WHERE isBTSP = 0 AND ttl <= 0");
355
+ const candidates = db.prepare("SELECT id, timestamp, ttl FROM entries_index WHERE isBTSP = 0").all();
356
+ for (const row of candidates) {
357
+ const ageSeconds = Math.max(0, (now - row.timestamp) / 1e3);
358
+ const ttlSeconds = row.ttl;
359
+ if (ttlSeconds <= 0) continue;
360
+ const decay = 1 - Math.exp(-ageSeconds / ttlSeconds);
361
+ if (decay >= 0.95) {
362
+ db.prepare("DELETE FROM entries_index WHERE id = ?").run(row.id);
363
+ }
364
+ }
365
+ db.exec("DELETE FROM entries_value WHERE id NOT IN (SELECT id FROM entries_index)");
286
366
  db.exec("VACUUM");
287
367
  const after = db.prepare("SELECT COUNT(*) as count FROM entries_index").get();
288
368
  return before.count - after.count;
@@ -302,6 +382,9 @@ async function createKVMemory(dbPath) {
302
382
  stats.entries_pruned,
303
383
  stats.duration_ms
304
384
  );
385
+ db.prepare(
386
+ "DELETE FROM optimization_stats WHERE id NOT IN (SELECT id FROM optimization_stats ORDER BY timestamp DESC LIMIT 1000)"
387
+ ).run();
305
388
  },
306
389
  async getOptimizationStats() {
307
390
  const stmt = db.prepare(`
@@ -314,6 +397,44 @@ async function createKVMemory(dbPath) {
314
397
  },
315
398
  async clearOptimizationStats() {
316
399
  db.exec("DELETE FROM optimization_stats");
400
+ },
401
+ async searchFTS(query, limit = 10) {
402
+ if (!query || query.trim().length === 0) return [];
403
+ const sanitized = query.replace(/[{}()[\]"':*^~]/g, " ").trim();
404
+ if (sanitized.length === 0) return [];
405
+ const stmt = db.prepare(`
406
+ SELECT
407
+ f.id, f.content, rank,
408
+ i.hash, i.timestamp, i.score, i.ttl, i.state, i.accessCount, i.isBTSP,
409
+ v.tags, v.metadata
410
+ FROM entries_fts f
411
+ JOIN entries_index i ON f.id = i.id
412
+ JOIN entries_value v ON f.id = v.id
413
+ WHERE entries_fts MATCH ?
414
+ ORDER BY rank
415
+ LIMIT ?
416
+ `);
417
+ try {
418
+ const rows = stmt.all(sanitized, limit);
419
+ return rows.map((r) => ({
420
+ entry: {
421
+ id: r.id,
422
+ content: r.content,
423
+ hash: r.hash,
424
+ timestamp: r.timestamp,
425
+ score: r.score,
426
+ ttl: r.ttl,
427
+ state: r.state,
428
+ accessCount: r.accessCount,
429
+ tags: r.tags ? JSON.parse(r.tags) : [],
430
+ metadata: r.metadata ? JSON.parse(r.metadata) : {},
431
+ isBTSP: r.isBTSP === 1
432
+ },
433
+ rank: r.rank
434
+ }));
435
+ } catch {
436
+ return [];
437
+ }
317
438
  }
318
439
  };
319
440
  }
@@ -373,26 +494,25 @@ __export(init_exports, {
373
494
  });
374
495
  import { readFileSync } from "fs";
375
496
  import { access, mkdir, writeFile } from "fs/promises";
376
- import { dirname, join } from "path";
377
- import { fileURLToPath as fileURLToPath2 } from "url";
497
+ import { dirname as dirname2, join as join2 } from "path";
498
+ import { fileURLToPath as fileURLToPath3 } from "url";
378
499
  import { dump as dumpYAML } from "js-yaml";
379
500
  function getVersion() {
380
501
  try {
381
- const pkg = JSON.parse(readFileSync(join(process.cwd(), "package.json"), "utf-8"));
502
+ const __filename2 = fileURLToPath3(import.meta.url);
503
+ const __dirname2 = dirname2(__filename2);
504
+ const pkg = JSON.parse(readFileSync(join2(__dirname2, "../../package.json"), "utf-8"));
382
505
  return pkg.version;
383
506
  } catch {
384
- const __filename2 = fileURLToPath2(import.meta.url);
385
- const __dirname2 = dirname(__filename2);
386
- const pkg = JSON.parse(readFileSync(join(__dirname2, "../../package.json"), "utf-8"));
387
- return pkg.version;
507
+ return "1.4.0";
388
508
  }
389
509
  }
390
510
  async function initCommand(options = {}) {
391
511
  const startTime = Date.now();
392
512
  const cwd = options.cwd || process.cwd();
393
- const sparnDir = join(cwd, ".sparn");
394
- const configPath = join(sparnDir, "config.yaml");
395
- const dbPath = join(sparnDir, "memory.db");
513
+ const sparnDir = join2(cwd, ".sparn");
514
+ const configPath = join2(sparnDir, "config.yaml");
515
+ const dbPath = join2(sparnDir, "memory.db");
396
516
  const exists = await checkExists(sparnDir);
397
517
  if (exists && !options.force) {
398
518
  throw new Error(
@@ -588,7 +708,7 @@ var init_hash = __esm({
588
708
 
589
709
  // src/core/btsp-embedder.ts
590
710
  import { randomUUID } from "crypto";
591
- function createBTSPEmbedder() {
711
+ function createBTSPEmbedder(config) {
592
712
  const BTSP_PATTERNS = [
593
713
  // Error patterns
594
714
  /\b(error|exception|failure|fatal|critical|panic)\b/i,
@@ -607,6 +727,14 @@ function createBTSPEmbedder() {
607
727
  /^=======/m,
608
728
  /^>>>>>>> /m
609
729
  ];
730
+ if (config?.customPatterns) {
731
+ for (const pattern of config.customPatterns) {
732
+ try {
733
+ BTSP_PATTERNS.push(new RegExp(pattern));
734
+ } catch {
735
+ }
736
+ }
737
+ }
610
738
  function detectBTSP(content) {
611
739
  return BTSP_PATTERNS.some((pattern) => pattern.test(content));
612
740
  }
@@ -648,7 +776,7 @@ function createConfidenceStates(config) {
648
776
  if (entry.isBTSP) {
649
777
  return "active";
650
778
  }
651
- if (entry.score > activeThreshold) {
779
+ if (entry.score >= activeThreshold) {
652
780
  return "active";
653
781
  }
654
782
  if (entry.score >= readyThreshold) {
@@ -692,6 +820,8 @@ var init_confidence_states = __esm({
692
820
  // src/core/engram-scorer.ts
693
821
  function createEngramScorer(config) {
694
822
  const { defaultTTL } = config;
823
+ const recencyWindowMs = (config.recencyBoostMinutes ?? 30) * 60 * 1e3;
824
+ const recencyMultiplier = config.recencyBoostMultiplier ?? 1.3;
695
825
  function calculateDecay(ageInSeconds, ttlInSeconds) {
696
826
  if (ttlInSeconds === 0) return 1;
697
827
  if (ageInSeconds <= 0) return 0;
@@ -711,6 +841,13 @@ function createEngramScorer(config) {
711
841
  if (entry.isBTSP) {
712
842
  score = Math.max(score, 0.9);
713
843
  }
844
+ if (!entry.isBTSP && recencyWindowMs > 0) {
845
+ const ageMs = currentTime - entry.timestamp;
846
+ if (ageMs >= 0 && ageMs < recencyWindowMs) {
847
+ const boostFactor = 1 + (recencyMultiplier - 1) * (1 - ageMs / recencyWindowMs);
848
+ score = score * boostFactor;
849
+ }
850
+ }
714
851
  return Math.max(0, Math.min(1, score));
715
852
  }
716
853
  function refreshTTL(entry) {
@@ -734,20 +871,44 @@ var init_engram_scorer = __esm({
734
871
  }
735
872
  });
736
873
 
737
- // src/utils/tokenizer.ts
738
- function estimateTokens(text) {
739
- if (!text || text.length === 0) {
740
- return 0;
874
+ // src/utils/tfidf.ts
875
+ function tokenize(text) {
876
+ return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
877
+ }
878
+ function calculateTF(term, tokens) {
879
+ const count = tokens.filter((t) => t === term).length;
880
+ return Math.sqrt(count);
881
+ }
882
+ function createTFIDFIndex(entries) {
883
+ const documentFrequency = /* @__PURE__ */ new Map();
884
+ for (const entry of entries) {
885
+ const tokens = tokenize(entry.content);
886
+ const uniqueTerms = new Set(tokens);
887
+ for (const term of uniqueTerms) {
888
+ documentFrequency.set(term, (documentFrequency.get(term) || 0) + 1);
889
+ }
741
890
  }
742
- const words = text.split(/\s+/).filter((w) => w.length > 0);
743
- const wordCount = words.length;
744
- const charCount = text.length;
745
- const charEstimate = Math.ceil(charCount / 4);
746
- const wordEstimate = Math.ceil(wordCount * 0.75);
747
- return Math.max(wordEstimate, charEstimate);
891
+ return {
892
+ documentFrequency,
893
+ totalDocuments: entries.length
894
+ };
748
895
  }
749
- var init_tokenizer = __esm({
750
- "src/utils/tokenizer.ts"() {
896
+ function scoreTFIDF(entry, index) {
897
+ const tokens = tokenize(entry.content);
898
+ if (tokens.length === 0) return 0;
899
+ const uniqueTerms = new Set(tokens);
900
+ let totalScore = 0;
901
+ for (const term of uniqueTerms) {
902
+ const tf = calculateTF(term, tokens);
903
+ const docsWithTerm = index.documentFrequency.get(term) || 0;
904
+ if (docsWithTerm === 0) continue;
905
+ const idf = Math.log(index.totalDocuments / docsWithTerm);
906
+ totalScore += tf * idf;
907
+ }
908
+ return totalScore / tokens.length;
909
+ }
910
+ var init_tfidf = __esm({
911
+ "src/utils/tfidf.ts"() {
751
912
  "use strict";
752
913
  init_esm_shims();
753
914
  }
@@ -756,33 +917,8 @@ var init_tokenizer = __esm({
756
917
  // src/core/sparse-pruner.ts
757
918
  function createSparsePruner(config) {
758
919
  const { threshold } = config;
759
- function tokenize(text) {
760
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
761
- }
762
- function calculateTF(term, tokens) {
763
- const count = tokens.filter((t) => t === term).length;
764
- return Math.sqrt(count);
765
- }
766
- function calculateIDF(term, allEntries) {
767
- const totalDocs = allEntries.length;
768
- const docsWithTerm = allEntries.filter((entry) => {
769
- const tokens = tokenize(entry.content);
770
- return tokens.includes(term);
771
- }).length;
772
- if (docsWithTerm === 0) return 0;
773
- return Math.log(totalDocs / docsWithTerm);
774
- }
775
920
  function scoreEntry(entry, allEntries) {
776
- const tokens = tokenize(entry.content);
777
- if (tokens.length === 0) return 0;
778
- const uniqueTerms = [...new Set(tokens)];
779
- let totalScore = 0;
780
- for (const term of uniqueTerms) {
781
- const tf = calculateTF(term, tokens);
782
- const idf = calculateIDF(term, allEntries);
783
- totalScore += tf * idf;
784
- }
785
- return totalScore / tokens.length;
921
+ return scoreTFIDF(entry, createTFIDFIndex(allEntries));
786
922
  }
787
923
  function prune(entries) {
788
924
  if (entries.length === 0) {
@@ -794,9 +930,10 @@ function createSparsePruner(config) {
794
930
  };
795
931
  }
796
932
  const originalTokens = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
933
+ const tfidfIndex = createTFIDFIndex(entries);
797
934
  const scored = entries.map((entry) => ({
798
935
  entry,
799
- score: scoreEntry(entry, entries)
936
+ score: scoreTFIDF(entry, tfidfIndex)
800
937
  }));
801
938
  scored.sort((a, b) => b.score - a.score);
802
939
  const keepCount = Math.max(1, Math.ceil(entries.length * (threshold / 100)));
@@ -819,6 +956,7 @@ var init_sparse_pruner = __esm({
819
956
  "src/core/sparse-pruner.ts"() {
820
957
  "use strict";
821
958
  init_esm_shims();
959
+ init_tfidf();
822
960
  init_tokenizer();
823
961
  }
824
962
  });
@@ -829,25 +967,28 @@ function createGenericAdapter(memory, config) {
829
967
  const pruner = createSparsePruner(config.pruning);
830
968
  const scorer = createEngramScorer(config.decay);
831
969
  const states = createConfidenceStates(config.states);
832
- const btsp = createBTSPEmbedder();
970
+ const btsp = createBTSPEmbedder({ customPatterns: config.btspPatterns });
833
971
  async function optimize(context, options = {}) {
834
972
  const startTime = Date.now();
835
973
  const lines = context.split("\n").filter((line) => line.trim().length > 0);
836
- const entries = lines.map((content) => ({
837
- id: randomUUID2(),
838
- content,
839
- hash: hashContent(content),
840
- timestamp: Date.now(),
841
- score: btsp.detectBTSP(content) ? 1 : 0.5,
842
- // BTSP gets high initial score
843
- ttl: config.decay.defaultTTL * 3600,
844
- // Convert hours to seconds
845
- state: "ready",
846
- accessCount: 0,
847
- tags: [],
848
- metadata: {},
849
- isBTSP: btsp.detectBTSP(content)
850
- }));
974
+ const now = Date.now();
975
+ const entries = lines.map((content, index) => {
976
+ const isBTSP = btsp.detectBTSP(content);
977
+ return {
978
+ id: randomUUID2(),
979
+ content,
980
+ hash: hashContent(content),
981
+ timestamp: now + index,
982
+ // Unique timestamps preserve ordering
983
+ score: isBTSP ? 1 : 0.5,
984
+ ttl: config.decay.defaultTTL * 3600,
985
+ state: "ready",
986
+ accessCount: 0,
987
+ tags: [],
988
+ metadata: {},
989
+ isBTSP
990
+ };
991
+ });
851
992
  const tokensBefore = entries.reduce((sum, e) => sum + estimateTokens(e.content), 0);
852
993
  const scoredEntries = entries.map((entry) => ({
853
994
  ...entry,
@@ -1059,7 +1200,7 @@ async function relayCommand(options) {
1059
1200
  return result;
1060
1201
  }
1061
1202
  function executeCommand(command, args) {
1062
- return new Promise((resolve3) => {
1203
+ return new Promise((resolve12) => {
1063
1204
  const child = spawn(command, args, {
1064
1205
  stdio: ["ignore", "pipe", "pipe"]
1065
1206
  });
@@ -1072,14 +1213,14 @@ function executeCommand(command, args) {
1072
1213
  stderr += data.toString();
1073
1214
  });
1074
1215
  child.on("close", (code) => {
1075
- resolve3({
1216
+ resolve12({
1076
1217
  stdout,
1077
1218
  stderr,
1078
1219
  exitCode: code ?? 0
1079
1220
  });
1080
1221
  });
1081
1222
  child.on("error", (error) => {
1082
- resolve3({
1223
+ resolve12({
1083
1224
  stdout,
1084
1225
  stderr: error.message,
1085
1226
  exitCode: 1
@@ -1187,13 +1328,15 @@ function createSleepCompressor() {
1187
1328
  function cosineSimilarity(text1, text2) {
1188
1329
  const words1 = tokenize(text1);
1189
1330
  const words2 = tokenize(text2);
1190
- const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
1191
1331
  const vec1 = {};
1192
1332
  const vec2 = {};
1193
- for (const word of vocab) {
1194
- vec1[word] = words1.filter((w) => w === word).length;
1195
- vec2[word] = words2.filter((w) => w === word).length;
1333
+ for (const word of words1) {
1334
+ vec1[word] = (vec1[word] ?? 0) + 1;
1335
+ }
1336
+ for (const word of words2) {
1337
+ vec2[word] = (vec2[word] ?? 0) + 1;
1196
1338
  }
1339
+ const vocab = /* @__PURE__ */ new Set([...words1, ...words2]);
1197
1340
  let dotProduct = 0;
1198
1341
  let mag1 = 0;
1199
1342
  let mag2 = 0;
@@ -1209,9 +1352,6 @@ function createSleepCompressor() {
1209
1352
  if (mag1 === 0 || mag2 === 0) return 0;
1210
1353
  return dotProduct / (mag1 * mag2);
1211
1354
  }
1212
- function tokenize(text) {
1213
- return text.toLowerCase().split(/\s+/).filter((word) => word.length > 0);
1214
- }
1215
1355
  return {
1216
1356
  consolidate,
1217
1357
  findDuplicates,
@@ -1222,6 +1362,7 @@ var init_sleep_compressor = __esm({
1222
1362
  "src/core/sleep-compressor.ts"() {
1223
1363
  "use strict";
1224
1364
  init_esm_shims();
1365
+ init_tfidf();
1225
1366
  init_engram_scorer();
1226
1367
  }
1227
1368
  });
@@ -1520,132 +1661,18 @@ var init_config2 = __esm({
1520
1661
  }
1521
1662
  });
1522
1663
 
1523
- // src/core/metrics.ts
1524
- function createMetricsCollector() {
1525
- const optimizations = [];
1526
- let daemonMetrics = {
1527
- startTime: Date.now(),
1528
- sessionsWatched: 0,
1529
- totalOptimizations: 0,
1530
- totalTokensSaved: 0,
1531
- averageLatency: 0,
1532
- memoryUsage: 0
1533
- };
1534
- let cacheHits = 0;
1535
- let cacheMisses = 0;
1536
- function recordOptimization(metric) {
1537
- optimizations.push(metric);
1538
- daemonMetrics.totalOptimizations++;
1539
- daemonMetrics.totalTokensSaved += metric.tokensBefore - metric.tokensAfter;
1540
- if (metric.cacheHitRate > 0) {
1541
- const hits = Math.round(metric.entriesProcessed * metric.cacheHitRate);
1542
- cacheHits += hits;
1543
- cacheMisses += metric.entriesProcessed - hits;
1544
- }
1545
- daemonMetrics.averageLatency = (daemonMetrics.averageLatency * (daemonMetrics.totalOptimizations - 1) + metric.duration) / daemonMetrics.totalOptimizations;
1546
- if (optimizations.length > 1e3) {
1547
- optimizations.shift();
1548
- }
1549
- }
1550
- function updateDaemon(metric) {
1551
- daemonMetrics = {
1552
- ...daemonMetrics,
1553
- ...metric
1554
- };
1555
- }
1556
- function calculatePercentile(values, percentile) {
1557
- if (values.length === 0) return 0;
1558
- const sorted = [...values].sort((a, b) => a - b);
1559
- const index = Math.ceil(percentile / 100 * sorted.length) - 1;
1560
- return sorted[index] || 0;
1561
- }
1562
- function getSnapshot() {
1563
- const totalRuns = optimizations.length;
1564
- const totalDuration = optimizations.reduce((sum, m) => sum + m.duration, 0);
1565
- const totalTokensSaved = optimizations.reduce(
1566
- (sum, m) => sum + (m.tokensBefore - m.tokensAfter),
1567
- 0
1568
- );
1569
- const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
1570
- const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
1571
- const durations = optimizations.map((m) => m.duration);
1572
- const totalCacheQueries = cacheHits + cacheMisses;
1573
- const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
1574
- return {
1575
- timestamp: Date.now(),
1576
- optimization: {
1577
- totalRuns,
1578
- totalDuration,
1579
- totalTokensSaved,
1580
- averageReduction,
1581
- p50Latency: calculatePercentile(durations, 50),
1582
- p95Latency: calculatePercentile(durations, 95),
1583
- p99Latency: calculatePercentile(durations, 99)
1584
- },
1585
- cache: {
1586
- hitRate,
1587
- totalHits: cacheHits,
1588
- totalMisses: cacheMisses,
1589
- size: optimizations.reduce((sum, m) => sum + m.entriesKept, 0)
1590
- },
1591
- daemon: {
1592
- uptime: Date.now() - daemonMetrics.startTime,
1593
- sessionsWatched: daemonMetrics.sessionsWatched,
1594
- memoryUsage: daemonMetrics.memoryUsage
1595
- }
1596
- };
1597
- }
1598
- function exportMetrics() {
1599
- return JSON.stringify(getSnapshot(), null, 2);
1600
- }
1601
- function reset() {
1602
- optimizations.length = 0;
1603
- cacheHits = 0;
1604
- cacheMisses = 0;
1605
- daemonMetrics = {
1606
- startTime: Date.now(),
1607
- sessionsWatched: 0,
1608
- totalOptimizations: 0,
1609
- totalTokensSaved: 0,
1610
- averageLatency: 0,
1611
- memoryUsage: 0
1612
- };
1613
- }
1614
- return {
1615
- recordOptimization,
1616
- updateDaemon,
1617
- getSnapshot,
1618
- export: exportMetrics,
1619
- reset
1620
- };
1621
- }
1622
- function getMetrics() {
1623
- if (!globalMetrics) {
1624
- globalMetrics = createMetricsCollector();
1625
- }
1626
- return globalMetrics;
1627
- }
1628
- var globalMetrics;
1629
- var init_metrics = __esm({
1630
- "src/core/metrics.ts"() {
1631
- "use strict";
1632
- init_esm_shims();
1633
- globalMetrics = null;
1634
- }
1635
- });
1636
-
1637
1664
  // src/daemon/daemon-process.ts
1638
1665
  var daemon_process_exports = {};
1639
1666
  __export(daemon_process_exports, {
1640
1667
  createDaemonCommand: () => createDaemonCommand
1641
1668
  });
1642
- import { fork } from "child_process";
1643
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
1644
- import { dirname as dirname2, join as join2 } from "path";
1645
- import { fileURLToPath as fileURLToPath3 } from "url";
1669
+ import { spawn as spawn2 } from "child_process";
1670
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
1671
+ import { dirname as dirname3, join as join3 } from "path";
1672
+ import { fileURLToPath as fileURLToPath4 } from "url";
1646
1673
  function createDaemonCommand() {
1647
1674
  function isDaemonRunning(pidFile) {
1648
- if (!existsSync2(pidFile)) {
1675
+ if (!existsSync3(pidFile)) {
1649
1676
  return { running: false };
1650
1677
  }
1651
1678
  try {
@@ -1666,14 +1693,14 @@ function createDaemonCommand() {
1666
1693
  }
1667
1694
  }
1668
1695
  function writePidFile(pidFile, pid) {
1669
- const dir = dirname2(pidFile);
1670
- if (!existsSync2(dir)) {
1696
+ const dir = dirname3(pidFile);
1697
+ if (!existsSync3(dir)) {
1671
1698
  mkdirSync(dir, { recursive: true });
1672
1699
  }
1673
1700
  writeFileSync2(pidFile, String(pid), "utf-8");
1674
1701
  }
1675
1702
  function removePidFile(pidFile) {
1676
- if (existsSync2(pidFile)) {
1703
+ if (existsSync3(pidFile)) {
1677
1704
  unlinkSync(pidFile);
1678
1705
  }
1679
1706
  }
@@ -1689,18 +1716,62 @@ function createDaemonCommand() {
1689
1716
  };
1690
1717
  }
1691
1718
  try {
1692
- const __filename2 = fileURLToPath3(import.meta.url);
1693
- const __dirname2 = dirname2(__filename2);
1694
- const daemonPath = join2(__dirname2, "index.js");
1695
- const child = fork(daemonPath, [], {
1719
+ const __filename2 = fileURLToPath4(import.meta.url);
1720
+ const __dirname2 = dirname3(__filename2);
1721
+ const daemonPath = join3(__dirname2, "..", "daemon", "index.js");
1722
+ const isWindows = process.platform === "win32";
1723
+ const childEnv = {
1724
+ ...process.env,
1725
+ SPARN_CONFIG: JSON.stringify(config),
1726
+ SPARN_PID_FILE: pidFile,
1727
+ SPARN_LOG_FILE: logFile
1728
+ };
1729
+ if (isWindows) {
1730
+ const configFile = join3(dirname3(pidFile), "daemon-config.json");
1731
+ writeFileSync2(configFile, JSON.stringify({ config, pidFile, logFile }), "utf-8");
1732
+ const launcherFile = join3(dirname3(pidFile), "daemon-launcher.mjs");
1733
+ const launcherCode = [
1734
+ `import { readFileSync } from 'node:fs';`,
1735
+ `const cfg = JSON.parse(readFileSync(${JSON.stringify(configFile)}, 'utf-8'));`,
1736
+ `process.env.SPARN_CONFIG = JSON.stringify(cfg.config);`,
1737
+ `process.env.SPARN_PID_FILE = cfg.pidFile;`,
1738
+ `process.env.SPARN_LOG_FILE = cfg.logFile;`,
1739
+ `await import(${JSON.stringify(`file:///${daemonPath.replace(/\\/g, "/")}`)});`
1740
+ ].join("\n");
1741
+ writeFileSync2(launcherFile, launcherCode, "utf-8");
1742
+ const ps = spawn2(
1743
+ "powershell.exe",
1744
+ [
1745
+ "-NoProfile",
1746
+ "-WindowStyle",
1747
+ "Hidden",
1748
+ "-Command",
1749
+ `Start-Process -FilePath '${process.execPath}' -ArgumentList '${launcherFile}' -WindowStyle Hidden`
1750
+ ],
1751
+ { stdio: "ignore", windowsHide: true }
1752
+ );
1753
+ ps.unref();
1754
+ await new Promise((resolve12) => setTimeout(resolve12, 2e3));
1755
+ if (existsSync3(pidFile)) {
1756
+ const pid = Number.parseInt(readFileSync3(pidFile, "utf-8").trim(), 10);
1757
+ if (!Number.isNaN(pid)) {
1758
+ return {
1759
+ success: true,
1760
+ pid,
1761
+ message: `Daemon started (PID ${pid})`
1762
+ };
1763
+ }
1764
+ }
1765
+ return {
1766
+ success: false,
1767
+ message: "Daemon failed to start (no PID file written)",
1768
+ error: "Timeout waiting for daemon PID"
1769
+ };
1770
+ }
1771
+ const child = spawn2(process.execPath, [daemonPath], {
1696
1772
  detached: true,
1697
1773
  stdio: "ignore",
1698
- env: {
1699
- ...process.env,
1700
- SPARN_CONFIG: JSON.stringify(config),
1701
- SPARN_PID_FILE: pidFile,
1702
- SPARN_LOG_FILE: logFile
1703
- }
1774
+ env: childEnv
1704
1775
  });
1705
1776
  child.unref();
1706
1777
  if (child.pid) {
@@ -1734,14 +1805,19 @@ function createDaemonCommand() {
1734
1805
  };
1735
1806
  }
1736
1807
  try {
1737
- process.kill(status2.pid, "SIGTERM");
1808
+ const isWindows = process.platform === "win32";
1809
+ if (isWindows) {
1810
+ process.kill(status2.pid);
1811
+ } else {
1812
+ process.kill(status2.pid, "SIGTERM");
1813
+ }
1738
1814
  const maxWait = 5e3;
1739
1815
  const interval = 100;
1740
1816
  let waited = 0;
1741
1817
  while (waited < maxWait) {
1742
1818
  try {
1743
1819
  process.kill(status2.pid, 0);
1744
- await new Promise((resolve3) => setTimeout(resolve3, interval));
1820
+ await new Promise((resolve12) => setTimeout(resolve12, interval));
1745
1821
  waited += interval;
1746
1822
  } catch {
1747
1823
  removePidFile(pidFile);
@@ -1752,7 +1828,9 @@ function createDaemonCommand() {
1752
1828
  }
1753
1829
  }
1754
1830
  try {
1755
- process.kill(status2.pid, "SIGKILL");
1831
+ if (!isWindows) {
1832
+ process.kill(status2.pid, "SIGKILL");
1833
+ }
1756
1834
  removePidFile(pidFile);
1757
1835
  return {
1758
1836
  success: true,
@@ -1782,13 +1860,9 @@ function createDaemonCommand() {
1782
1860
  message: "Daemon not running"
1783
1861
  };
1784
1862
  }
1785
- const metrics = getMetrics().getSnapshot();
1786
1863
  return {
1787
1864
  running: true,
1788
1865
  pid: daemonStatus.pid,
1789
- uptime: metrics.daemon.uptime,
1790
- sessionsWatched: metrics.daemon.sessionsWatched,
1791
- tokensSaved: metrics.optimization.totalTokensSaved,
1792
1866
  message: `Daemon running (PID ${daemonStatus.pid})`
1793
1867
  };
1794
1868
  }
@@ -1802,7 +1876,6 @@ var init_daemon_process = __esm({
1802
1876
  "src/daemon/daemon-process.ts"() {
1803
1877
  "use strict";
1804
1878
  init_esm_shims();
1805
- init_metrics();
1806
1879
  }
1807
1880
  });
1808
1881
 
@@ -1811,25 +1884,32 @@ var hooks_exports = {};
1811
1884
  __export(hooks_exports, {
1812
1885
  hooksCommand: () => hooksCommand
1813
1886
  });
1814
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1887
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1815
1888
  import { homedir } from "os";
1816
- import { dirname as dirname3, join as join3 } from "path";
1817
- import { fileURLToPath as fileURLToPath4 } from "url";
1889
+ import { dirname as dirname4, join as join4 } from "path";
1890
+ import { fileURLToPath as fileURLToPath5 } from "url";
1818
1891
  async function hooksCommand(options) {
1819
1892
  const { subcommand, global } = options;
1820
- const settingsPath = global ? join3(homedir(), ".claude", "settings.json") : join3(process.cwd(), ".claude", "settings.json");
1821
- const __filename2 = fileURLToPath4(import.meta.url);
1822
- const __dirname2 = dirname3(__filename2);
1823
- const hooksDir = join3(dirname3(__dirname2), "hooks");
1824
- const prePromptPath = join3(hooksDir, "pre-prompt.js");
1825
- const postToolResultPath = join3(hooksDir, "post-tool-result.js");
1893
+ const settingsPath = global ? join4(homedir(), ".claude", "settings.json") : join4(process.cwd(), ".claude", "settings.json");
1894
+ const __filename2 = fileURLToPath5(import.meta.url);
1895
+ const __dirname2 = dirname4(__filename2);
1896
+ const hooksDir = join4(dirname4(__dirname2), "hooks");
1897
+ const prePromptPath = join4(hooksDir, "pre-prompt.js");
1898
+ const postToolResultPath = join4(hooksDir, "post-tool-result.js");
1899
+ const stopDocsRefreshPath = join4(hooksDir, "stop-docs-refresh.js");
1826
1900
  switch (subcommand) {
1827
1901
  case "install":
1828
- return await installHooks(settingsPath, prePromptPath, postToolResultPath, global);
1902
+ return installHooks(
1903
+ settingsPath,
1904
+ prePromptPath,
1905
+ postToolResultPath,
1906
+ stopDocsRefreshPath,
1907
+ global
1908
+ );
1829
1909
  case "uninstall":
1830
- return await uninstallHooks(settingsPath, global);
1910
+ return uninstallHooks(settingsPath, global);
1831
1911
  case "status":
1832
- return await hooksStatus(settingsPath, global);
1912
+ return hooksStatus(settingsPath, global);
1833
1913
  default:
1834
1914
  return {
1835
1915
  success: false,
@@ -1838,37 +1918,79 @@ async function hooksCommand(options) {
1838
1918
  };
1839
1919
  }
1840
1920
  }
1841
- async function installHooks(settingsPath, prePromptPath, postToolResultPath, global) {
1921
+ function installHooks(settingsPath, prePromptPath, postToolResultPath, stopDocsRefreshPath, global) {
1842
1922
  try {
1843
- if (!existsSync3(prePromptPath)) {
1923
+ if (!existsSync4(prePromptPath)) {
1844
1924
  return {
1845
1925
  success: false,
1846
1926
  message: `Hook script not found: ${prePromptPath}`,
1847
1927
  error: "Hook scripts not built. Run `npm run build` first."
1848
1928
  };
1849
1929
  }
1850
- if (!existsSync3(postToolResultPath)) {
1930
+ if (!existsSync4(postToolResultPath)) {
1851
1931
  return {
1852
1932
  success: false,
1853
1933
  message: `Hook script not found: ${postToolResultPath}`,
1854
1934
  error: "Hook scripts not built. Run `npm run build` first."
1855
1935
  };
1856
1936
  }
1937
+ if (!existsSync4(stopDocsRefreshPath)) {
1938
+ return {
1939
+ success: false,
1940
+ message: `Hook script not found: ${stopDocsRefreshPath}`,
1941
+ error: "Hook scripts not built. Run `npm run build` first."
1942
+ };
1943
+ }
1857
1944
  let settings = {};
1858
- if (existsSync3(settingsPath)) {
1945
+ if (existsSync4(settingsPath)) {
1859
1946
  const settingsJson = readFileSync4(settingsPath, "utf-8");
1860
1947
  settings = JSON.parse(settingsJson);
1861
1948
  } else {
1862
- const claudeDir = dirname3(settingsPath);
1863
- if (!existsSync3(claudeDir)) {
1949
+ const claudeDir = dirname4(settingsPath);
1950
+ if (!existsSync4(claudeDir)) {
1864
1951
  mkdirSync2(claudeDir, { recursive: true });
1865
1952
  }
1866
1953
  }
1867
- settings["hooks"] = {
1868
- ...typeof settings["hooks"] === "object" && settings["hooks"] !== null ? settings["hooks"] : {},
1869
- prePrompt: `node ${prePromptPath}`,
1870
- postToolResult: `node ${postToolResultPath}`
1871
- };
1954
+ const hooks = typeof settings["hooks"] === "object" && settings["hooks"] !== null ? settings["hooks"] : {};
1955
+ removeSparnHooks(hooks);
1956
+ if (!hooks[PRE_PROMPT_EVENT]) {
1957
+ hooks[PRE_PROMPT_EVENT] = [];
1958
+ }
1959
+ hooks[PRE_PROMPT_EVENT].push({
1960
+ hooks: [
1961
+ {
1962
+ type: "command",
1963
+ command: `node "${prePromptPath.replace(/\\/g, "/")}"`,
1964
+ timeout: 10
1965
+ }
1966
+ ]
1967
+ });
1968
+ if (!hooks[POST_TOOL_EVENT]) {
1969
+ hooks[POST_TOOL_EVENT] = [];
1970
+ }
1971
+ hooks[POST_TOOL_EVENT].push({
1972
+ matcher: POST_TOOL_MATCHER,
1973
+ hooks: [
1974
+ {
1975
+ type: "command",
1976
+ command: `node "${postToolResultPath.replace(/\\/g, "/")}"`,
1977
+ timeout: 10
1978
+ }
1979
+ ]
1980
+ });
1981
+ if (!hooks[STOP_DOCS_EVENT]) {
1982
+ hooks[STOP_DOCS_EVENT] = [];
1983
+ }
1984
+ hooks[STOP_DOCS_EVENT].push({
1985
+ hooks: [
1986
+ {
1987
+ type: "command",
1988
+ command: `node "${stopDocsRefreshPath.replace(/\\/g, "/")}"`,
1989
+ timeout: 10
1990
+ }
1991
+ ]
1992
+ });
1993
+ settings["hooks"] = hooks;
1872
1994
  writeFileSync3(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
1873
1995
  return {
1874
1996
  success: true,
@@ -1876,7 +1998,8 @@ async function installHooks(settingsPath, prePromptPath, postToolResultPath, glo
1876
1998
  installed: true,
1877
1999
  hookPaths: {
1878
2000
  prePrompt: prePromptPath,
1879
- postToolResult: postToolResultPath
2001
+ postToolResult: postToolResultPath,
2002
+ stopDocsRefresh: stopDocsRefreshPath
1880
2003
  }
1881
2004
  };
1882
2005
  } catch (error) {
@@ -1887,9 +2010,9 @@ async function installHooks(settingsPath, prePromptPath, postToolResultPath, glo
1887
2010
  };
1888
2011
  }
1889
2012
  }
1890
- async function uninstallHooks(settingsPath, global) {
2013
+ function uninstallHooks(settingsPath, global) {
1891
2014
  try {
1892
- if (!existsSync3(settingsPath)) {
2015
+ if (!existsSync4(settingsPath)) {
1893
2016
  return {
1894
2017
  success: true,
1895
2018
  message: "No hooks installed (settings.json not found)",
@@ -1900,8 +2023,12 @@ async function uninstallHooks(settingsPath, global) {
1900
2023
  const settings = JSON.parse(settingsJson);
1901
2024
  if (settings["hooks"] && typeof settings["hooks"] === "object" && settings["hooks"] !== null) {
1902
2025
  const hooks = settings["hooks"];
1903
- delete hooks["prePrompt"];
1904
- delete hooks["postToolResult"];
2026
+ removeSparnHooks(hooks);
2027
+ for (const event of Object.keys(hooks)) {
2028
+ if (Array.isArray(hooks[event]) && hooks[event].length === 0) {
2029
+ delete hooks[event];
2030
+ }
2031
+ }
1905
2032
  if (Object.keys(hooks).length === 0) {
1906
2033
  delete settings["hooks"];
1907
2034
  }
@@ -1920,9 +2047,9 @@ async function uninstallHooks(settingsPath, global) {
1920
2047
  };
1921
2048
  }
1922
2049
  }
1923
- async function hooksStatus(settingsPath, global) {
2050
+ function hooksStatus(settingsPath, global) {
1924
2051
  try {
1925
- if (!existsSync3(settingsPath)) {
2052
+ if (!existsSync4(settingsPath)) {
1926
2053
  return {
1927
2054
  success: true,
1928
2055
  message: global ? "No global hooks installed (settings.json not found)" : "No project hooks installed (settings.json not found)",
@@ -1931,22 +2058,25 @@ async function hooksStatus(settingsPath, global) {
1931
2058
  }
1932
2059
  const settingsJson = readFileSync4(settingsPath, "utf-8");
1933
2060
  const settings = JSON.parse(settingsJson);
1934
- const hasHooks = settings["hooks"] && typeof settings["hooks"] === "object" && settings["hooks"] !== null && "prePrompt" in settings["hooks"] && "postToolResult" in settings["hooks"];
1935
- if (!hasHooks) {
2061
+ const hooks = settings["hooks"] && typeof settings["hooks"] === "object" && settings["hooks"] !== null ? settings["hooks"] : {};
2062
+ const prePromptHook = findSparnHook(hooks, PRE_PROMPT_EVENT);
2063
+ const postToolHook = findSparnHook(hooks, POST_TOOL_EVENT);
2064
+ const stopDocsHook = findSparnHook(hooks, STOP_DOCS_EVENT);
2065
+ if (!prePromptHook && !postToolHook && !stopDocsHook) {
1936
2066
  return {
1937
2067
  success: true,
1938
- message: global ? "No global hooks installed" : "No project hooks installed",
2068
+ message: global ? "No global sparn hooks installed" : "No project sparn hooks installed",
1939
2069
  installed: false
1940
2070
  };
1941
2071
  }
1942
- const hooks = settings["hooks"];
1943
2072
  return {
1944
2073
  success: true,
1945
- message: global ? "Global hooks active" : "Project hooks active",
2074
+ message: global ? "Global sparn hooks active" : "Project sparn hooks active",
1946
2075
  installed: true,
1947
2076
  hookPaths: {
1948
- prePrompt: hooks["prePrompt"] || "",
1949
- postToolResult: hooks["postToolResult"] || ""
2077
+ prePrompt: prePromptHook || "(not installed)",
2078
+ postToolResult: postToolHook || "(not installed)",
2079
+ stopDocsRefresh: stopDocsHook || "(not installed)"
1950
2080
  }
1951
2081
  };
1952
2082
  } catch (error) {
@@ -1957,20 +2087,163 @@ async function hooksStatus(settingsPath, global) {
1957
2087
  };
1958
2088
  }
1959
2089
  }
2090
+ function removeSparnHooks(hooks) {
2091
+ for (const event of Object.keys(hooks)) {
2092
+ if (!Array.isArray(hooks[event])) continue;
2093
+ hooks[event] = hooks[event].filter((group) => {
2094
+ if (!Array.isArray(group.hooks)) return true;
2095
+ return !group.hooks.some(
2096
+ (h) => typeof h.command === "string" && h.command.includes(SPARN_MARKER)
2097
+ );
2098
+ });
2099
+ }
2100
+ }
2101
+ function findSparnHook(hooks, event) {
2102
+ const groups = hooks[event];
2103
+ if (!Array.isArray(groups)) return null;
2104
+ for (const group of groups) {
2105
+ if (!Array.isArray(group.hooks)) continue;
2106
+ for (const h of group.hooks) {
2107
+ if (typeof h.command === "string" && h.command.includes(SPARN_MARKER)) {
2108
+ return h.command;
2109
+ }
2110
+ }
2111
+ }
2112
+ return null;
2113
+ }
2114
+ var PRE_PROMPT_EVENT, POST_TOOL_EVENT, STOP_DOCS_EVENT, POST_TOOL_MATCHER, SPARN_MARKER;
1960
2115
  var init_hooks = __esm({
1961
2116
  "src/cli/commands/hooks.ts"() {
1962
2117
  "use strict";
1963
2118
  init_esm_shims();
2119
+ PRE_PROMPT_EVENT = "UserPromptSubmit";
2120
+ POST_TOOL_EVENT = "PostToolUse";
2121
+ STOP_DOCS_EVENT = "Stop";
2122
+ POST_TOOL_MATCHER = "Bash|Read|Grep|Glob";
2123
+ SPARN_MARKER = "sparn";
1964
2124
  }
1965
2125
  });
1966
2126
 
1967
- // src/cli/commands/interactive.ts
1968
- var interactive_exports = {};
1969
- __export(interactive_exports, {
1970
- interactiveCommand: () => interactiveCommand
1971
- });
1972
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
1973
- import { resolve } from "path";
2127
+ // src/core/metrics.ts
2128
+ function createMetricsCollector() {
2129
+ const optimizations = [];
2130
+ let daemonMetrics = {
2131
+ startTime: Date.now(),
2132
+ sessionsWatched: 0,
2133
+ totalOptimizations: 0,
2134
+ totalTokensSaved: 0,
2135
+ averageLatency: 0,
2136
+ memoryUsage: 0
2137
+ };
2138
+ let cacheHits = 0;
2139
+ let cacheMisses = 0;
2140
+ function recordOptimization(metric) {
2141
+ optimizations.push(metric);
2142
+ daemonMetrics.totalOptimizations++;
2143
+ daemonMetrics.totalTokensSaved += metric.tokensBefore - metric.tokensAfter;
2144
+ if (metric.cacheHitRate > 0) {
2145
+ const hits = Math.round(metric.entriesProcessed * metric.cacheHitRate);
2146
+ cacheHits += hits;
2147
+ cacheMisses += metric.entriesProcessed - hits;
2148
+ }
2149
+ daemonMetrics.averageLatency = (daemonMetrics.averageLatency * (daemonMetrics.totalOptimizations - 1) + metric.duration) / daemonMetrics.totalOptimizations;
2150
+ if (optimizations.length > 1e3) {
2151
+ optimizations.shift();
2152
+ }
2153
+ }
2154
+ function updateDaemon(metric) {
2155
+ daemonMetrics = {
2156
+ ...daemonMetrics,
2157
+ ...metric
2158
+ };
2159
+ }
2160
+ function calculatePercentile(sortedValues, percentile) {
2161
+ if (sortedValues.length === 0) return 0;
2162
+ const index = Math.ceil(percentile / 100 * sortedValues.length) - 1;
2163
+ return sortedValues[index] || 0;
2164
+ }
2165
+ function getSnapshot() {
2166
+ const totalRuns = optimizations.length;
2167
+ const totalDuration = optimizations.reduce((sum, m) => sum + m.duration, 0);
2168
+ const totalTokensSaved = optimizations.reduce(
2169
+ (sum, m) => sum + (m.tokensBefore - m.tokensAfter),
2170
+ 0
2171
+ );
2172
+ const totalTokensBefore = optimizations.reduce((sum, m) => sum + m.tokensBefore, 0);
2173
+ const averageReduction = totalTokensBefore > 0 ? totalTokensSaved / totalTokensBefore : 0;
2174
+ const sortedDurations = optimizations.map((m) => m.duration).sort((a, b) => a - b);
2175
+ const totalCacheQueries = cacheHits + cacheMisses;
2176
+ const hitRate = totalCacheQueries > 0 ? cacheHits / totalCacheQueries : 0;
2177
+ return {
2178
+ timestamp: Date.now(),
2179
+ optimization: {
2180
+ totalRuns,
2181
+ totalDuration,
2182
+ totalTokensSaved,
2183
+ averageReduction,
2184
+ p50Latency: calculatePercentile(sortedDurations, 50),
2185
+ p95Latency: calculatePercentile(sortedDurations, 95),
2186
+ p99Latency: calculatePercentile(sortedDurations, 99)
2187
+ },
2188
+ cache: {
2189
+ hitRate,
2190
+ totalHits: cacheHits,
2191
+ totalMisses: cacheMisses,
2192
+ size: optimizations.reduce((sum, m) => sum + m.entriesKept, 0)
2193
+ },
2194
+ daemon: {
2195
+ uptime: Date.now() - daemonMetrics.startTime,
2196
+ sessionsWatched: daemonMetrics.sessionsWatched,
2197
+ memoryUsage: daemonMetrics.memoryUsage
2198
+ }
2199
+ };
2200
+ }
2201
+ function exportMetrics() {
2202
+ return JSON.stringify(getSnapshot(), null, 2);
2203
+ }
2204
+ function reset() {
2205
+ optimizations.length = 0;
2206
+ cacheHits = 0;
2207
+ cacheMisses = 0;
2208
+ daemonMetrics = {
2209
+ startTime: Date.now(),
2210
+ sessionsWatched: 0,
2211
+ totalOptimizations: 0,
2212
+ totalTokensSaved: 0,
2213
+ averageLatency: 0,
2214
+ memoryUsage: 0
2215
+ };
2216
+ }
2217
+ return {
2218
+ recordOptimization,
2219
+ updateDaemon,
2220
+ getSnapshot,
2221
+ export: exportMetrics,
2222
+ reset
2223
+ };
2224
+ }
2225
+ function getMetrics() {
2226
+ if (!globalMetrics) {
2227
+ globalMetrics = createMetricsCollector();
2228
+ }
2229
+ return globalMetrics;
2230
+ }
2231
+ var globalMetrics;
2232
+ var init_metrics = __esm({
2233
+ "src/core/metrics.ts"() {
2234
+ "use strict";
2235
+ init_esm_shims();
2236
+ globalMetrics = null;
2237
+ }
2238
+ });
2239
+
2240
+ // src/cli/commands/interactive.ts
2241
+ var interactive_exports = {};
2242
+ __export(interactive_exports, {
2243
+ interactiveCommand: () => interactiveCommand
2244
+ });
2245
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
2246
+ import { resolve as resolve2 } from "path";
1974
2247
  import { confirm, input, number, select } from "@inquirer/prompts";
1975
2248
  import { load as parseYAML2, dump as stringifyYAML2 } from "js-yaml";
1976
2249
  function showWelcomeBanner() {
@@ -2023,9 +2296,9 @@ async function configureWizard(configPath) {
2023
2296
  const section = await select({
2024
2297
  message: "Which settings would you like to configure?",
2025
2298
  choices: [
2026
- { name: "\u{1F52A} Pruning (Sparse Coding)", value: "pruning" },
2027
- { name: "\u23F3 Decay (Engram Theory)", value: "decay" },
2028
- { name: "\u{1F3AF} States (Multi-State Synapses)", value: "states" },
2299
+ { name: "\u{1F52A} Pruning (Relevance Filtering)", value: "pruning" },
2300
+ { name: "\u23F3 Decay (Time-Based)", value: "decay" },
2301
+ { name: "\u{1F3AF} States (Entry Classification)", value: "states" },
2029
2302
  { name: "\u26A1 Real-time Optimization", value: "realtime" },
2030
2303
  { name: "\u{1F3A8} UI Preferences", value: "ui" },
2031
2304
  { name: "\u2190 Back to Main Menu", value: "back" }
@@ -2035,7 +2308,7 @@ async function configureWizard(configPath) {
2035
2308
  switch (section) {
2036
2309
  case "pruning": {
2037
2310
  console.log(synapseViolet("\n\u{1F52A} Pruning Configuration"));
2038
- console.log(dim("Sparse coding: Keep only the most relevant context\n"));
2311
+ console.log(dim("Keep only the most relevant context\n"));
2039
2312
  const threshold = await number({
2040
2313
  message: "Pruning threshold (percentage of entries to keep):",
2041
2314
  default: config.pruning.threshold,
@@ -2055,7 +2328,7 @@ async function configureWizard(configPath) {
2055
2328
  }
2056
2329
  case "decay": {
2057
2330
  console.log(synapseViolet("\n\u23F3 Decay Configuration"));
2058
- console.log(dim("Engram theory: Apply time-based decay to memories\n"));
2331
+ console.log(dim("Apply time-based decay to older entries\n"));
2059
2332
  const defaultTTL = await number({
2060
2333
  message: "Default TTL in hours:",
2061
2334
  default: config.decay.defaultTTL,
@@ -2075,7 +2348,7 @@ async function configureWizard(configPath) {
2075
2348
  }
2076
2349
  case "states": {
2077
2350
  console.log(synapseViolet("\n\u{1F3AF} State Threshold Configuration"));
2078
- console.log(dim("Multi-state synapses: Classify entries as active/ready/silent\n"));
2351
+ console.log(dim("Classify entries as active/ready/silent based on score\n"));
2079
2352
  const activeThreshold = await number({
2080
2353
  message: "Active state threshold (0.0-1.0):",
2081
2354
  default: config.states.activeThreshold,
@@ -2158,7 +2431,7 @@ async function optimizePreview(memory) {
2158
2431
  return;
2159
2432
  }
2160
2433
  try {
2161
- const content = readFileSync5(resolve(process.cwd(), inputFile), "utf-8");
2434
+ const content = readFileSync5(resolve2(process.cwd(), inputFile), "utf-8");
2162
2435
  const tokensBefore = Math.ceil(content.length / 4);
2163
2436
  console.log(synapseViolet("\n\u{1F4C4} File Preview:"));
2164
2437
  console.log(dim(` Length: ${content.length} characters`));
@@ -2198,7 +2471,7 @@ async function optimizePreview(memory) {
2198
2471
  message: "Output file path:",
2199
2472
  default: inputFile.replace(/(\.[^.]+)$/, ".optimized$1")
2200
2473
  });
2201
- writeFileSync4(resolve(process.cwd(), outputFile), result.output, "utf-8");
2474
+ writeFileSync4(resolve2(process.cwd(), outputFile), result.output, "utf-8");
2202
2475
  console.log(neuralCyan(`
2203
2476
  \u{1F4BE} Saved to ${outputFile}
2204
2477
  `));
@@ -2371,7 +2644,7 @@ async function showQuickActions(memory, configPath) {
2371
2644
  default: false
2372
2645
  });
2373
2646
  if (shouldSave) {
2374
- const outputPath = resolve(configPath.replace(/\.yaml$/, ".json"));
2647
+ const outputPath = resolve2(configPath.replace(/\.yaml$/, ".json"));
2375
2648
  writeFileSync4(outputPath, json, "utf-8");
2376
2649
  console.log(neuralCyan(`
2377
2650
  \u{1F4BE} Saved to ${outputPath}
@@ -2389,7 +2662,7 @@ It includes some sample content to demonstrate the optimization process.
2389
2662
 
2390
2663
  ## Features
2391
2664
  - Token counting
2392
- - Sparse coding
2665
+ - Relevance filtering
2393
2666
  - Decay application
2394
2667
  - State classification
2395
2668
  `.trim();
@@ -2468,94 +2741,2891 @@ var init_interactive = __esm({
2468
2741
  }
2469
2742
  });
2470
2743
 
2471
- // src/cli/index.ts
2472
- init_esm_shims();
2473
- init_banner();
2474
- import { spawn as spawn2 } from "child_process";
2475
- import { readFileSync as readFileSync6 } from "fs";
2476
- import { dirname as dirname4, join as join4, resolve as resolve2 } from "path";
2477
- import { fileURLToPath as fileURLToPath5 } from "url";
2478
- import { Command } from "commander";
2479
- function getVersion2() {
2744
+ // src/core/dependency-graph.ts
2745
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync6, statSync } from "fs";
2746
+ import { extname, join as join5, relative, resolve as resolve3 } from "path";
2747
+ function parseImports(content, filePath) {
2748
+ const edges = [];
2749
+ const seen = /* @__PURE__ */ new Set();
2750
+ for (const pattern of IMPORT_PATTERNS) {
2751
+ const regex = new RegExp(pattern.source, pattern.flags);
2752
+ const matches = [...content.matchAll(regex)];
2753
+ for (const match of matches) {
2754
+ if (pattern.source.includes("from")) {
2755
+ const symbolsRaw = match[1] || "";
2756
+ const target = match[2] || "";
2757
+ if (!target || target.startsWith(".") === false && !target.startsWith("/")) {
2758
+ continue;
2759
+ }
2760
+ const symbols = symbolsRaw.split(",").map(
2761
+ (s) => s.trim().split(/\s+as\s+/)[0]?.trim() || ""
2762
+ ).filter(Boolean);
2763
+ const key = `${filePath}->${target}`;
2764
+ if (!seen.has(key)) {
2765
+ seen.add(key);
2766
+ edges.push({ source: filePath, target, symbols });
2767
+ }
2768
+ } else if (pattern.source.includes("require")) {
2769
+ const target = match[1] || "";
2770
+ if (!target || !target.startsWith(".") && !target.startsWith("/")) {
2771
+ continue;
2772
+ }
2773
+ const key = `${filePath}->${target}`;
2774
+ if (!seen.has(key)) {
2775
+ seen.add(key);
2776
+ edges.push({ source: filePath, target, symbols: [] });
2777
+ }
2778
+ } else {
2779
+ const target = match[1] || "";
2780
+ if (!target || !target.startsWith(".") && !target.startsWith("/")) {
2781
+ continue;
2782
+ }
2783
+ const key = `${filePath}->${target}`;
2784
+ if (!seen.has(key)) {
2785
+ seen.add(key);
2786
+ edges.push({ source: filePath, target, symbols: [] });
2787
+ }
2788
+ }
2789
+ }
2790
+ }
2791
+ return edges;
2792
+ }
2793
+ function parseExports(content) {
2794
+ const exportsList = [];
2795
+ for (const pattern of EXPORT_PATTERNS) {
2796
+ const regex = new RegExp(pattern.source, pattern.flags);
2797
+ const matches = [...content.matchAll(regex)];
2798
+ for (const match of matches) {
2799
+ if (match[1]) {
2800
+ const symbols = match[1].split(",").map(
2801
+ (s) => s.trim().split(/\s+as\s+/)[0]?.trim() || ""
2802
+ ).filter(Boolean);
2803
+ exportsList.push(...symbols);
2804
+ } else {
2805
+ exportsList.push("default");
2806
+ }
2807
+ }
2808
+ }
2809
+ return [...new Set(exportsList)];
2810
+ }
2811
+ function resolveImportPath(importPath, fromFile, projectRoot, extensions) {
2812
+ const cleanImport = importPath.replace(/\.(js|ts|tsx|jsx)$/, "");
2813
+ const baseDir = join5(projectRoot, fromFile, "..");
2814
+ const candidates = [
2815
+ ...extensions.map((ext) => resolve3(baseDir, `${cleanImport}${ext}`)),
2816
+ ...extensions.map((ext) => resolve3(baseDir, cleanImport, `index${ext}`))
2817
+ ];
2818
+ for (const candidate of candidates) {
2819
+ if (existsSync5(candidate)) {
2820
+ return relative(projectRoot, candidate).replace(/\\/g, "/");
2821
+ }
2822
+ }
2823
+ return null;
2824
+ }
2825
+ function collectFiles(dir, projectRoot, extensions, ignoreDirs) {
2826
+ const files = [];
2480
2827
  try {
2481
- const pkg = JSON.parse(readFileSync6(join4(process.cwd(), "package.json"), "utf-8"));
2482
- return pkg.version;
2828
+ const entries = readdirSync(dir, { withFileTypes: true });
2829
+ for (const entry of entries) {
2830
+ const fullPath = join5(dir, entry.name);
2831
+ if (entry.isDirectory()) {
2832
+ if (!ignoreDirs.includes(entry.name)) {
2833
+ files.push(...collectFiles(fullPath, projectRoot, extensions, ignoreDirs));
2834
+ }
2835
+ } else if (entry.isFile() && extensions.includes(extname(entry.name))) {
2836
+ files.push(relative(projectRoot, fullPath).replace(/\\/g, "/"));
2837
+ }
2838
+ }
2483
2839
  } catch {
2484
- const __filename2 = fileURLToPath5(import.meta.url);
2485
- const __dirname2 = dirname4(__filename2);
2486
- const pkg = JSON.parse(readFileSync6(join4(__dirname2, "../../package.json"), "utf-8"));
2487
- return pkg.version;
2488
2840
  }
2841
+ return files;
2489
2842
  }
2490
- var VERSION2 = getVersion2();
2491
- async function handleError(error, context) {
2492
- const { errorRed: errorRed2, synapseViolet: synapseViolet2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
2493
- const errorMsg = error instanceof Error ? error.message : String(error);
2494
- const stack = error instanceof Error ? error.stack : void 0;
2495
- console.error(errorRed2("\n\u2717 Error:"), errorMsg);
2496
- if (context) {
2497
- console.error(errorRed2("Context:"), context);
2498
- }
2499
- if (errorMsg.includes("SQLITE") || errorMsg.includes("database")) {
2500
- console.error(synapseViolet2("\n\u{1F4A1} Database issue detected:"));
2501
- console.error(" Try running: rm -rf .sparn/ && sparn init");
2502
- console.error(" This will reinitialize your Sparn database.\n");
2843
+ function createDependencyGraph(config) {
2844
+ const {
2845
+ projectRoot,
2846
+ maxDepth = 50,
2847
+ extensions = [".ts", ".tsx", ".js", ".jsx"],
2848
+ ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"]
2849
+ } = config;
2850
+ const nodes = /* @__PURE__ */ new Map();
2851
+ let built = false;
2852
+ async function build() {
2853
+ nodes.clear();
2854
+ const files = collectFiles(projectRoot, projectRoot, extensions, ignoreDirs);
2855
+ for (const filePath of files) {
2856
+ const fullPath = join5(projectRoot, filePath);
2857
+ try {
2858
+ const content = readFileSync6(fullPath, "utf-8");
2859
+ const stat = statSync(fullPath);
2860
+ const exports = parseExports(content);
2861
+ const imports = parseImports(content, filePath);
2862
+ const resolvedImports = [];
2863
+ for (const imp of imports) {
2864
+ const resolved = resolveImportPath(imp.target, filePath, projectRoot, extensions);
2865
+ if (resolved) {
2866
+ resolvedImports.push({ ...imp, target: resolved });
2867
+ }
2868
+ }
2869
+ nodes.set(filePath, {
2870
+ filePath,
2871
+ exports,
2872
+ imports: resolvedImports,
2873
+ callers: [],
2874
+ // Populated in second pass
2875
+ engram_score: 0,
2876
+ lastModified: stat.mtimeMs,
2877
+ tokenEstimate: estimateTokens(content)
2878
+ });
2879
+ } catch {
2880
+ }
2881
+ }
2882
+ for (const [filePath, node] of nodes) {
2883
+ for (const imp of node.imports) {
2884
+ const targetNode = nodes.get(imp.target);
2885
+ if (targetNode && !targetNode.callers.includes(filePath)) {
2886
+ targetNode.callers.push(filePath);
2887
+ }
2888
+ }
2889
+ }
2890
+ built = true;
2891
+ return nodes;
2503
2892
  }
2504
- if (errorMsg.includes("EACCES") || errorMsg.includes("permission")) {
2505
- console.error(synapseViolet2("\n\u{1F4A1} Permission issue detected:"));
2506
- console.error(" Check file permissions in .sparn/ directory");
2507
- console.error(" Try: chmod -R u+rw .sparn/\n");
2893
+ async function analyze() {
2894
+ if (!built) await build();
2895
+ const entryPoints = [];
2896
+ const orphans = [];
2897
+ const callerCounts = /* @__PURE__ */ new Map();
2898
+ for (const [filePath, node] of nodes) {
2899
+ if (node.callers.length === 0 && node.imports.length > 0) {
2900
+ entryPoints.push(filePath);
2901
+ }
2902
+ if (node.callers.length === 0 && node.imports.length === 0) {
2903
+ orphans.push(filePath);
2904
+ }
2905
+ callerCounts.set(filePath, node.callers.length);
2906
+ }
2907
+ const sortedByCallers = [...callerCounts.entries()].sort((a, b) => b[1] - a[1]).filter(([, count]) => count > 0);
2908
+ const hotPaths = sortedByCallers.slice(0, 10).map(([path2]) => path2);
2909
+ const totalTokens = [...nodes.values()].reduce((sum, n) => sum + n.tokenEstimate, 0);
2910
+ const hotPathTokens = hotPaths.reduce(
2911
+ (sum, path2) => sum + (nodes.get(path2)?.tokenEstimate || 0),
2912
+ 0
2913
+ );
2914
+ return {
2915
+ entryPoints,
2916
+ hotPaths,
2917
+ orphans,
2918
+ totalTokens,
2919
+ optimizedTokens: hotPathTokens
2920
+ };
2508
2921
  }
2509
- if (errorMsg.includes("ENOENT") || errorMsg.includes("no such file")) {
2510
- console.error(synapseViolet2("\n\u{1F4A1} File not found:"));
2511
- console.error(" Make sure you have run: sparn init");
2512
- console.error(" Or check that the specified file exists.\n");
2922
+ async function focus(pattern) {
2923
+ if (!built) await build();
2924
+ const matching = /* @__PURE__ */ new Map();
2925
+ const lowerPattern = pattern.toLowerCase();
2926
+ for (const [filePath, node] of nodes) {
2927
+ if (filePath.toLowerCase().includes(lowerPattern)) {
2928
+ matching.set(filePath, node);
2929
+ for (const imp of node.imports) {
2930
+ const depNode = nodes.get(imp.target);
2931
+ if (depNode) {
2932
+ matching.set(imp.target, depNode);
2933
+ }
2934
+ }
2935
+ for (const caller of node.callers) {
2936
+ const callerNode = nodes.get(caller);
2937
+ if (callerNode) {
2938
+ matching.set(caller, callerNode);
2939
+ }
2940
+ }
2941
+ }
2942
+ }
2943
+ return matching;
2513
2944
  }
2514
- if (errorMsg.includes("out of memory") || errorMsg.includes("heap")) {
2515
- console.error(synapseViolet2("\n\u{1F4A1} Memory issue detected:"));
2516
- console.error(" Try processing smaller chunks of context");
2517
- console.error(" Or increase Node.js memory: NODE_OPTIONS=--max-old-space-size=4096\n");
2945
+ async function getFilesFromEntry(entryPoint, depth = maxDepth) {
2946
+ if (!built) await build();
2947
+ const visited = /* @__PURE__ */ new Set();
2948
+ const queue = [{ path: entryPoint, depth: 0 }];
2949
+ while (queue.length > 0) {
2950
+ const item = queue.shift();
2951
+ if (!item) break;
2952
+ if (visited.has(item.path) || item.depth > depth) continue;
2953
+ visited.add(item.path);
2954
+ const node = nodes.get(item.path);
2955
+ if (node) {
2956
+ for (const imp of node.imports) {
2957
+ if (!visited.has(imp.target)) {
2958
+ queue.push({ path: imp.target, depth: item.depth + 1 });
2959
+ }
2960
+ }
2961
+ }
2962
+ }
2963
+ return [...visited];
2518
2964
  }
2519
- if (process.env["SPARN_DEBUG"] === "true" && stack) {
2520
- console.error(errorRed2("\nStack trace:"));
2521
- console.error(stack);
2522
- } else {
2523
- console.error(" Run with SPARN_DEBUG=true for stack trace\n");
2965
+ function getNodes() {
2966
+ return nodes;
2524
2967
  }
2525
- process.exit(1);
2968
+ return {
2969
+ build,
2970
+ analyze,
2971
+ focus,
2972
+ getFilesFromEntry,
2973
+ getNodes
2974
+ };
2526
2975
  }
2527
- process.on("unhandledRejection", (reason) => {
2528
- void handleError(reason, "Unhandled promise rejection");
2529
- });
2530
- process.on("uncaughtException", (error) => {
2531
- void handleError(error, "Uncaught exception");
2976
+ var IMPORT_PATTERNS, EXPORT_PATTERNS;
2977
+ var init_dependency_graph = __esm({
2978
+ "src/core/dependency-graph.ts"() {
2979
+ "use strict";
2980
+ init_esm_shims();
2981
+ init_tokenizer();
2982
+ IMPORT_PATTERNS = [
2983
+ // import { Foo, Bar } from './module'
2984
+ /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g,
2985
+ // import Foo from './module'
2986
+ /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
2987
+ // import * as Foo from './module'
2988
+ /import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
2989
+ // import './module' (side-effect)
2990
+ /import\s+['"]([^'"]+)['"]/g,
2991
+ // require('./module')
2992
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
2993
+ ];
2994
+ EXPORT_PATTERNS = [
2995
+ // export { Foo, Bar }
2996
+ /export\s+\{([^}]+)\}/g,
2997
+ // export function/class/const/let/var/type/interface
2998
+ /export\s+(?:default\s+)?(?:function|class|const|let|var|type|interface|enum)\s+(\w+)/g,
2999
+ // export default
3000
+ /export\s+default\s+/g
3001
+ ];
3002
+ }
2532
3003
  });
2533
- var program = new Command();
2534
- program.name("sparn").description("Neuroscience-inspired context optimization for AI coding agents").version(VERSION2, "-v, --version", "Output the current version").helpOption("-h, --help", "Display help for command");
2535
- program.command("init").description("Initialize Sparn in the current project").option("-f, --force", "Force overwrite if .sparn/ already exists").addHelpText(
2536
- "after",
2537
- `
2538
- Examples:
2539
- $ sparn init # Initialize in current directory
2540
- $ sparn init --force # Overwrite existing .sparn/ directory
2541
3004
 
2542
- Files Created:
2543
- .sparn/config.yaml # Configuration with neuroscience parameters
2544
- .sparn/memory.db # SQLite database for context storage
3005
+ // src/cli/commands/graph.ts
3006
+ var graph_exports = {};
3007
+ __export(graph_exports, {
3008
+ graphCommand: () => graphCommand
3009
+ });
3010
+ import { resolve as resolve4 } from "path";
3011
+ async function graphCommand(options) {
3012
+ const projectRoot = resolve4(process.cwd());
3013
+ const graph = createDependencyGraph({
3014
+ projectRoot,
3015
+ maxDepth: options.depth
3016
+ });
3017
+ await graph.build();
3018
+ let nodes = graph.getNodes();
3019
+ if (options.focus) {
3020
+ nodes = await graph.focus(options.focus);
3021
+ }
3022
+ if (options.entry) {
3023
+ const allNodes = graph.getNodes();
3024
+ if (!allNodes.has(options.entry)) {
3025
+ throw new Error(
3026
+ `Entry point not found in graph: ${options.entry}. Available: ${[...allNodes.keys()].slice(0, 5).join(", ")}${allNodes.size > 5 ? "..." : ""}`
3027
+ );
3028
+ }
3029
+ const files = await graph.getFilesFromEntry(options.entry, options.depth);
3030
+ const entryNodes = /* @__PURE__ */ new Map();
3031
+ for (const f of files) {
3032
+ const node = nodes.get(f);
3033
+ if (node) entryNodes.set(f, node);
3034
+ }
3035
+ nodes = entryNodes;
3036
+ }
3037
+ const analysis = await graph.analyze();
3038
+ const result = {
3039
+ analysis,
3040
+ nodeCount: nodes.size
3041
+ };
3042
+ if (options.json) {
3043
+ result.json = JSON.stringify(
3044
+ {
3045
+ analysis,
3046
+ nodeCount: nodes.size,
3047
+ nodes: Object.fromEntries(
3048
+ [...nodes.entries()].map(([k, v]) => [
3049
+ k,
3050
+ {
3051
+ exports: v.exports,
3052
+ imports: v.imports.map((i) => i.target),
3053
+ callers: v.callers,
3054
+ tokens: v.tokenEstimate
3055
+ }
3056
+ ])
3057
+ )
3058
+ },
3059
+ null,
3060
+ 2
3061
+ );
3062
+ }
3063
+ return result;
3064
+ }
3065
+ var init_graph = __esm({
3066
+ "src/cli/commands/graph.ts"() {
3067
+ "use strict";
3068
+ init_esm_shims();
3069
+ init_dependency_graph();
3070
+ }
3071
+ });
2545
3072
 
2546
- Next Steps:
2547
- After initialization, use 'sparn optimize' to start optimizing context.
2548
- `
2549
- ).action(async (options) => {
2550
- const { initCommand: initCommand2, displayInitSuccess: displayInitSuccess2 } = await Promise.resolve().then(() => (init_init(), init_exports));
2551
- const { createInitSpinner: createInitSpinner2 } = await Promise.resolve().then(() => (init_progress(), progress_exports));
2552
- const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
2553
- const spinner = createInitSpinner2("\u{1F9E0} Initializing Sparn...");
3073
+ // src/core/search-engine.ts
3074
+ import { execFileSync as execFileSync2, execSync } from "child_process";
3075
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
3076
+ import { extname as extname2, join as join6, relative as relative2 } from "path";
3077
+ import Database2 from "better-sqlite3";
3078
+ function hasRipgrep() {
2554
3079
  try {
2555
- spinner.start();
2556
- spinner.text = "\u{1F4C1} Creating .sparn/ directory...";
2557
- const result = await initCommand2({ force: options.force });
2558
- spinner.succeed(neuralCyan2("Sparn initialized successfully!"));
3080
+ execSync("rg --version", { stdio: "pipe" });
3081
+ return true;
3082
+ } catch {
3083
+ return false;
3084
+ }
3085
+ }
3086
+ function ripgrepSearch(query, projectRoot, opts = {}) {
3087
+ try {
3088
+ const args = ["--line-number", "--no-heading", "--color=never"];
3089
+ if (opts.fileGlob) {
3090
+ args.push("--glob", opts.fileGlob);
3091
+ }
3092
+ args.push(
3093
+ "--glob",
3094
+ "!node_modules",
3095
+ "--glob",
3096
+ "!dist",
3097
+ "--glob",
3098
+ "!.git",
3099
+ "--glob",
3100
+ "!.sparn",
3101
+ "--glob",
3102
+ "!coverage"
3103
+ );
3104
+ const maxResults = opts.maxResults || 20;
3105
+ args.push("--max-count", String(maxResults));
3106
+ if (opts.includeContext) {
3107
+ args.push("-C", "2");
3108
+ }
3109
+ args.push("--", query, projectRoot);
3110
+ const output = execFileSync2("rg", args, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
3111
+ const results = [];
3112
+ const lines = output.split("\n").filter(Boolean);
3113
+ for (const line of lines) {
3114
+ const match = line.match(/^(.+?):(\d+):(.*)/);
3115
+ if (match) {
3116
+ const filePath = relative2(projectRoot, match[1] || "").replace(/\\/g, "/");
3117
+ const lineNumber = Number.parseInt(match[2] || "0", 10);
3118
+ const content = (match[3] || "").trim();
3119
+ results.push({
3120
+ filePath,
3121
+ lineNumber,
3122
+ content,
3123
+ score: 0.8,
3124
+ // Exact match gets high base score
3125
+ context: [],
3126
+ engram_score: 0
3127
+ });
3128
+ }
3129
+ }
3130
+ return results.slice(0, maxResults);
3131
+ } catch {
3132
+ return [];
3133
+ }
3134
+ }
3135
+ function collectIndexableFiles(dir, projectRoot, ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"], exts = [".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".yaml", ".yml"]) {
3136
+ const files = [];
3137
+ try {
3138
+ const entries = readdirSync2(dir, { withFileTypes: true });
3139
+ for (const entry of entries) {
3140
+ const fullPath = join6(dir, entry.name);
3141
+ if (entry.isDirectory()) {
3142
+ if (!ignoreDirs.includes(entry.name)) {
3143
+ files.push(...collectIndexableFiles(fullPath, projectRoot, ignoreDirs, exts));
3144
+ }
3145
+ } else if (entry.isFile() && exts.includes(extname2(entry.name))) {
3146
+ try {
3147
+ const stat = statSync2(fullPath);
3148
+ if (stat.size < 100 * 1024) {
3149
+ files.push(relative2(projectRoot, fullPath).replace(/\\/g, "/"));
3150
+ }
3151
+ } catch {
3152
+ }
3153
+ }
3154
+ }
3155
+ } catch {
3156
+ }
3157
+ return files;
3158
+ }
3159
+ function createSearchEngine(dbPath) {
3160
+ let db = null;
3161
+ let projectRoot = "";
3162
+ const rgAvailable = hasRipgrep();
3163
+ async function init(root) {
3164
+ if (db) {
3165
+ try {
3166
+ db.close();
3167
+ } catch {
3168
+ }
3169
+ }
3170
+ projectRoot = root;
3171
+ db = new Database2(dbPath);
3172
+ db.pragma("journal_mode = WAL");
3173
+ db.exec(`
3174
+ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
3175
+ filepath, line_number, content, tokenize='porter'
3176
+ );
3177
+ `);
3178
+ db.exec(`
3179
+ CREATE TABLE IF NOT EXISTS search_meta (
3180
+ filepath TEXT PRIMARY KEY,
3181
+ mtime INTEGER NOT NULL,
3182
+ indexed_at INTEGER NOT NULL
3183
+ );
3184
+ `);
3185
+ }
3186
+ async function index(paths) {
3187
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
3188
+ const startTime = Date.now();
3189
+ const filesToIndex = paths || collectIndexableFiles(projectRoot, projectRoot);
3190
+ let filesIndexed = 0;
3191
+ let totalLines = 0;
3192
+ const insertStmt = db.prepare(
3193
+ "INSERT INTO search_index(filepath, line_number, content) VALUES (?, ?, ?)"
3194
+ );
3195
+ const metaStmt = db.prepare(
3196
+ "INSERT OR REPLACE INTO search_meta(filepath, mtime, indexed_at) VALUES (?, ?, ?)"
3197
+ );
3198
+ const checkMeta = db.prepare("SELECT mtime FROM search_meta WHERE filepath = ?");
3199
+ const deleteFile = db.prepare("DELETE FROM search_index WHERE filepath = ?");
3200
+ const transaction = db.transaction(() => {
3201
+ for (const filePath of filesToIndex) {
3202
+ const fullPath = join6(projectRoot, filePath);
3203
+ if (!existsSync6(fullPath)) continue;
3204
+ try {
3205
+ const stat = statSync2(fullPath);
3206
+ const existing = checkMeta.get(filePath);
3207
+ if (existing && existing.mtime >= stat.mtimeMs) {
3208
+ continue;
3209
+ }
3210
+ deleteFile.run(filePath);
3211
+ const content = readFileSync7(fullPath, "utf-8");
3212
+ const lines = content.split("\n");
3213
+ for (let i = 0; i < lines.length; i++) {
3214
+ const line = lines[i];
3215
+ if (line && line.trim().length > 0) {
3216
+ insertStmt.run(filePath, i + 1, line);
3217
+ totalLines++;
3218
+ }
3219
+ }
3220
+ metaStmt.run(filePath, stat.mtimeMs, Date.now());
3221
+ filesIndexed++;
3222
+ } catch {
3223
+ }
3224
+ }
3225
+ });
3226
+ transaction();
3227
+ return {
3228
+ filesIndexed,
3229
+ totalLines,
3230
+ duration: Date.now() - startTime
3231
+ };
3232
+ }
3233
+ async function search(query, opts = {}) {
3234
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
3235
+ const maxResults = opts.maxResults || 10;
3236
+ const results = [];
3237
+ const seen = /* @__PURE__ */ new Set();
3238
+ try {
3239
+ const ftsQuery = query.replace(/['"*(){}[\]^~\\:]/g, " ").trim().split(/\s+/).filter((w) => w.length > 0).map((w) => `content:${w}`).join(" ");
3240
+ if (ftsQuery.length > 0) {
3241
+ let sql = `
3242
+ SELECT filepath, line_number, content, rank
3243
+ FROM search_index
3244
+ WHERE search_index MATCH ?
3245
+ `;
3246
+ const params = [ftsQuery];
3247
+ if (opts.fileGlob) {
3248
+ const likePattern = opts.fileGlob.replace(/\*/g, "%").replace(/\?/g, "_");
3249
+ sql += " AND filepath LIKE ?";
3250
+ params.push(likePattern);
3251
+ }
3252
+ sql += " ORDER BY rank LIMIT ?";
3253
+ params.push(maxResults * 2);
3254
+ const rows = db.prepare(sql).all(...params);
3255
+ for (const row of rows) {
3256
+ const key = `${row.filepath}:${row.line_number}`;
3257
+ if (!seen.has(key)) {
3258
+ seen.add(key);
3259
+ const score = Math.min(1, Math.max(0, 1 + row.rank / 10));
3260
+ const context = [];
3261
+ if (opts.includeContext) {
3262
+ const contextRows = db.prepare(
3263
+ `SELECT content FROM search_index WHERE filepath = ? AND CAST(line_number AS INTEGER) BETWEEN ? AND ? ORDER BY CAST(line_number AS INTEGER)`
3264
+ ).all(row.filepath, row.line_number - 2, row.line_number + 2);
3265
+ context.push(...contextRows.map((r) => r.content));
3266
+ }
3267
+ results.push({
3268
+ filePath: row.filepath,
3269
+ lineNumber: row.line_number,
3270
+ content: row.content,
3271
+ score,
3272
+ context,
3273
+ engram_score: 0
3274
+ });
3275
+ }
3276
+ }
3277
+ }
3278
+ } catch {
3279
+ }
3280
+ if (rgAvailable && opts.useRipgrep !== false) {
3281
+ const rgResults = ripgrepSearch(query, projectRoot, opts);
3282
+ for (const result of rgResults) {
3283
+ const key = `${result.filePath}:${result.lineNumber}`;
3284
+ if (!seen.has(key)) {
3285
+ seen.add(key);
3286
+ results.push(result);
3287
+ }
3288
+ }
3289
+ }
3290
+ results.sort((a, b) => b.score - a.score);
3291
+ return results.slice(0, maxResults);
3292
+ }
3293
+ async function refresh() {
3294
+ if (!db) throw new Error("Search engine not initialized. Call init() first.");
3295
+ db.exec("DELETE FROM search_index");
3296
+ db.exec("DELETE FROM search_meta");
3297
+ return index();
3298
+ }
3299
+ async function close() {
3300
+ if (db) {
3301
+ db.close();
3302
+ db = null;
3303
+ }
3304
+ }
3305
+ return {
3306
+ init,
3307
+ index,
3308
+ search,
3309
+ refresh,
3310
+ close
3311
+ };
3312
+ }
3313
+ var init_search_engine = __esm({
3314
+ "src/core/search-engine.ts"() {
3315
+ "use strict";
3316
+ init_esm_shims();
3317
+ }
3318
+ });
3319
+
3320
+ // src/cli/commands/search.ts
3321
+ var search_exports = {};
3322
+ __export(search_exports, {
3323
+ searchCommand: () => searchCommand
3324
+ });
3325
+ import { resolve as resolve5 } from "path";
3326
+ async function searchCommand(options) {
3327
+ const projectRoot = resolve5(process.cwd());
3328
+ const dbPath = resolve5(projectRoot, ".sparn", "search.db");
3329
+ const engine = createSearchEngine(dbPath);
3330
+ try {
3331
+ await engine.init(projectRoot);
3332
+ if (options.subcommand === "init" || options.subcommand === "refresh") {
3333
+ const stats = options.subcommand === "refresh" ? await engine.refresh() : await engine.index();
3334
+ const result = {
3335
+ indexStats: stats,
3336
+ message: `Indexed ${stats.filesIndexed} files (${stats.totalLines} lines) in ${stats.duration}ms`
3337
+ };
3338
+ if (options.json) {
3339
+ result.json = JSON.stringify(stats, null, 2);
3340
+ }
3341
+ return result;
3342
+ }
3343
+ if (!options.query) {
3344
+ return { message: 'No search query provided. Usage: sparn search "query"' };
3345
+ }
3346
+ const results = await engine.search(options.query, {
3347
+ maxResults: options.maxResults || 10,
3348
+ fileGlob: options.glob,
3349
+ includeContext: true
3350
+ });
3351
+ const cmdResult = { results };
3352
+ if (options.json) {
3353
+ cmdResult.json = JSON.stringify(results, null, 2);
3354
+ }
3355
+ return cmdResult;
3356
+ } finally {
3357
+ await engine.close();
3358
+ }
3359
+ }
3360
+ var init_search = __esm({
3361
+ "src/cli/commands/search.ts"() {
3362
+ "use strict";
3363
+ init_esm_shims();
3364
+ init_search_engine();
3365
+ }
3366
+ });
3367
+
3368
+ // src/core/workflow-planner.ts
3369
+ import { randomUUID as randomUUID3 } from "crypto";
3370
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
3371
+ import { join as join7 } from "path";
3372
+ function createWorkflowPlanner(projectRoot) {
3373
+ const plansDir = join7(projectRoot, ".sparn", "plans");
3374
+ if (!existsSync7(plansDir)) {
3375
+ mkdirSync3(plansDir, { recursive: true });
3376
+ }
3377
+ function sanitizeId(id) {
3378
+ return id.replace(/[/\\:.]/g, "").replace(/\.\./g, "");
3379
+ }
3380
+ function planPath(planId) {
3381
+ const safeId = sanitizeId(planId);
3382
+ if (!safeId) throw new Error("Invalid plan ID");
3383
+ return join7(plansDir, `plan-${safeId}.json`);
3384
+ }
3385
+ async function createPlan(taskDescription, filesNeeded, searchQueries, steps, tokenBudget = { planning: 0, estimated_execution: 0, max_file_reads: 5 }) {
3386
+ const id = randomUUID3().split("-")[0] || "plan";
3387
+ const plan = {
3388
+ id,
3389
+ created_at: Date.now(),
3390
+ task_description: taskDescription,
3391
+ steps: steps.map((s) => ({ ...s, status: "pending" })),
3392
+ token_budget: tokenBudget,
3393
+ files_needed: filesNeeded,
3394
+ search_queries: searchQueries,
3395
+ status: "draft"
3396
+ };
3397
+ writeFileSync5(planPath(id), JSON.stringify(plan, null, 2), "utf-8");
3398
+ return plan;
3399
+ }
3400
+ async function loadPlan(planId) {
3401
+ const path2 = planPath(planId);
3402
+ if (!existsSync7(path2)) return null;
3403
+ try {
3404
+ return JSON.parse(readFileSync8(path2, "utf-8"));
3405
+ } catch {
3406
+ return null;
3407
+ }
3408
+ }
3409
+ async function listPlans() {
3410
+ if (!existsSync7(plansDir)) return [];
3411
+ const files = readdirSync3(plansDir).filter((f) => f.startsWith("plan-") && f.endsWith(".json"));
3412
+ const plans = [];
3413
+ for (const file of files) {
3414
+ try {
3415
+ const plan = JSON.parse(readFileSync8(join7(plansDir, file), "utf-8"));
3416
+ plans.push({
3417
+ id: plan.id,
3418
+ task: plan.task_description,
3419
+ status: plan.status,
3420
+ created: plan.created_at
3421
+ });
3422
+ } catch {
3423
+ }
3424
+ }
3425
+ return plans.sort((a, b) => b.created - a.created);
3426
+ }
3427
+ async function updateStep(planId, stepOrder, status, result) {
3428
+ const plan = await loadPlan(planId);
3429
+ if (!plan) throw new Error(`Plan ${planId} not found`);
3430
+ const step = plan.steps.find((s) => s.order === stepOrder);
3431
+ if (!step) throw new Error(`Step ${stepOrder} not found in plan ${planId}`);
3432
+ step.status = status;
3433
+ if (result !== void 0) {
3434
+ step.result = result;
3435
+ }
3436
+ writeFileSync5(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
3437
+ }
3438
+ async function startExec(planId) {
3439
+ const plan = await loadPlan(planId);
3440
+ if (!plan) throw new Error(`Plan ${planId} not found`);
3441
+ plan.status = "executing";
3442
+ writeFileSync5(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
3443
+ return {
3444
+ maxFileReads: plan.token_budget.max_file_reads,
3445
+ tokenBudget: plan.token_budget.estimated_execution,
3446
+ allowReplan: false
3447
+ };
3448
+ }
3449
+ async function verify(planId) {
3450
+ const plan = await loadPlan(planId);
3451
+ if (!plan) throw new Error(`Plan ${planId} not found`);
3452
+ let stepsCompleted = 0;
3453
+ let stepsFailed = 0;
3454
+ let stepsSkipped = 0;
3455
+ let tokensUsed = 0;
3456
+ const details = [];
3457
+ for (const step of plan.steps) {
3458
+ switch (step.status) {
3459
+ case "completed":
3460
+ stepsCompleted++;
3461
+ tokensUsed += step.estimated_tokens;
3462
+ break;
3463
+ case "failed":
3464
+ stepsFailed++;
3465
+ break;
3466
+ case "skipped":
3467
+ stepsSkipped++;
3468
+ break;
3469
+ default:
3470
+ break;
3471
+ }
3472
+ details.push({
3473
+ step: step.order,
3474
+ action: step.action,
3475
+ target: step.target,
3476
+ status: step.status
3477
+ });
3478
+ }
3479
+ const totalSteps = plan.steps.length;
3480
+ const success = stepsFailed === 0 && stepsCompleted === totalSteps;
3481
+ const hasInProgress = plan.steps.some(
3482
+ (s) => s.status === "pending" || s.status === "in_progress"
3483
+ );
3484
+ if (!hasInProgress) {
3485
+ plan.status = success ? "completed" : "failed";
3486
+ writeFileSync5(planPath(planId), JSON.stringify(plan, null, 2), "utf-8");
3487
+ }
3488
+ return {
3489
+ planId,
3490
+ stepsCompleted,
3491
+ stepsFailed,
3492
+ stepsSkipped,
3493
+ totalSteps,
3494
+ tokensBudgeted: plan.token_budget.estimated_execution,
3495
+ tokensUsed,
3496
+ success,
3497
+ details
3498
+ };
3499
+ }
3500
+ function getPlansDir() {
3501
+ return plansDir;
3502
+ }
3503
+ return {
3504
+ createPlan,
3505
+ loadPlan,
3506
+ listPlans,
3507
+ updateStep,
3508
+ startExec,
3509
+ verify,
3510
+ getPlansDir
3511
+ };
3512
+ }
3513
+ var init_workflow_planner = __esm({
3514
+ "src/core/workflow-planner.ts"() {
3515
+ "use strict";
3516
+ init_esm_shims();
3517
+ }
3518
+ });
3519
+
3520
+ // src/cli/commands/plan.ts
3521
+ var plan_exports = {};
3522
+ __export(plan_exports, {
3523
+ planCommand: () => planCommand,
3524
+ planListCommand: () => planListCommand
3525
+ });
3526
+ import { resolve as resolve6 } from "path";
3527
+ async function planCommand(options) {
3528
+ const projectRoot = resolve6(process.cwd());
3529
+ const planner = createWorkflowPlanner(projectRoot);
3530
+ const steps = [];
3531
+ let order = 1;
3532
+ if (options.searches) {
3533
+ for (const query of options.searches) {
3534
+ steps.push({
3535
+ order: order++,
3536
+ action: "search",
3537
+ target: query,
3538
+ description: `Search for: ${query}`,
3539
+ dependencies: [],
3540
+ estimated_tokens: 500
3541
+ });
3542
+ }
3543
+ }
3544
+ if (options.files) {
3545
+ for (const file of options.files) {
3546
+ steps.push({
3547
+ order: order++,
3548
+ action: "read",
3549
+ target: file,
3550
+ description: `Read file: ${file}`,
3551
+ dependencies: [],
3552
+ estimated_tokens: 1e3
3553
+ });
3554
+ }
3555
+ }
3556
+ steps.push({
3557
+ order: order++,
3558
+ action: "verify",
3559
+ target: "tests",
3560
+ description: "Run tests to verify changes",
3561
+ dependencies: steps.map((s) => s.order),
3562
+ estimated_tokens: 200
3563
+ });
3564
+ const plan = await planner.createPlan(
3565
+ options.task,
3566
+ options.files || [],
3567
+ options.searches || [],
3568
+ steps,
3569
+ {
3570
+ planning: 0,
3571
+ estimated_execution: steps.reduce((sum, s) => sum + s.estimated_tokens, 0),
3572
+ max_file_reads: options.maxReads || 5
3573
+ }
3574
+ );
3575
+ const result = {
3576
+ plan,
3577
+ message: `Plan ${plan.id} created with ${plan.steps.length} steps`
3578
+ };
3579
+ if (options.json) {
3580
+ result.json = JSON.stringify(plan, null, 2);
3581
+ }
3582
+ return result;
3583
+ }
3584
+ async function planListCommand(options) {
3585
+ const projectRoot = resolve6(process.cwd());
3586
+ const planner = createWorkflowPlanner(projectRoot);
3587
+ const plans = await planner.listPlans();
3588
+ const result = { plans };
3589
+ if (options.json) {
3590
+ result.json = JSON.stringify(plans, null, 2);
3591
+ }
3592
+ return result;
3593
+ }
3594
+ var init_plan = __esm({
3595
+ "src/cli/commands/plan.ts"() {
3596
+ "use strict";
3597
+ init_esm_shims();
3598
+ init_workflow_planner();
3599
+ }
3600
+ });
3601
+
3602
+ // src/cli/commands/exec.ts
3603
+ var exec_exports = {};
3604
+ __export(exec_exports, {
3605
+ execCommand: () => execCommand
3606
+ });
3607
+ import { resolve as resolve7 } from "path";
3608
+ async function execCommand(options) {
3609
+ const projectRoot = resolve7(process.cwd());
3610
+ const planner = createWorkflowPlanner(projectRoot);
3611
+ const plan = await planner.loadPlan(options.planId);
3612
+ if (!plan) {
3613
+ throw new Error(`Plan ${options.planId} not found`);
3614
+ }
3615
+ const constraints = await planner.startExec(options.planId);
3616
+ const updatedPlan = await planner.loadPlan(options.planId) || plan;
3617
+ const result = {
3618
+ plan: updatedPlan,
3619
+ constraints,
3620
+ message: `Executing plan ${options.planId}: max ${constraints.maxFileReads} reads, ${constraints.tokenBudget} token budget`
3621
+ };
3622
+ if (options.json) {
3623
+ result.json = JSON.stringify({ plan: updatedPlan, constraints }, null, 2);
3624
+ }
3625
+ return result;
3626
+ }
3627
+ var init_exec = __esm({
3628
+ "src/cli/commands/exec.ts"() {
3629
+ "use strict";
3630
+ init_esm_shims();
3631
+ init_workflow_planner();
3632
+ }
3633
+ });
3634
+
3635
+ // src/cli/commands/verify.ts
3636
+ var verify_exports = {};
3637
+ __export(verify_exports, {
3638
+ verifyCommand: () => verifyCommand
3639
+ });
3640
+ import { resolve as resolve8 } from "path";
3641
+ async function verifyCommand(options) {
3642
+ const projectRoot = resolve8(process.cwd());
3643
+ const planner = createWorkflowPlanner(projectRoot);
3644
+ const plan = await planner.loadPlan(options.planId);
3645
+ if (!plan) {
3646
+ throw new Error(`Plan ${options.planId} not found`);
3647
+ }
3648
+ const verification = await planner.verify(options.planId);
3649
+ const status = verification.success ? "PASSED" : "FAILED";
3650
+ const message = `Plan ${options.planId} verification: ${status} (${verification.stepsCompleted}/${verification.totalSteps} steps completed)`;
3651
+ const result = {
3652
+ verification,
3653
+ message
3654
+ };
3655
+ if (options.json) {
3656
+ result.json = JSON.stringify(verification, null, 2);
3657
+ }
3658
+ return result;
3659
+ }
3660
+ var init_verify = __esm({
3661
+ "src/cli/commands/verify.ts"() {
3662
+ "use strict";
3663
+ init_esm_shims();
3664
+ init_workflow_planner();
3665
+ }
3666
+ });
3667
+
3668
+ // src/core/docs-generator.ts
3669
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync9 } from "fs";
3670
+ import { extname as extname3, join as join8, relative as relative3 } from "path";
3671
+ function detectEntryPoints(projectRoot) {
3672
+ const entries = [];
3673
+ const candidates = [
3674
+ { path: "src/index.ts", desc: "Library API" },
3675
+ { path: "src/cli/index.ts", desc: "CLI entry point" },
3676
+ { path: "src/daemon/index.ts", desc: "Daemon process" },
3677
+ { path: "src/mcp/index.ts", desc: "MCP server" },
3678
+ { path: "src/main.ts", desc: "Main entry" },
3679
+ { path: "src/app.ts", desc: "App entry" },
3680
+ { path: "src/server.ts", desc: "Server entry" },
3681
+ { path: "index.ts", desc: "Root entry" },
3682
+ { path: "index.js", desc: "Root entry" }
3683
+ ];
3684
+ for (const c of candidates) {
3685
+ if (existsSync8(join8(projectRoot, c.path))) {
3686
+ entries.push({ path: c.path, description: c.desc });
3687
+ }
3688
+ }
3689
+ return entries;
3690
+ }
3691
+ function scanModules(dir, projectRoot, ignoreDirs = ["node_modules", "dist", ".git", ".sparn", "coverage"]) {
3692
+ const modules = [];
3693
+ try {
3694
+ const entries = readdirSync4(dir, { withFileTypes: true });
3695
+ for (const entry of entries) {
3696
+ const fullPath = join8(dir, entry.name);
3697
+ if (entry.isDirectory() && !ignoreDirs.includes(entry.name)) {
3698
+ modules.push(...scanModules(fullPath, projectRoot, ignoreDirs));
3699
+ } else if (entry.isFile() && [".ts", ".tsx", ".js", ".jsx"].includes(extname3(entry.name))) {
3700
+ try {
3701
+ const content = readFileSync9(fullPath, "utf-8");
3702
+ modules.push({
3703
+ path: relative3(projectRoot, fullPath).replace(/\\/g, "/"),
3704
+ lines: content.split("\n").length
3705
+ });
3706
+ } catch {
3707
+ }
3708
+ }
3709
+ }
3710
+ } catch {
3711
+ }
3712
+ return modules;
3713
+ }
3714
+ function readPackageJson(projectRoot) {
3715
+ const pkgPath = join8(projectRoot, "package.json");
3716
+ if (!existsSync8(pkgPath)) return null;
3717
+ try {
3718
+ return JSON.parse(readFileSync9(pkgPath, "utf-8"));
3719
+ } catch {
3720
+ return null;
3721
+ }
3722
+ }
3723
+ function detectStack(projectRoot) {
3724
+ const stack = [];
3725
+ const pkg = readPackageJson(projectRoot);
3726
+ if (!pkg) return stack;
3727
+ const allDeps = {
3728
+ ...pkg.dependencies,
3729
+ ...pkg.devDependencies
3730
+ };
3731
+ if (allDeps["typescript"]) stack.push("TypeScript");
3732
+ if (allDeps["vitest"]) stack.push("Vitest");
3733
+ if (allDeps["@biomejs/biome"]) stack.push("Biome");
3734
+ if (allDeps["eslint"]) stack.push("ESLint");
3735
+ if (allDeps["prettier"]) stack.push("Prettier");
3736
+ if (allDeps["react"]) stack.push("React");
3737
+ if (allDeps["next"]) stack.push("Next.js");
3738
+ if (allDeps["express"]) stack.push("Express");
3739
+ if (allDeps["commander"]) stack.push("Commander.js CLI");
3740
+ if (allDeps["better-sqlite3"]) stack.push("SQLite (better-sqlite3)");
3741
+ if (allDeps["zod"]) stack.push("Zod validation");
3742
+ return stack;
3743
+ }
3744
+ function createDocsGenerator(config) {
3745
+ const { projectRoot, includeGraph = true } = config;
3746
+ async function generate(graph) {
3747
+ const lines = [];
3748
+ const pkg = readPackageJson(projectRoot);
3749
+ const projectName = pkg?.name || "Project";
3750
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3751
+ lines.push(`# ${projectName} \u2014 Developer Guide`);
3752
+ lines.push(`<!-- Auto-generated by Sparn v1.3.0 \u2014 ${now} -->`);
3753
+ lines.push("");
3754
+ const stack = detectStack(projectRoot);
3755
+ if (stack.length > 0) {
3756
+ lines.push(`**Stack**: ${stack.join(", ")}`);
3757
+ lines.push("");
3758
+ }
3759
+ if (pkg?.scripts) {
3760
+ lines.push("## Commands");
3761
+ lines.push("");
3762
+ const important = ["build", "dev", "test", "lint", "typecheck", "start"];
3763
+ for (const cmd of important) {
3764
+ if (pkg.scripts[cmd]) {
3765
+ lines.push(`- \`npm run ${cmd}\` \u2014 \`${pkg.scripts[cmd]}\``);
3766
+ }
3767
+ }
3768
+ lines.push("");
3769
+ }
3770
+ const entryPoints = detectEntryPoints(projectRoot);
3771
+ if (entryPoints.length > 0) {
3772
+ lines.push("## Entry Points");
3773
+ lines.push("");
3774
+ for (const ep of entryPoints) {
3775
+ lines.push(`- \`${ep.path}\` \u2014 ${ep.description}`);
3776
+ }
3777
+ lines.push("");
3778
+ }
3779
+ const srcDir = join8(projectRoot, "src");
3780
+ if (existsSync8(srcDir)) {
3781
+ const modules = scanModules(srcDir, projectRoot);
3782
+ const dirGroups = /* @__PURE__ */ new Map();
3783
+ for (const mod of modules) {
3784
+ const parts = mod.path.split("/");
3785
+ const dir = parts.length > 2 ? parts.slice(0, 2).join("/") : parts[0] || "";
3786
+ if (!dirGroups.has(dir)) {
3787
+ dirGroups.set(dir, []);
3788
+ }
3789
+ dirGroups.get(dir)?.push({
3790
+ file: parts[parts.length - 1] || mod.path,
3791
+ lines: mod.lines
3792
+ });
3793
+ }
3794
+ lines.push("## Structure");
3795
+ lines.push("");
3796
+ for (const [dir, files] of dirGroups) {
3797
+ lines.push(`### ${dir}/ (${files.length} files)`);
3798
+ const shown = files.slice(0, 8);
3799
+ for (const f of shown) {
3800
+ lines.push(`- \`${f.file}\` (${f.lines}L)`);
3801
+ }
3802
+ if (files.length > 8) {
3803
+ lines.push(`- ... and ${files.length - 8} more`);
3804
+ }
3805
+ lines.push("");
3806
+ }
3807
+ }
3808
+ if (includeGraph && graph) {
3809
+ try {
3810
+ const analysis = await graph.analyze();
3811
+ lines.push("## Hot Dependencies (most imported)");
3812
+ lines.push("");
3813
+ for (const path2 of analysis.hotPaths.slice(0, 5)) {
3814
+ const node = graph.getNodes().get(path2);
3815
+ const callerCount = node?.callers.length || 0;
3816
+ lines.push(`- \`${path2}\` (imported by ${callerCount} modules)`);
3817
+ }
3818
+ lines.push("");
3819
+ if (analysis.orphans.length > 0) {
3820
+ lines.push(
3821
+ `**Orphaned files** (${analysis.orphans.length}): ${analysis.orphans.slice(0, 3).join(", ")}${analysis.orphans.length > 3 ? "..." : ""}`
3822
+ );
3823
+ lines.push("");
3824
+ }
3825
+ lines.push(
3826
+ `**Total tokens**: ${analysis.totalTokens.toLocaleString()} | **Hot path tokens**: ${analysis.optimizedTokens.toLocaleString()}`
3827
+ );
3828
+ lines.push("");
3829
+ } catch {
3830
+ }
3831
+ }
3832
+ const sparnConfigPath = join8(projectRoot, ".sparn", "config.yaml");
3833
+ if (existsSync8(sparnConfigPath)) {
3834
+ lines.push("## Sparn Optimization");
3835
+ lines.push("");
3836
+ lines.push("Sparn is active in this project. Key features:");
3837
+ lines.push("- Context optimization (60-70% token reduction)");
3838
+ lines.push("- Use `sparn search` before reading files");
3839
+ lines.push("- Use `sparn graph` to understand dependencies");
3840
+ lines.push("- Use `sparn plan` for planning, `sparn exec` for execution");
3841
+ lines.push("");
3842
+ }
3843
+ if (config.customSections) {
3844
+ for (const section of config.customSections) {
3845
+ lines.push(section);
3846
+ lines.push("");
3847
+ }
3848
+ }
3849
+ return lines.join("\n");
3850
+ }
3851
+ return { generate };
3852
+ }
3853
+ var init_docs_generator = __esm({
3854
+ "src/core/docs-generator.ts"() {
3855
+ "use strict";
3856
+ init_esm_shims();
3857
+ }
3858
+ });
3859
+
3860
+ // src/cli/commands/docs.ts
3861
+ var docs_exports = {};
3862
+ __export(docs_exports, {
3863
+ docsCommand: () => docsCommand
3864
+ });
3865
+ import { writeFileSync as writeFileSync6 } from "fs";
3866
+ import { resolve as resolve9 } from "path";
3867
+ async function docsCommand(options) {
3868
+ const projectRoot = resolve9(process.cwd());
3869
+ const generator = createDocsGenerator({
3870
+ projectRoot,
3871
+ includeGraph: options.includeGraph !== false
3872
+ });
3873
+ let graph;
3874
+ if (options.includeGraph !== false) {
3875
+ graph = createDependencyGraph({ projectRoot });
3876
+ await graph.build();
3877
+ }
3878
+ const content = await generator.generate(graph);
3879
+ if (options.json) {
3880
+ return {
3881
+ content,
3882
+ message: `CLAUDE.md generated (${content.split("\n").length} lines)`
3883
+ };
3884
+ }
3885
+ const outputPath = options.output || resolve9(projectRoot, "CLAUDE.md");
3886
+ writeFileSync6(outputPath, content, "utf-8");
3887
+ return {
3888
+ content,
3889
+ outputPath,
3890
+ message: `CLAUDE.md generated at ${outputPath} (${content.split("\n").length} lines)`
3891
+ };
3892
+ }
3893
+ var init_docs = __esm({
3894
+ "src/cli/commands/docs.ts"() {
3895
+ "use strict";
3896
+ init_esm_shims();
3897
+ init_dependency_graph();
3898
+ init_docs_generator();
3899
+ }
3900
+ });
3901
+
3902
+ // src/core/debt-tracker.ts
3903
+ import { randomUUID as randomUUID4 } from "crypto";
3904
+ import Database3 from "better-sqlite3";
3905
+ function createDebtTracker(dbPath) {
3906
+ const db = new Database3(dbPath);
3907
+ db.pragma("journal_mode = WAL");
3908
+ db.exec(`
3909
+ CREATE TABLE IF NOT EXISTS tech_debt (
3910
+ id TEXT PRIMARY KEY NOT NULL,
3911
+ description TEXT NOT NULL,
3912
+ created_at INTEGER NOT NULL,
3913
+ repayment_date INTEGER NOT NULL,
3914
+ severity TEXT NOT NULL CHECK(severity IN ('P0', 'P1', 'P2')),
3915
+ token_cost INTEGER NOT NULL DEFAULT 0,
3916
+ files_affected TEXT NOT NULL DEFAULT '[]',
3917
+ status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'in_progress', 'resolved')),
3918
+ resolution_tokens INTEGER,
3919
+ resolved_at INTEGER
3920
+ );
3921
+ `);
3922
+ db.exec(`
3923
+ CREATE INDEX IF NOT EXISTS idx_debt_status ON tech_debt(status);
3924
+ CREATE INDEX IF NOT EXISTS idx_debt_severity ON tech_debt(severity);
3925
+ CREATE INDEX IF NOT EXISTS idx_debt_repayment ON tech_debt(repayment_date);
3926
+ `);
3927
+ const insertStmt = db.prepare(`
3928
+ INSERT INTO tech_debt (id, description, created_at, repayment_date, severity, token_cost, files_affected, status)
3929
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'open')
3930
+ `);
3931
+ const getStmt = db.prepare("SELECT * FROM tech_debt WHERE id = ?");
3932
+ function rowToDebt(row) {
3933
+ return {
3934
+ id: row["id"],
3935
+ description: row["description"],
3936
+ created_at: row["created_at"],
3937
+ repayment_date: row["repayment_date"],
3938
+ severity: row["severity"],
3939
+ token_cost: row["token_cost"],
3940
+ files_affected: JSON.parse(row["files_affected"] || "[]"),
3941
+ status: row["status"],
3942
+ resolution_tokens: row["resolution_tokens"],
3943
+ resolved_at: row["resolved_at"]
3944
+ };
3945
+ }
3946
+ async function add(debt) {
3947
+ const id = randomUUID4().split("-")[0] || "debt";
3948
+ const created_at = Date.now();
3949
+ insertStmt.run(
3950
+ id,
3951
+ debt.description,
3952
+ created_at,
3953
+ debt.repayment_date,
3954
+ debt.severity,
3955
+ debt.token_cost,
3956
+ JSON.stringify(debt.files_affected)
3957
+ );
3958
+ return {
3959
+ id,
3960
+ created_at,
3961
+ status: "open",
3962
+ description: debt.description,
3963
+ repayment_date: debt.repayment_date,
3964
+ severity: debt.severity,
3965
+ token_cost: debt.token_cost,
3966
+ files_affected: debt.files_affected
3967
+ };
3968
+ }
3969
+ async function list(filter = {}) {
3970
+ let sql = "SELECT * FROM tech_debt WHERE 1=1";
3971
+ const params = [];
3972
+ if (filter.status) {
3973
+ sql += " AND status = ?";
3974
+ params.push(filter.status);
3975
+ }
3976
+ if (filter.severity) {
3977
+ sql += " AND severity = ?";
3978
+ params.push(filter.severity);
3979
+ }
3980
+ if (filter.overdue) {
3981
+ sql += " AND repayment_date < ? AND status != ?";
3982
+ params.push(Date.now());
3983
+ params.push("resolved");
3984
+ }
3985
+ sql += " ORDER BY severity ASC, repayment_date ASC";
3986
+ const rows = db.prepare(sql).all(...params);
3987
+ return rows.map(rowToDebt);
3988
+ }
3989
+ async function get(id) {
3990
+ const row = getStmt.get(id);
3991
+ if (!row) return null;
3992
+ return rowToDebt(row);
3993
+ }
3994
+ async function resolve12(id, resolutionTokens) {
3995
+ const result = db.prepare(
3996
+ "UPDATE tech_debt SET status = ?, resolution_tokens = ?, resolved_at = ? WHERE id = ?"
3997
+ ).run("resolved", resolutionTokens ?? null, Date.now(), id);
3998
+ if (result.changes === 0) {
3999
+ throw new Error(`Debt not found: ${id}`);
4000
+ }
4001
+ }
4002
+ async function start(id) {
4003
+ const result = db.prepare("UPDATE tech_debt SET status = ? WHERE id = ?").run("in_progress", id);
4004
+ if (result.changes === 0) {
4005
+ throw new Error(`Debt not found: ${id}`);
4006
+ }
4007
+ }
4008
+ async function remove(id) {
4009
+ db.prepare("DELETE FROM tech_debt WHERE id = ?").run(id);
4010
+ }
4011
+ async function stats() {
4012
+ const all = db.prepare("SELECT * FROM tech_debt").all();
4013
+ const debts = all.map(rowToDebt);
4014
+ const now = Date.now();
4015
+ const open = debts.filter((d) => d.status === "open");
4016
+ const inProgress = debts.filter((d) => d.status === "in_progress");
4017
+ const resolved = debts.filter((d) => d.status === "resolved");
4018
+ const overdue = debts.filter((d) => d.status !== "resolved" && d.repayment_date < now);
4019
+ const totalTokenCost = debts.reduce((sum, d) => sum + d.token_cost, 0);
4020
+ const resolvedTokenCost = resolved.reduce(
4021
+ (sum, d) => sum + (d.resolution_tokens || d.token_cost),
4022
+ 0
4023
+ );
4024
+ const resolvedOnTime = resolved.filter(
4025
+ (d) => d.resolved_at && d.resolved_at <= d.repayment_date
4026
+ ).length;
4027
+ const repaymentRate = resolved.length > 0 ? resolvedOnTime / resolved.length : 0;
4028
+ return {
4029
+ total: debts.length,
4030
+ open: open.length,
4031
+ in_progress: inProgress.length,
4032
+ resolved: resolved.length,
4033
+ overdue: overdue.length,
4034
+ totalTokenCost,
4035
+ resolvedTokenCost,
4036
+ repaymentRate
4037
+ };
4038
+ }
4039
+ async function getCritical() {
4040
+ const rows = db.prepare(
4041
+ "SELECT * FROM tech_debt WHERE severity = 'P0' AND status != 'resolved' ORDER BY repayment_date ASC"
4042
+ ).all();
4043
+ return rows.map(rowToDebt);
4044
+ }
4045
+ async function close() {
4046
+ db.close();
4047
+ }
4048
+ return {
4049
+ add,
4050
+ list,
4051
+ get,
4052
+ resolve: resolve12,
4053
+ start,
4054
+ remove,
4055
+ stats,
4056
+ getCritical,
4057
+ close
4058
+ };
4059
+ }
4060
+ var init_debt_tracker = __esm({
4061
+ "src/core/debt-tracker.ts"() {
4062
+ "use strict";
4063
+ init_esm_shims();
4064
+ }
4065
+ });
4066
+
4067
+ // src/cli/commands/debt.ts
4068
+ var debt_exports = {};
4069
+ __export(debt_exports, {
4070
+ debtCommand: () => debtCommand
4071
+ });
4072
+ import { resolve as resolve10 } from "path";
4073
+ async function debtCommand(options) {
4074
+ const projectRoot = resolve10(process.cwd());
4075
+ const dbPath = resolve10(projectRoot, ".sparn", "memory.db");
4076
+ const tracker = createDebtTracker(dbPath);
4077
+ try {
4078
+ switch (options.subcommand) {
4079
+ case "add": {
4080
+ if (!options.description) {
4081
+ throw new Error("Description is required");
4082
+ }
4083
+ const severity = options.severity || "P1";
4084
+ if (!["P0", "P1", "P2"].includes(severity)) {
4085
+ throw new Error("Severity must be P0, P1, or P2");
4086
+ }
4087
+ let repaymentDate;
4088
+ if (options.due) {
4089
+ repaymentDate = new Date(options.due).getTime();
4090
+ if (Number.isNaN(repaymentDate)) {
4091
+ throw new Error(`Invalid date: ${options.due}`);
4092
+ }
4093
+ } else {
4094
+ repaymentDate = Date.now() + 14 * 24 * 60 * 60 * 1e3;
4095
+ }
4096
+ const debt = await tracker.add({
4097
+ description: options.description,
4098
+ repayment_date: repaymentDate,
4099
+ severity,
4100
+ token_cost: options.tokenCost || 0,
4101
+ files_affected: options.files || []
4102
+ });
4103
+ const result = {
4104
+ debt,
4105
+ message: `Debt ${debt.id} added (${severity}): ${options.description}`
4106
+ };
4107
+ if (options.json) {
4108
+ result.json = JSON.stringify(debt, null, 2);
4109
+ }
4110
+ return result;
4111
+ }
4112
+ case "list": {
4113
+ const debts = await tracker.list({
4114
+ overdue: options.overdue
4115
+ });
4116
+ const result = {
4117
+ debts,
4118
+ message: `${debts.length} debt item(s)`
4119
+ };
4120
+ if (options.json) {
4121
+ result.json = JSON.stringify(debts, null, 2);
4122
+ }
4123
+ return result;
4124
+ }
4125
+ case "resolve": {
4126
+ if (!options.id) {
4127
+ throw new Error("Debt ID is required");
4128
+ }
4129
+ await tracker.resolve(options.id, options.tokenCost);
4130
+ return {
4131
+ message: `Debt ${options.id} resolved`
4132
+ };
4133
+ }
4134
+ case "start": {
4135
+ if (!options.id) {
4136
+ throw new Error("Debt ID is required");
4137
+ }
4138
+ await tracker.start(options.id);
4139
+ return {
4140
+ message: `Debt ${options.id} started`
4141
+ };
4142
+ }
4143
+ case "stats": {
4144
+ const stats = await tracker.stats();
4145
+ const result = {
4146
+ stats,
4147
+ message: `${stats.total} total, ${stats.open} open, ${stats.overdue} overdue, ${(stats.repaymentRate * 100).toFixed(0)}% on-time rate`
4148
+ };
4149
+ if (options.json) {
4150
+ result.json = JSON.stringify(stats, null, 2);
4151
+ }
4152
+ return result;
4153
+ }
4154
+ default:
4155
+ throw new Error(`Unknown subcommand: ${options.subcommand}`);
4156
+ }
4157
+ } finally {
4158
+ await tracker.close();
4159
+ }
4160
+ }
4161
+ var init_debt = __esm({
4162
+ "src/cli/commands/debt.ts"() {
4163
+ "use strict";
4164
+ init_esm_shims();
4165
+ init_debt_tracker();
4166
+ }
4167
+ });
4168
+
4169
+ // src/cli/dashboard/theme.ts
4170
+ var theme;
4171
+ var init_theme = __esm({
4172
+ "src/cli/dashboard/theme.ts"() {
4173
+ "use strict";
4174
+ init_esm_shims();
4175
+ theme = {
4176
+ neuralCyan: "#00D4AA",
4177
+ synapseViolet: "#7B61FF",
4178
+ errorRed: "#FF6B6B",
4179
+ brainPink: "#FF6B9D",
4180
+ dimGray: "#555555",
4181
+ white: "#FFFFFF"
4182
+ };
4183
+ }
4184
+ });
4185
+
4186
+ // src/cli/dashboard/components/command-input.tsx
4187
+ import { Box, Text } from "ink";
4188
+ import TextInput from "ink-text-input";
4189
+ import { useState } from "react";
4190
+ import { jsx, jsxs } from "react/jsx-runtime";
4191
+ function CommandInput({
4192
+ onSubmit,
4193
+ isRunning,
4194
+ runningCommand,
4195
+ active
4196
+ }) {
4197
+ const [value, setValue] = useState("");
4198
+ const handleSubmit = (input2) => {
4199
+ if (input2.trim().length === 0) return;
4200
+ onSubmit(input2.trim());
4201
+ setValue("");
4202
+ };
4203
+ if (isRunning) {
4204
+ return /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsxs(Text, { color: theme.synapseViolet, children: [
4205
+ "Running ",
4206
+ runningCommand || "command",
4207
+ "..."
4208
+ ] }) });
4209
+ }
4210
+ if (!active) {
4211
+ return /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: theme.dimGray, children: "Press : to enter command mode" }) });
4212
+ }
4213
+ return /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
4214
+ /* @__PURE__ */ jsxs(Text, { color: theme.neuralCyan, children: [
4215
+ "\u276F",
4216
+ " "
4217
+ ] }),
4218
+ /* @__PURE__ */ jsx(
4219
+ TextInput,
4220
+ {
4221
+ value,
4222
+ onChange: setValue,
4223
+ onSubmit: handleSubmit,
4224
+ focus: active,
4225
+ showCursor: true
4226
+ }
4227
+ ),
4228
+ /* @__PURE__ */ jsx(Text, { color: theme.dimGray, children: " [Esc: monitor mode]" })
4229
+ ] });
4230
+ }
4231
+ var init_command_input = __esm({
4232
+ "src/cli/dashboard/components/command-input.tsx"() {
4233
+ "use strict";
4234
+ init_esm_shims();
4235
+ init_theme();
4236
+ }
4237
+ });
4238
+
4239
+ // src/cli/dashboard/components/output-panel.tsx
4240
+ import { Box as Box2, Text as Text2 } from "ink";
4241
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
4242
+ function OutputPanel({
4243
+ lines,
4244
+ scrollOffset,
4245
+ focused,
4246
+ maxVisibleLines = 8
4247
+ }) {
4248
+ const borderColor = focused ? theme.neuralCyan : theme.dimGray;
4249
+ if (lines.length === 0) {
4250
+ return /* @__PURE__ */ jsxs2(
4251
+ Box2,
4252
+ {
4253
+ flexDirection: "column",
4254
+ borderStyle: "round",
4255
+ borderColor,
4256
+ paddingX: 1,
4257
+ width: "100%",
4258
+ children: [
4259
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.dimGray, children: "Output" }),
4260
+ /* @__PURE__ */ jsx2(Text2, { color: theme.dimGray, children: "Type : then a command. Try help for available commands." })
4261
+ ]
4262
+ }
4263
+ );
4264
+ }
4265
+ const visible = lines.slice(scrollOffset, scrollOffset + maxVisibleLines);
4266
+ const hasMore = lines.length > maxVisibleLines;
4267
+ return /* @__PURE__ */ jsxs2(
4268
+ Box2,
4269
+ {
4270
+ flexDirection: "column",
4271
+ borderStyle: "round",
4272
+ borderColor,
4273
+ paddingX: 1,
4274
+ width: "100%",
4275
+ children: [
4276
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: theme.dimGray, children: "Output" }),
4277
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: visible.map((line, i) => /* @__PURE__ */ jsx2(Text2, { color: line.color, children: line.text }, `${scrollOffset + i}`)) }),
4278
+ hasMore && /* @__PURE__ */ jsxs2(Text2, { color: theme.dimGray, children: [
4279
+ "[",
4280
+ scrollOffset + 1,
4281
+ "-",
4282
+ Math.min(scrollOffset + maxVisibleLines, lines.length),
4283
+ "/",
4284
+ lines.length,
4285
+ "]",
4286
+ focused ? " \u2191\u2193 scroll" : ""
4287
+ ] })
4288
+ ]
4289
+ }
4290
+ );
4291
+ }
4292
+ var init_output_panel = __esm({
4293
+ "src/cli/dashboard/components/output-panel.tsx"() {
4294
+ "use strict";
4295
+ init_esm_shims();
4296
+ init_theme();
4297
+ }
4298
+ });
4299
+
4300
+ // src/cli/dashboard/command-parser.ts
4301
+ function tokenize2(input2) {
4302
+ const tokens = [];
4303
+ let current = "";
4304
+ let inQuote = null;
4305
+ for (const ch of input2) {
4306
+ if (inQuote) {
4307
+ if (ch === inQuote) {
4308
+ inQuote = null;
4309
+ } else {
4310
+ current += ch;
4311
+ }
4312
+ } else if (ch === '"' || ch === "'") {
4313
+ inQuote = ch;
4314
+ } else if (ch === " " || ch === " ") {
4315
+ if (current.length > 0) {
4316
+ tokens.push(current);
4317
+ current = "";
4318
+ }
4319
+ } else {
4320
+ current += ch;
4321
+ }
4322
+ }
4323
+ if (current.length > 0) {
4324
+ tokens.push(current);
4325
+ }
4326
+ return tokens;
4327
+ }
4328
+ function parseCommand(input2) {
4329
+ const trimmed = input2.trim();
4330
+ if (trimmed.length === 0) return null;
4331
+ const tokens = tokenize2(trimmed);
4332
+ const first = tokens[0];
4333
+ if (!first) return null;
4334
+ const name = first.toLowerCase();
4335
+ const positionals = [];
4336
+ const flags = {};
4337
+ let i = 1;
4338
+ while (i < tokens.length) {
4339
+ const token = tokens[i];
4340
+ if (!token) {
4341
+ i++;
4342
+ continue;
4343
+ }
4344
+ if (token.startsWith("--")) {
4345
+ const key = token.slice(2);
4346
+ const next = tokens[i + 1];
4347
+ if (next !== void 0 && !next.startsWith("-")) {
4348
+ flags[key] = next;
4349
+ i += 2;
4350
+ } else {
4351
+ flags[key] = true;
4352
+ i++;
4353
+ }
4354
+ } else if (token.startsWith("-") && token.length === 2) {
4355
+ const key = token.slice(1);
4356
+ const next = tokens[i + 1];
4357
+ if (next !== void 0 && !next.startsWith("-")) {
4358
+ flags[key] = next;
4359
+ i += 2;
4360
+ } else {
4361
+ flags[key] = true;
4362
+ i++;
4363
+ }
4364
+ } else {
4365
+ positionals.push(token);
4366
+ i++;
4367
+ }
4368
+ }
4369
+ return { name, positionals, flags };
4370
+ }
4371
+ var init_command_parser = __esm({
4372
+ "src/cli/dashboard/command-parser.ts"() {
4373
+ "use strict";
4374
+ init_esm_shims();
4375
+ }
4376
+ });
4377
+
4378
+ // src/cli/dashboard/formatters.ts
4379
+ function formatOptimize(result) {
4380
+ const pct = (result.reduction * 100).toFixed(1);
4381
+ return [
4382
+ { text: `\u2713 Optimization complete in ${result.durationMs}ms`, color: theme.neuralCyan },
4383
+ {
4384
+ text: ` Tokens: ${result.tokensBefore} \u2192 ${result.tokensAfter} (${pct}% reduction)`
4385
+ },
4386
+ {
4387
+ text: ` Entries: processed=${result.entriesProcessed} kept=${result.entriesKept}`
4388
+ },
4389
+ ...result.outputFile ? [{ text: ` Output: ${result.outputFile}`, color: theme.dimGray }] : []
4390
+ ];
4391
+ }
4392
+ function formatStats(result) {
4393
+ if (result.resetConfirmed) {
4394
+ return [{ text: "\u2713 Statistics reset", color: theme.neuralCyan }];
4395
+ }
4396
+ const avgPct = (result.averageReduction * 100).toFixed(1);
4397
+ const lines = [
4398
+ { text: "Optimization Statistics", color: theme.neuralCyan },
4399
+ { text: ` Total commands: ${result.totalCommands}` },
4400
+ { text: ` Tokens saved: ${result.totalTokensSaved}` },
4401
+ { text: ` Avg reduction: ${avgPct}%` }
4402
+ ];
4403
+ if (result.graph) {
4404
+ for (const line of result.graph.split("\n")) {
4405
+ lines.push({ text: line, color: theme.dimGray });
4406
+ }
4407
+ }
4408
+ return lines;
4409
+ }
4410
+ function formatConsolidate(result) {
4411
+ const pct = (result.compressionRatio * 100).toFixed(1);
4412
+ return [
4413
+ { text: `\u2713 Consolidation complete in ${result.durationMs}ms`, color: theme.neuralCyan },
4414
+ { text: ` Entries: ${result.entriesBefore} \u2192 ${result.entriesAfter}` },
4415
+ { text: ` Decayed removed: ${result.decayedRemoved}` },
4416
+ { text: ` Duplicates removed: ${result.duplicatesRemoved}` },
4417
+ { text: ` Compression: ${pct}%` }
4418
+ ];
4419
+ }
4420
+ function formatSearch(result) {
4421
+ if (result.indexStats) {
4422
+ const s = result.indexStats;
4423
+ return [
4424
+ {
4425
+ text: `\u2713 Indexed ${s.filesIndexed} files (${s.totalLines} lines) in ${s.duration}ms`,
4426
+ color: theme.neuralCyan
4427
+ }
4428
+ ];
4429
+ }
4430
+ if (!result.results || result.results.length === 0) {
4431
+ return [{ text: result.message || "No results found", color: theme.dimGray }];
4432
+ }
4433
+ const lines = [
4434
+ { text: `${result.results.length} result(s):`, color: theme.neuralCyan }
4435
+ ];
4436
+ for (const r of result.results) {
4437
+ lines.push({
4438
+ text: ` ${r.filePath}:${r.lineNumber} (score: ${r.score.toFixed(2)})`,
4439
+ color: theme.synapseViolet
4440
+ });
4441
+ lines.push({ text: ` ${r.content.trim()}` });
4442
+ }
4443
+ return lines;
4444
+ }
4445
+ function formatGraph(result) {
4446
+ const a = result.analysis;
4447
+ const lines = [
4448
+ { text: `Graph: ${result.nodeCount} nodes`, color: theme.neuralCyan },
4449
+ { text: ` Tokens: ${a.totalTokens} (optimized: ${a.optimizedTokens})` }
4450
+ ];
4451
+ if (a.hotPaths.length > 0) {
4452
+ lines.push({
4453
+ text: ` Hot paths: ${a.hotPaths.slice(0, 5).join(", ")}`,
4454
+ color: theme.brainPink
4455
+ });
4456
+ }
4457
+ if (a.orphans.length > 0) {
4458
+ lines.push({ text: ` Orphans: ${a.orphans.length}`, color: theme.dimGray });
4459
+ }
4460
+ return lines;
4461
+ }
4462
+ function formatPlan(result) {
4463
+ return [{ text: `\u2713 ${result.message}`, color: theme.neuralCyan }];
4464
+ }
4465
+ function formatPlanList(result) {
4466
+ if (result.plans.length === 0) {
4467
+ return [{ text: "No plans found", color: theme.dimGray }];
4468
+ }
4469
+ const lines = [
4470
+ { text: `${result.plans.length} plan(s):`, color: theme.neuralCyan }
4471
+ ];
4472
+ for (const p of result.plans) {
4473
+ const statusColor = p.status === "completed" ? theme.neuralCyan : theme.synapseViolet;
4474
+ lines.push({ text: ` ${p.id} [${p.status}] ${p.task}`, color: statusColor });
4475
+ }
4476
+ return lines;
4477
+ }
4478
+ function formatExec(result) {
4479
+ return [
4480
+ { text: `\u2713 ${result.message}`, color: theme.neuralCyan },
4481
+ {
4482
+ text: ` Max reads: ${result.constraints.maxFileReads}, Budget: ${result.constraints.tokenBudget} tokens`
4483
+ }
4484
+ ];
4485
+ }
4486
+ function formatVerify(result) {
4487
+ const v = result.verification;
4488
+ const icon = v.success ? "\u2713" : "\u2717";
4489
+ const color = v.success ? theme.neuralCyan : theme.errorRed;
4490
+ return [
4491
+ { text: `${icon} ${result.message}`, color },
4492
+ { text: ` Steps: ${v.stepsCompleted}/${v.totalSteps} completed` }
4493
+ ];
4494
+ }
4495
+ function formatDocs(result) {
4496
+ return [{ text: `\u2713 ${result.message}`, color: theme.neuralCyan }];
4497
+ }
4498
+ function formatDebt(result) {
4499
+ if (result.stats) {
4500
+ const s = result.stats;
4501
+ return [
4502
+ { text: "Debt Stats", color: theme.neuralCyan },
4503
+ { text: ` Total: ${s.total}, Open: ${s.open}, Overdue: ${s.overdue}` },
4504
+ { text: ` Repayment rate: ${(s.repaymentRate * 100).toFixed(0)}%` }
4505
+ ];
4506
+ }
4507
+ if (result.debts) {
4508
+ if (result.debts.length === 0) {
4509
+ return [{ text: "No debt items", color: theme.dimGray }];
4510
+ }
4511
+ const lines = [
4512
+ { text: `${result.debts.length} debt item(s):`, color: theme.neuralCyan }
4513
+ ];
4514
+ for (const d of result.debts) {
4515
+ const sevColor = d.severity === "P0" ? theme.errorRed : d.severity === "P1" ? theme.synapseViolet : theme.dimGray;
4516
+ const desc = d.description.length > 40 ? `${d.description.substring(0, 37)}...` : d.description;
4517
+ lines.push({
4518
+ text: ` [${d.severity}] ${d.id.slice(0, 8)} ${desc} (${d.status})`,
4519
+ color: sevColor
4520
+ });
4521
+ }
4522
+ return lines;
4523
+ }
4524
+ return [{ text: `\u2713 ${result.message}`, color: theme.neuralCyan }];
4525
+ }
4526
+ function formatError(err) {
4527
+ return [{ text: `Error: ${err.message}`, color: theme.errorRed }];
4528
+ }
4529
+ function formatHelp() {
4530
+ return [
4531
+ { text: "Available commands:", color: theme.neuralCyan },
4532
+ { text: " optimize --input <text> [--input-file <path>] [--output-file <path>] [--dry-run]" },
4533
+ { text: " stats [--graph] [--reset --confirm-reset]" },
4534
+ { text: " consolidate" },
4535
+ { text: " search <query> [--glob <pattern>] [--max-results <n>]" },
4536
+ { text: " search init | search refresh" },
4537
+ { text: " graph [--entry <file>] [--depth <n>] [--focus <file>]" },
4538
+ { text: " plan <task> [--files <f1,f2>] [--searches <q1,q2>]" },
4539
+ { text: " plan list" },
4540
+ { text: " exec <planId>" },
4541
+ { text: " verify <planId>" },
4542
+ { text: " docs [--output <path>] [--no-graph]" },
4543
+ { text: " debt add <desc> [--severity P0|P1|P2] [--due <date>] [--token-cost <n>]" },
4544
+ { text: " debt list [--overdue]" },
4545
+ { text: " debt resolve <id> [--token-cost <n>]" },
4546
+ { text: " debt start <id>" },
4547
+ { text: " debt stats" },
4548
+ { text: " clear Clear output" },
4549
+ { text: " help Show this help" },
4550
+ { text: "" },
4551
+ { text: "Keybinds:", color: theme.neuralCyan },
4552
+ { text: " : Enter command mode" },
4553
+ { text: " Escape Exit command mode" },
4554
+ { text: " Tab Cycle panel focus" },
4555
+ { text: " q Quit (in monitor mode)" }
4556
+ ];
4557
+ }
4558
+ var init_formatters = __esm({
4559
+ "src/cli/dashboard/formatters.ts"() {
4560
+ "use strict";
4561
+ init_esm_shims();
4562
+ init_theme();
4563
+ }
4564
+ });
4565
+
4566
+ // src/cli/dashboard/hooks/use-command-executor.ts
4567
+ import { useCallback, useState as useState2 } from "react";
4568
+ function useCommandExecutor({
4569
+ getMemory,
4570
+ forceRefresh,
4571
+ dbPath,
4572
+ projectRoot
4573
+ }) {
4574
+ const [outputLines, setOutputLines] = useState2([]);
4575
+ const [isRunning, setIsRunning] = useState2(false);
4576
+ const [runningCommand, setRunningCommand] = useState2();
4577
+ const appendLines = useCallback((lines) => {
4578
+ setOutputLines((prev) => {
4579
+ const next = [...prev, ...lines];
4580
+ return next.length > MAX_OUTPUT_LINES ? next.slice(-MAX_OUTPUT_LINES) : next;
4581
+ });
4582
+ }, []);
4583
+ const clearOutput = useCallback(() => {
4584
+ setOutputLines([]);
4585
+ }, []);
4586
+ const execute = useCallback(
4587
+ (rawInput) => {
4588
+ const parsed = parseCommand(rawInput);
4589
+ if (!parsed) return;
4590
+ if (parsed.name === "clear") {
4591
+ clearOutput();
4592
+ return;
4593
+ }
4594
+ if (parsed.name === "help") {
4595
+ appendLines(formatHelp());
4596
+ return;
4597
+ }
4598
+ setIsRunning(true);
4599
+ setRunningCommand(parsed.name);
4600
+ void (async () => {
4601
+ try {
4602
+ const lines = await executeCommand2(parsed.name, parsed.positionals, parsed.flags, {
4603
+ getMemory,
4604
+ dbPath,
4605
+ projectRoot
4606
+ });
4607
+ appendLines(lines);
4608
+ forceRefresh();
4609
+ } catch (err) {
4610
+ appendLines(formatError(err instanceof Error ? err : new Error(String(err))));
4611
+ } finally {
4612
+ setIsRunning(false);
4613
+ setRunningCommand(void 0);
4614
+ }
4615
+ })();
4616
+ },
4617
+ [getMemory, forceRefresh, dbPath, projectRoot, appendLines, clearOutput]
4618
+ );
4619
+ return { execute, outputLines, isRunning, runningCommand, clearOutput };
4620
+ }
4621
+ async function executeCommand2(name, positionals, flags, ctx) {
4622
+ switch (name) {
4623
+ case "optimize": {
4624
+ const memory = ctx.getMemory();
4625
+ if (!memory) throw new Error("Memory not initialized");
4626
+ const { optimizeCommand: optimizeCommand2 } = await Promise.resolve().then(() => (init_optimize(), optimize_exports));
4627
+ const result = await optimizeCommand2({
4628
+ memory,
4629
+ input: typeof flags["input"] === "string" ? flags["input"] : void 0,
4630
+ inputFile: typeof flags["input-file"] === "string" ? flags["input-file"] : void 0,
4631
+ outputFile: typeof flags["output-file"] === "string" ? flags["output-file"] : void 0,
4632
+ dryRun: flags["dry-run"] === true,
4633
+ verbose: flags["verbose"] === true || flags["v"] === true
4634
+ });
4635
+ return formatOptimize(result);
4636
+ }
4637
+ case "stats": {
4638
+ const memory = ctx.getMemory();
4639
+ if (!memory) throw new Error("Memory not initialized");
4640
+ const { statsCommand: statsCommand2 } = await Promise.resolve().then(() => (init_stats(), stats_exports));
4641
+ const result = await statsCommand2({
4642
+ memory,
4643
+ graph: flags["graph"] === true || flags["g"] === true,
4644
+ reset: flags["reset"] === true,
4645
+ confirmReset: flags["confirm-reset"] === true
4646
+ });
4647
+ return formatStats(result);
4648
+ }
4649
+ case "consolidate": {
4650
+ const memory = ctx.getMemory();
4651
+ if (!memory) throw new Error("Memory not initialized");
4652
+ const { consolidateCommand: consolidateCommand2 } = await Promise.resolve().then(() => (init_consolidate(), consolidate_exports));
4653
+ const result = await consolidateCommand2({ memory });
4654
+ return formatConsolidate(result);
4655
+ }
4656
+ case "search": {
4657
+ const { searchCommand: searchCommand2 } = await Promise.resolve().then(() => (init_search(), search_exports));
4658
+ const subcommand = positionals[0] === "init" || positionals[0] === "refresh" ? positionals[0] : void 0;
4659
+ const query = subcommand ? void 0 : positionals.join(" ") || void 0;
4660
+ const result = await searchCommand2({
4661
+ query,
4662
+ subcommand,
4663
+ glob: typeof flags["glob"] === "string" ? flags["glob"] : void 0,
4664
+ maxResults: typeof flags["max-results"] === "string" ? Number.parseInt(flags["max-results"], 10) : void 0
4665
+ });
4666
+ return formatSearch(result);
4667
+ }
4668
+ case "graph": {
4669
+ const { graphCommand: graphCommand2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
4670
+ const result = await graphCommand2({
4671
+ entry: typeof flags["entry"] === "string" ? flags["entry"] : void 0,
4672
+ depth: typeof flags["depth"] === "string" ? Number.parseInt(flags["depth"], 10) : void 0,
4673
+ focus: typeof flags["focus"] === "string" ? flags["focus"] : void 0
4674
+ });
4675
+ return formatGraph(result);
4676
+ }
4677
+ case "plan": {
4678
+ if (positionals[0] === "list") {
4679
+ const { planListCommand: planListCommand2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
4680
+ const result2 = await planListCommand2({});
4681
+ return formatPlanList(result2);
4682
+ }
4683
+ const { planCommand: planCommand2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
4684
+ const task = positionals.join(" ");
4685
+ if (!task) throw new Error("Task description required: plan <task>");
4686
+ const files = typeof flags["files"] === "string" ? flags["files"].split(",") : void 0;
4687
+ const searches = typeof flags["searches"] === "string" ? flags["searches"].split(",") : void 0;
4688
+ const result = await planCommand2({ task, files, searches });
4689
+ return formatPlan(result);
4690
+ }
4691
+ case "exec": {
4692
+ const planId = positionals[0];
4693
+ if (!planId) throw new Error("Plan ID required: exec <planId>");
4694
+ const { execCommand: execCommand2 } = await Promise.resolve().then(() => (init_exec(), exec_exports));
4695
+ const result = await execCommand2({ planId });
4696
+ return formatExec(result);
4697
+ }
4698
+ case "verify": {
4699
+ const planId = positionals[0];
4700
+ if (!planId) throw new Error("Plan ID required: verify <planId>");
4701
+ const { verifyCommand: verifyCommand2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
4702
+ const result = await verifyCommand2({ planId });
4703
+ return formatVerify(result);
4704
+ }
4705
+ case "docs": {
4706
+ const { docsCommand: docsCommand2 } = await Promise.resolve().then(() => (init_docs(), docs_exports));
4707
+ const result = await docsCommand2({
4708
+ output: typeof flags["output"] === "string" ? flags["output"] : void 0,
4709
+ includeGraph: flags["no-graph"] !== true
4710
+ });
4711
+ return formatDocs(result);
4712
+ }
4713
+ case "debt": {
4714
+ const subcommand = positionals[0];
4715
+ if (!subcommand || !["add", "list", "resolve", "stats", "start"].includes(subcommand)) {
4716
+ throw new Error("Subcommand required: debt add|list|resolve|start|stats");
4717
+ }
4718
+ const { debtCommand: debtCommand2 } = await Promise.resolve().then(() => (init_debt(), debt_exports));
4719
+ const result = await debtCommand2({
4720
+ subcommand,
4721
+ description: subcommand === "add" ? positionals.slice(1).join(" ") || void 0 : void 0,
4722
+ severity: typeof flags["severity"] === "string" ? flags["severity"] : void 0,
4723
+ due: typeof flags["due"] === "string" ? flags["due"] : void 0,
4724
+ tokenCost: typeof flags["token-cost"] === "string" ? Number.parseInt(flags["token-cost"], 10) : void 0,
4725
+ id: subcommand === "resolve" || subcommand === "start" ? positionals[1] : void 0,
4726
+ overdue: flags["overdue"] === true
4727
+ });
4728
+ return formatDebt(result);
4729
+ }
4730
+ default:
4731
+ throw new Error(`Unknown command: ${name}. Type help for available commands.`);
4732
+ }
4733
+ }
4734
+ var MAX_OUTPUT_LINES;
4735
+ var init_use_command_executor = __esm({
4736
+ "src/cli/dashboard/hooks/use-command-executor.ts"() {
4737
+ "use strict";
4738
+ init_esm_shims();
4739
+ init_command_parser();
4740
+ init_formatters();
4741
+ MAX_OUTPUT_LINES = 200;
4742
+ }
4743
+ });
4744
+
4745
+ // src/cli/dashboard/hooks/use-data.ts
4746
+ import { statSync as statSync3 } from "fs";
4747
+ import { useCallback as useCallback2, useEffect, useRef, useState as useState3 } from "react";
4748
+ function useDashboardData(dbPath, projectRoot, refreshInterval = 2e3) {
4749
+ const [data, setData] = useState3(EMPTY_STATE);
4750
+ const memoryRef = useRef(null);
4751
+ const mountedRef = useRef(true);
4752
+ const dbPathRef = useRef(dbPath);
4753
+ dbPathRef.current = dbPath;
4754
+ const getMemory = useCallback2(() => memoryRef.current, []);
4755
+ const doRefreshFast = useCallback2(async () => {
4756
+ const memory = memoryRef.current;
4757
+ if (!memory) return;
4758
+ await refreshFast(memory, dbPathRef.current, projectRoot, mountedRef, setData);
4759
+ }, [projectRoot]);
4760
+ const forceRefresh = useCallback2(() => {
4761
+ void doRefreshFast();
4762
+ }, [doRefreshFast]);
4763
+ useEffect(() => {
4764
+ let timer = null;
4765
+ async function init() {
4766
+ try {
4767
+ const memory = await createKVMemory(dbPath);
4768
+ if (!mountedRef.current) {
4769
+ await memory.close();
4770
+ return;
4771
+ }
4772
+ memoryRef.current = memory;
4773
+ await refreshFast(memory, dbPath, projectRoot, mountedRef, setData);
4774
+ await refreshGraph(projectRoot, mountedRef, setData);
4775
+ timer = setInterval(() => {
4776
+ if (memoryRef.current) {
4777
+ void refreshFast(memoryRef.current, dbPath, projectRoot, mountedRef, setData);
4778
+ }
4779
+ }, refreshInterval);
4780
+ } catch (err) {
4781
+ if (mountedRef.current) {
4782
+ setData((prev) => ({
4783
+ ...prev,
4784
+ loading: false,
4785
+ error: err instanceof Error ? err.message : String(err)
4786
+ }));
4787
+ }
4788
+ }
4789
+ }
4790
+ void init();
4791
+ return () => {
4792
+ mountedRef.current = false;
4793
+ if (timer) clearInterval(timer);
4794
+ if (memoryRef.current) {
4795
+ void memoryRef.current.close();
4796
+ memoryRef.current = null;
4797
+ }
4798
+ };
4799
+ }, [dbPath, projectRoot, refreshInterval]);
4800
+ return { ...data, getMemory, forceRefresh };
4801
+ }
4802
+ async function refreshFast(memory, path2, projectRoot, mountedRef, setData) {
4803
+ try {
4804
+ const [stats, ids, activeEntries, readyEntries, silentEntries] = await Promise.all([
4805
+ memory.getOptimizationStats(),
4806
+ memory.list(),
4807
+ memory.query({ state: "active" }),
4808
+ memory.query({ state: "ready" }),
4809
+ memory.query({ state: "silent" })
4810
+ ]);
4811
+ let debts = [];
4812
+ let debtStats = null;
4813
+ try {
4814
+ const tracker = createDebtTracker(path2);
4815
+ debts = await tracker.list();
4816
+ debtStats = await tracker.stats();
4817
+ await tracker.close();
4818
+ } catch {
4819
+ }
4820
+ let daemon = null;
4821
+ try {
4822
+ const { createDaemonCommand: createDaemonCommand2 } = await Promise.resolve().then(() => (init_daemon_process(), daemon_process_exports));
4823
+ const { readFileSync: readFileSync11 } = await import("fs");
4824
+ const { resolve: resolve12 } = await import("path");
4825
+ const { load: parseYAML3 } = await import("js-yaml");
4826
+ const configPath = resolve12(projectRoot, ".sparn/config.yaml");
4827
+ const configContent = readFileSync11(configPath, "utf-8");
4828
+ const config = parseYAML3(configContent);
4829
+ daemon = await createDaemonCommand2().status(config);
4830
+ } catch {
4831
+ }
4832
+ let dbSizeBytes = 0;
4833
+ try {
4834
+ dbSizeBytes = statSync3(path2).size;
4835
+ } catch {
4836
+ }
4837
+ if (mountedRef.current) {
4838
+ setData((prev) => ({
4839
+ ...prev,
4840
+ optimizationStats: stats,
4841
+ totalEntries: ids.length,
4842
+ stateDistribution: {
4843
+ active: activeEntries.length,
4844
+ ready: readyEntries.length,
4845
+ silent: silentEntries.length
4846
+ },
4847
+ debts,
4848
+ debtStats,
4849
+ daemon,
4850
+ dbSizeBytes,
4851
+ loading: false,
4852
+ error: null,
4853
+ lastRefresh: /* @__PURE__ */ new Date()
4854
+ }));
4855
+ }
4856
+ } catch (err) {
4857
+ if (mountedRef.current) {
4858
+ setData((prev) => ({
4859
+ ...prev,
4860
+ loading: false,
4861
+ error: err instanceof Error ? err.message : String(err)
4862
+ }));
4863
+ }
4864
+ }
4865
+ }
4866
+ async function refreshGraph(projectRoot, mountedRef, setData) {
4867
+ try {
4868
+ const graph = createDependencyGraph({ projectRoot });
4869
+ const analysis = await graph.analyze();
4870
+ const nodes = graph.getNodes();
4871
+ if (mountedRef.current) {
4872
+ setData((prev) => ({
4873
+ ...prev,
4874
+ graphAnalysis: analysis,
4875
+ graphNodes: nodes
4876
+ }));
4877
+ }
4878
+ } catch {
4879
+ }
4880
+ }
4881
+ var NOOP, EMPTY_STATE;
4882
+ var init_use_data = __esm({
4883
+ "src/cli/dashboard/hooks/use-data.ts"() {
4884
+ "use strict";
4885
+ init_esm_shims();
4886
+ init_debt_tracker();
4887
+ init_dependency_graph();
4888
+ init_kv_memory();
4889
+ NOOP = () => {
4890
+ };
4891
+ EMPTY_STATE = {
4892
+ optimizationStats: [],
4893
+ totalEntries: 0,
4894
+ stateDistribution: { active: 0, ready: 0, silent: 0 },
4895
+ debts: [],
4896
+ debtStats: null,
4897
+ graphAnalysis: null,
4898
+ graphNodes: /* @__PURE__ */ new Map(),
4899
+ daemon: null,
4900
+ dbSizeBytes: 0,
4901
+ loading: true,
4902
+ error: null,
4903
+ lastRefresh: /* @__PURE__ */ new Date(),
4904
+ getMemory: () => null,
4905
+ forceRefresh: NOOP
4906
+ };
4907
+ }
4908
+ });
4909
+
4910
+ // src/cli/dashboard/panels/debt-tracker.tsx
4911
+ import { Box as Box3, Text as Text3 } from "ink";
4912
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
4913
+ function severityColor(severity) {
4914
+ switch (severity) {
4915
+ case "P0":
4916
+ return theme.errorRed;
4917
+ case "P1":
4918
+ return theme.synapseViolet;
4919
+ default:
4920
+ return theme.dimGray;
4921
+ }
4922
+ }
4923
+ function formatDate(ts) {
4924
+ const d = new Date(ts);
4925
+ return `${d.getMonth() + 1}/${d.getDate()}`;
4926
+ }
4927
+ function DebtTrackerPanel({
4928
+ debts,
4929
+ debtStats,
4930
+ focused,
4931
+ scrollOffset
4932
+ }) {
4933
+ const borderColor = focused ? theme.neuralCyan : theme.dimGray;
4934
+ const now = Date.now();
4935
+ const openDebts = debts.filter((d) => d.status !== "resolved");
4936
+ const visibleDebts = openDebts.slice(scrollOffset, scrollOffset + 8);
4937
+ return /* @__PURE__ */ jsxs3(
4938
+ Box3,
4939
+ {
4940
+ flexDirection: "column",
4941
+ borderStyle: "round",
4942
+ borderColor,
4943
+ paddingX: 1,
4944
+ width: "50%",
4945
+ flexGrow: 1,
4946
+ children: [
4947
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.errorRed, children: "Tech Debt" }),
4948
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
4949
+ visibleDebts.length === 0 && /* @__PURE__ */ jsx3(Text3, { color: theme.dimGray, children: "No open debts" }),
4950
+ visibleDebts.map((d) => {
4951
+ const isOverdue = d.repayment_date < now && d.status !== "resolved";
4952
+ const color = severityColor(d.severity);
4953
+ const desc = d.description.length > 30 ? `${d.description.substring(0, 27)}...` : d.description;
4954
+ return /* @__PURE__ */ jsxs3(Text3, { children: [
4955
+ /* @__PURE__ */ jsxs3(Text3, { color, children: [
4956
+ "[",
4957
+ d.severity,
4958
+ "]"
4959
+ ] }),
4960
+ " ",
4961
+ desc,
4962
+ " ",
4963
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.dimGray, children: [
4964
+ "(due: ",
4965
+ formatDate(d.repayment_date),
4966
+ ")"
4967
+ ] }),
4968
+ isOverdue && /* @__PURE__ */ jsx3(Text3, { color: theme.errorRed, children: " OVERDUE" })
4969
+ ] }, d.id);
4970
+ }),
4971
+ openDebts.length > 8 && /* @__PURE__ */ jsxs3(Text3, { color: theme.dimGray, children: [
4972
+ "[",
4973
+ scrollOffset + 1,
4974
+ "-",
4975
+ Math.min(scrollOffset + 8, openDebts.length),
4976
+ "/",
4977
+ openDebts.length,
4978
+ "]",
4979
+ focused ? " \u2191\u2193 scroll" : ""
4980
+ ] })
4981
+ ] }),
4982
+ debtStats && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: theme.dimGray, children: [
4983
+ "Open:",
4984
+ debtStats.open,
4985
+ " InProg:",
4986
+ debtStats.in_progress,
4987
+ " Resolved:",
4988
+ debtStats.resolved,
4989
+ " ",
4990
+ "Overdue:",
4991
+ debtStats.overdue
4992
+ ] }) })
4993
+ ]
4994
+ }
4995
+ );
4996
+ }
4997
+ var init_debt_tracker2 = __esm({
4998
+ "src/cli/dashboard/panels/debt-tracker.tsx"() {
4999
+ "use strict";
5000
+ init_esm_shims();
5001
+ init_theme();
5002
+ }
5003
+ });
5004
+
5005
+ // src/cli/dashboard/panels/graph-view.tsx
5006
+ import { Box as Box4, Text as Text4 } from "ink";
5007
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
5008
+ function formatTokens(n) {
5009
+ if (n >= 1e3) return `${(n / 1e3).toFixed(0)}K`;
5010
+ return String(n);
5011
+ }
5012
+ function basename(filePath) {
5013
+ const parts = filePath.replace(/\\/g, "/").split("/");
5014
+ return parts[parts.length - 1] || filePath;
5015
+ }
5016
+ function buildTreeLines(nodes) {
5017
+ const lines = [];
5018
+ const sorted = [...nodes.values()].filter((n) => n.callers.length > 0).sort((a, b) => b.callers.length - a.callers.length).slice(0, 8);
5019
+ for (const node of sorted) {
5020
+ const name = basename(node.filePath);
5021
+ const tok = formatTokens(node.tokenEstimate);
5022
+ lines.push({
5023
+ text: `${name} (${node.callers.length} callers, ${tok}tok)`,
5024
+ color: theme.neuralCyan
5025
+ });
5026
+ const callerNames = node.callers.slice(0, 3).map(basename);
5027
+ const remaining = node.callers.length - callerNames.length;
5028
+ for (let i = 0; i < callerNames.length; i++) {
5029
+ const isLast = i === callerNames.length - 1 && remaining === 0;
5030
+ const prefix = isLast ? " \u2514\u2500\u2500 " : " \u251C\u2500\u2500 ";
5031
+ lines.push({
5032
+ text: `${prefix}${callerNames[i]}`,
5033
+ color: theme.dimGray
5034
+ });
5035
+ }
5036
+ if (remaining > 0) {
5037
+ lines.push({
5038
+ text: ` \u2514\u2500\u2500 ... ${remaining} more`,
5039
+ color: theme.dimGray
5040
+ });
5041
+ }
5042
+ }
5043
+ return lines;
5044
+ }
5045
+ function GraphViewPanel({
5046
+ analysis,
5047
+ nodes,
5048
+ focused,
5049
+ scrollOffset
5050
+ }) {
5051
+ const borderColor = focused ? theme.neuralCyan : theme.dimGray;
5052
+ if (!analysis) {
5053
+ return /* @__PURE__ */ jsxs4(
5054
+ Box4,
5055
+ {
5056
+ flexDirection: "column",
5057
+ borderStyle: "round",
5058
+ borderColor,
5059
+ paddingX: 1,
5060
+ width: "50%",
5061
+ flexGrow: 1,
5062
+ children: [
5063
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.synapseViolet, children: "Dependency Graph" }),
5064
+ /* @__PURE__ */ jsx4(Text4, { color: theme.dimGray, children: "Loading graph..." })
5065
+ ]
5066
+ }
5067
+ );
5068
+ }
5069
+ const lines = buildTreeLines(nodes);
5070
+ const visibleLines = lines.slice(scrollOffset, scrollOffset + 12);
5071
+ return /* @__PURE__ */ jsxs4(
5072
+ Box4,
5073
+ {
5074
+ flexDirection: "column",
5075
+ borderStyle: "round",
5076
+ borderColor,
5077
+ paddingX: 1,
5078
+ width: "50%",
5079
+ flexGrow: 1,
5080
+ children: [
5081
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: theme.synapseViolet, children: "Dependency Graph" }),
5082
+ /* @__PURE__ */ jsxs4(Text4, { color: theme.dimGray, children: [
5083
+ analysis.entryPoints.length,
5084
+ " entries | ",
5085
+ analysis.orphans.length,
5086
+ " orphans |",
5087
+ " ",
5088
+ formatTokens(analysis.totalTokens),
5089
+ "tok"
5090
+ ] }),
5091
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginTop: 1, children: [
5092
+ visibleLines.map((line) => /* @__PURE__ */ jsx4(Text4, { color: line.color, children: line.text }, line.text)),
5093
+ lines.length > 12 && /* @__PURE__ */ jsxs4(Text4, { color: theme.dimGray, children: [
5094
+ "[",
5095
+ scrollOffset + 1,
5096
+ "-",
5097
+ Math.min(scrollOffset + 12, lines.length),
5098
+ "/",
5099
+ lines.length,
5100
+ "]",
5101
+ focused ? " \u2191\u2193 scroll" : ""
5102
+ ] })
5103
+ ] })
5104
+ ]
5105
+ }
5106
+ );
5107
+ }
5108
+ var init_graph_view = __esm({
5109
+ "src/cli/dashboard/panels/graph-view.tsx"() {
5110
+ "use strict";
5111
+ init_esm_shims();
5112
+ init_theme();
5113
+ }
5114
+ });
5115
+
5116
+ // src/cli/dashboard/panels/memory-status.tsx
5117
+ import { Box as Box5, Text as Text5 } from "ink";
5118
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
5119
+ function formatSize(bytes) {
5120
+ if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`;
5121
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
5122
+ return `${bytes} B`;
5123
+ }
5124
+ function formatTokens2(n) {
5125
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
5126
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
5127
+ return String(n);
5128
+ }
5129
+ function MemoryStatusPanel({
5130
+ totalEntries,
5131
+ dbSizeBytes,
5132
+ stateDistribution,
5133
+ daemon,
5134
+ focused
5135
+ }) {
5136
+ const borderColor = focused ? theme.neuralCyan : theme.dimGray;
5137
+ const daemonRunning = daemon?.running ?? false;
5138
+ const daemonPid = daemon?.pid;
5139
+ const sessions = daemon?.sessionsWatched;
5140
+ const tokensSaved = daemon?.tokensSaved;
5141
+ return /* @__PURE__ */ jsxs5(
5142
+ Box5,
5143
+ {
5144
+ flexDirection: "column",
5145
+ borderStyle: "round",
5146
+ borderColor,
5147
+ paddingX: 1,
5148
+ width: "50%",
5149
+ flexGrow: 1,
5150
+ children: [
5151
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: theme.brainPink, children: "Memory / Daemon" }),
5152
+ /* @__PURE__ */ jsxs5(Text5, { children: [
5153
+ "Entries: ",
5154
+ /* @__PURE__ */ jsx5(Text5, { color: theme.white, children: totalEntries }),
5155
+ " | Size:",
5156
+ " ",
5157
+ /* @__PURE__ */ jsx5(Text5, { color: theme.white, children: formatSize(dbSizeBytes) })
5158
+ ] }),
5159
+ /* @__PURE__ */ jsxs5(Text5, { children: [
5160
+ "Active: ",
5161
+ /* @__PURE__ */ jsx5(Text5, { color: theme.neuralCyan, children: stateDistribution.active }),
5162
+ " Ready:",
5163
+ " ",
5164
+ /* @__PURE__ */ jsx5(Text5, { color: theme.synapseViolet, children: stateDistribution.ready }),
5165
+ " Silent:",
5166
+ " ",
5167
+ /* @__PURE__ */ jsx5(Text5, { color: theme.dimGray, children: stateDistribution.silent })
5168
+ ] }),
5169
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: daemonRunning ? /* @__PURE__ */ jsxs5(Text5, { children: [
5170
+ "Daemon: ",
5171
+ /* @__PURE__ */ jsxs5(Text5, { color: theme.neuralCyan, children: [
5172
+ "\u25CF",
5173
+ " Running"
5174
+ ] }),
5175
+ daemonPid != null ? ` (PID ${daemonPid})` : ""
5176
+ ] }) : /* @__PURE__ */ jsxs5(Text5, { children: [
5177
+ "Daemon: ",
5178
+ /* @__PURE__ */ jsxs5(Text5, { color: theme.errorRed, children: [
5179
+ "\u25CF",
5180
+ " Stopped"
5181
+ ] })
5182
+ ] }) }),
5183
+ daemonRunning && /* @__PURE__ */ jsxs5(Text5, { children: [
5184
+ "Sessions: ",
5185
+ /* @__PURE__ */ jsx5(Text5, { color: theme.white, children: sessions ?? 0 }),
5186
+ " | Saved:",
5187
+ " ",
5188
+ /* @__PURE__ */ jsx5(Text5, { color: theme.white, children: formatTokens2(tokensSaved ?? 0) })
5189
+ ] })
5190
+ ]
5191
+ }
5192
+ );
5193
+ }
5194
+ var init_memory_status = __esm({
5195
+ "src/cli/dashboard/panels/memory-status.tsx"() {
5196
+ "use strict";
5197
+ init_esm_shims();
5198
+ init_theme();
5199
+ }
5200
+ });
5201
+
5202
+ // src/cli/dashboard/panels/optimization.tsx
5203
+ import { Box as Box6, Text as Text6 } from "ink";
5204
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
5205
+ function formatTime(ts) {
5206
+ const d = new Date(ts);
5207
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
5208
+ }
5209
+ function formatTokens3(n) {
5210
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
5211
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
5212
+ return String(n);
5213
+ }
5214
+ function makeBar(pct, width) {
5215
+ const filled = Math.round(pct / 100 * width);
5216
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
5217
+ }
5218
+ function OptimizationPanel({ stats, focused }) {
5219
+ const totalOpts = stats.length;
5220
+ const totalSaved = stats.reduce((s, r) => s + (r.tokens_before - r.tokens_after), 0);
5221
+ let avgReduction = 0;
5222
+ if (totalOpts > 0) {
5223
+ const totalBefore = stats.reduce((s, r) => s + r.tokens_before, 0);
5224
+ avgReduction = totalBefore > 0 ? (totalBefore - (totalBefore - totalSaved)) / totalBefore * 100 : 0;
5225
+ }
5226
+ const recent = stats.slice(-5).reverse();
5227
+ const borderColor = focused ? theme.neuralCyan : theme.dimGray;
5228
+ return /* @__PURE__ */ jsxs6(
5229
+ Box6,
5230
+ {
5231
+ flexDirection: "column",
5232
+ borderStyle: "round",
5233
+ borderColor,
5234
+ paddingX: 1,
5235
+ width: "50%",
5236
+ flexGrow: 1,
5237
+ children: [
5238
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: theme.neuralCyan, children: "Optimization Stats" }),
5239
+ /* @__PURE__ */ jsxs6(Text6, { children: [
5240
+ "Total: ",
5241
+ /* @__PURE__ */ jsx6(Text6, { color: theme.white, children: totalOpts }),
5242
+ " optimizations"
5243
+ ] }),
5244
+ /* @__PURE__ */ jsxs6(Text6, { children: [
5245
+ "Saved: ",
5246
+ /* @__PURE__ */ jsx6(Text6, { color: theme.white, children: formatTokens3(totalSaved) }),
5247
+ " tokens"
5248
+ ] }),
5249
+ /* @__PURE__ */ jsxs6(Text6, { children: [
5250
+ "Avg: ",
5251
+ /* @__PURE__ */ jsxs6(Text6, { color: theme.white, children: [
5252
+ avgReduction.toFixed(1),
5253
+ "%"
5254
+ ] }),
5255
+ " ",
5256
+ /* @__PURE__ */ jsx6(Text6, { color: theme.neuralCyan, children: makeBar(avgReduction, 10) })
5257
+ ] }),
5258
+ recent.length > 0 && /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
5259
+ /* @__PURE__ */ jsx6(Text6, { color: theme.dimGray, children: "Recent:" }),
5260
+ recent.map((r) => {
5261
+ const pct = r.tokens_before > 0 ? ((r.tokens_before - r.tokens_after) / r.tokens_before * 100).toFixed(0) : "0";
5262
+ return /* @__PURE__ */ jsxs6(Text6, { children: [
5263
+ " ",
5264
+ formatTime(r.timestamp),
5265
+ " | ",
5266
+ formatTokens3(r.tokens_before),
5267
+ "\u2192",
5268
+ formatTokens3(r.tokens_after),
5269
+ " | ",
5270
+ pct,
5271
+ "%"
5272
+ ] }, r.id);
5273
+ })
5274
+ ] })
5275
+ ]
5276
+ }
5277
+ );
5278
+ }
5279
+ var init_optimization = __esm({
5280
+ "src/cli/dashboard/panels/optimization.tsx"() {
5281
+ "use strict";
5282
+ init_esm_shims();
5283
+ init_theme();
5284
+ }
5285
+ });
5286
+
5287
+ // src/cli/dashboard/app.tsx
5288
+ var app_exports = {};
5289
+ __export(app_exports, {
5290
+ renderDashboard: () => renderDashboard
5291
+ });
5292
+ import { Box as Box7, render, Text as Text7, useApp, useInput } from "ink";
5293
+ import { useEffect as useEffect2, useState as useState4 } from "react";
5294
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
5295
+ function Dashboard({ dbPath, projectRoot, refreshInterval }) {
5296
+ const { exit } = useApp();
5297
+ const data = useDashboardData(dbPath, projectRoot, refreshInterval);
5298
+ const [mode, setMode] = useState4("monitor");
5299
+ const [focusIndex, setFocusIndex] = useState4(0);
5300
+ const [graphScroll, setGraphScroll] = useState4(0);
5301
+ const [debtScroll, setDebtScroll] = useState4(0);
5302
+ const [outputScroll, setOutputScroll] = useState4(0);
5303
+ const focusedPanel = PANELS[focusIndex] ?? "optimization";
5304
+ const executor = useCommandExecutor({
5305
+ getMemory: data.getMemory,
5306
+ forceRefresh: data.forceRefresh,
5307
+ dbPath,
5308
+ projectRoot
5309
+ });
5310
+ useInput(
5311
+ (input2, key) => {
5312
+ if (mode === "command") {
5313
+ if (key.escape) {
5314
+ setMode("monitor");
5315
+ }
5316
+ return;
5317
+ }
5318
+ if (input2 === "q") {
5319
+ exit();
5320
+ return;
5321
+ }
5322
+ if (input2 === ":") {
5323
+ setMode("command");
5324
+ return;
5325
+ }
5326
+ if (key.tab) {
5327
+ setFocusIndex((prev) => (prev + 1) % PANELS.length);
5328
+ return;
5329
+ }
5330
+ if (key.upArrow) {
5331
+ if (focusedPanel === "graph") setGraphScroll((prev) => Math.max(0, prev - 1));
5332
+ if (focusedPanel === "debt") setDebtScroll((prev) => Math.max(0, prev - 1));
5333
+ if (focusedPanel === "output") setOutputScroll((prev) => Math.max(0, prev - 1));
5334
+ }
5335
+ if (key.downArrow) {
5336
+ if (focusedPanel === "graph") setGraphScroll((prev) => prev + 1);
5337
+ if (focusedPanel === "debt") setDebtScroll((prev) => prev + 1);
5338
+ if (focusedPanel === "output") setOutputScroll((prev) => prev + 1);
5339
+ }
5340
+ },
5341
+ { isActive: mode === "monitor" || mode === "command" }
5342
+ );
5343
+ const outputMaxVisible = 8;
5344
+ useEffect2(() => {
5345
+ setOutputScroll(Math.max(0, executor.outputLines.length - outputMaxVisible));
5346
+ }, [executor.outputLines.length]);
5347
+ if (data.loading) {
5348
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
5349
+ /* @__PURE__ */ jsx7(Text7, { color: theme.neuralCyan, children: "Sparn Dashboard" }),
5350
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dimGray, children: "Loading data..." })
5351
+ ] });
5352
+ }
5353
+ if (data.error) {
5354
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
5355
+ /* @__PURE__ */ jsx7(Text7, { color: theme.neuralCyan, children: "Sparn Dashboard" }),
5356
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.errorRed, children: [
5357
+ "Error: ",
5358
+ data.error
5359
+ ] }),
5360
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dimGray, children: "Press q to quit" })
5361
+ ] });
5362
+ }
5363
+ const timeStr = data.lastRefresh.toLocaleTimeString();
5364
+ const modeHint = mode === "command" ? "Esc:monitor Enter:run" : "q:quit Tab:focus \u2191\u2193:scroll ::cmd";
5365
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
5366
+ /* @__PURE__ */ jsxs7(Box7, { justifyContent: "space-between", paddingX: 1, children: [
5367
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: theme.neuralCyan, children: "Sparn Dashboard" }),
5368
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dimGray, children: [
5369
+ modeHint,
5370
+ " | ",
5371
+ timeStr
5372
+ ] })
5373
+ ] }),
5374
+ /* @__PURE__ */ jsxs7(Box7, { children: [
5375
+ /* @__PURE__ */ jsx7(
5376
+ OptimizationPanel,
5377
+ {
5378
+ stats: data.optimizationStats,
5379
+ focused: focusedPanel === "optimization"
5380
+ }
5381
+ ),
5382
+ /* @__PURE__ */ jsx7(
5383
+ GraphViewPanel,
5384
+ {
5385
+ analysis: data.graphAnalysis,
5386
+ nodes: data.graphNodes,
5387
+ focused: focusedPanel === "graph",
5388
+ scrollOffset: graphScroll
5389
+ }
5390
+ )
5391
+ ] }),
5392
+ /* @__PURE__ */ jsxs7(Box7, { children: [
5393
+ /* @__PURE__ */ jsx7(
5394
+ MemoryStatusPanel,
5395
+ {
5396
+ totalEntries: data.totalEntries,
5397
+ dbSizeBytes: data.dbSizeBytes,
5398
+ stateDistribution: data.stateDistribution,
5399
+ daemon: data.daemon,
5400
+ focused: focusedPanel === "memory"
5401
+ }
5402
+ ),
5403
+ /* @__PURE__ */ jsx7(
5404
+ DebtTrackerPanel,
5405
+ {
5406
+ debts: data.debts,
5407
+ debtStats: data.debtStats,
5408
+ focused: focusedPanel === "debt",
5409
+ scrollOffset: debtScroll
5410
+ }
5411
+ )
5412
+ ] }),
5413
+ /* @__PURE__ */ jsx7(
5414
+ OutputPanel,
5415
+ {
5416
+ lines: executor.outputLines,
5417
+ scrollOffset: outputScroll,
5418
+ focused: focusedPanel === "output"
5419
+ }
5420
+ ),
5421
+ /* @__PURE__ */ jsx7(
5422
+ CommandInput,
5423
+ {
5424
+ onSubmit: executor.execute,
5425
+ isRunning: executor.isRunning,
5426
+ runningCommand: executor.runningCommand,
5427
+ active: mode === "command"
5428
+ }
5429
+ )
5430
+ ] });
5431
+ }
5432
+ function renderDashboard(options) {
5433
+ render(
5434
+ /* @__PURE__ */ jsx7(
5435
+ Dashboard,
5436
+ {
5437
+ dbPath: options.dbPath,
5438
+ projectRoot: options.projectRoot,
5439
+ refreshInterval: options.refreshInterval ?? 2e3
5440
+ }
5441
+ )
5442
+ );
5443
+ }
5444
+ var PANELS;
5445
+ var init_app = __esm({
5446
+ "src/cli/dashboard/app.tsx"() {
5447
+ "use strict";
5448
+ init_esm_shims();
5449
+ init_command_input();
5450
+ init_output_panel();
5451
+ init_use_command_executor();
5452
+ init_use_data();
5453
+ init_debt_tracker2();
5454
+ init_graph_view();
5455
+ init_memory_status();
5456
+ init_optimization();
5457
+ init_theme();
5458
+ PANELS = ["optimization", "graph", "memory", "debt", "output"];
5459
+ }
5460
+ });
5461
+
5462
+ // src/cli/index.ts
5463
+ init_esm_shims();
5464
+ import { spawn as spawn3 } from "child_process";
5465
+ import { readFileSync as readFileSync10 } from "fs";
5466
+ import { dirname as dirname5, join as join9, resolve as resolve11 } from "path";
5467
+ import { fileURLToPath as fileURLToPath6 } from "url";
5468
+ import { Command } from "commander";
5469
+ import { load as loadYAML } from "js-yaml";
5470
+
5471
+ // src/utils/audio.ts
5472
+ init_esm_shims();
5473
+ import { execFileSync } from "child_process";
5474
+ import { existsSync } from "fs";
5475
+ import { dirname, join, resolve } from "path";
5476
+ import { fileURLToPath as fileURLToPath2 } from "url";
5477
+ var AUDIO_DIR = join(dirname(fileURLToPath2(import.meta.url)), "../../audio");
5478
+ var DISABLED = process.env["SPARN_AUDIO"] === "false";
5479
+ function playSound(filename) {
5480
+ if (DISABLED) return;
5481
+ const filepath = resolve(AUDIO_DIR, filename);
5482
+ if (!existsSync(filepath)) return;
5483
+ try {
5484
+ if (process.platform === "win32") {
5485
+ const escaped = filepath.replace(/'/g, "''");
5486
+ execFileSync(
5487
+ "powershell.exe",
5488
+ [
5489
+ "-NoProfile",
5490
+ "-NonInteractive",
5491
+ "-Command",
5492
+ `(New-Object Media.SoundPlayer '${escaped}').PlaySync()`
5493
+ ],
5494
+ { windowsHide: true, timeout: 5e3, stdio: "ignore" }
5495
+ );
5496
+ } else if (process.platform === "darwin") {
5497
+ execFileSync("afplay", [filepath], { timeout: 5e3, stdio: "ignore" });
5498
+ } else {
5499
+ try {
5500
+ execFileSync("aplay", ["-q", filepath], { timeout: 5e3, stdio: "ignore" });
5501
+ } catch {
5502
+ try {
5503
+ execFileSync("paplay", [filepath], { timeout: 5e3, stdio: "ignore" });
5504
+ } catch {
5505
+ execFileSync("play", ["-q", filepath], { timeout: 5e3, stdio: "ignore" });
5506
+ }
5507
+ }
5508
+ }
5509
+ } catch {
5510
+ }
5511
+ }
5512
+ function playStartup() {
5513
+ playSound("startup.wav");
5514
+ }
5515
+ function playCommand() {
5516
+ playSound("command.wav");
5517
+ }
5518
+ function playComplete() {
5519
+ playSound("complete.wav");
5520
+ }
5521
+ function playEnd() {
5522
+ playSound("end.wav");
5523
+ }
5524
+
5525
+ // src/cli/index.ts
5526
+ init_tokenizer();
5527
+ init_banner();
5528
+ function getVersion2() {
5529
+ try {
5530
+ const __filename2 = fileURLToPath6(import.meta.url);
5531
+ const __dirname2 = dirname5(__filename2);
5532
+ const pkg = JSON.parse(readFileSync10(join9(__dirname2, "../../package.json"), "utf-8"));
5533
+ return pkg.version;
5534
+ } catch {
5535
+ return "1.4.0";
5536
+ }
5537
+ }
5538
+ var VERSION2 = getVersion2();
5539
+ try {
5540
+ const configPath = resolve11(process.cwd(), ".sparn/config.yaml");
5541
+ const configContent = readFileSync10(configPath, "utf-8");
5542
+ const config = loadYAML(configContent);
5543
+ if (config?.realtime?.preciseTokenCounting) {
5544
+ setPreciseTokenCounting(true);
5545
+ }
5546
+ } catch {
5547
+ }
5548
+ async function handleError(error, context) {
5549
+ const { errorRed: errorRed2, synapseViolet: synapseViolet2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
5550
+ const errorMsg = error instanceof Error ? error.message : String(error);
5551
+ const stack = error instanceof Error ? error.stack : void 0;
5552
+ console.error(errorRed2("\n\u2717 Error:"), errorMsg);
5553
+ if (context) {
5554
+ console.error(errorRed2("Context:"), context);
5555
+ }
5556
+ if (errorMsg.includes("SQLITE") || errorMsg.includes("database")) {
5557
+ console.error(synapseViolet2("\n\u{1F4A1} Database issue detected:"));
5558
+ console.error(" Try running: rm -rf .sparn/ && sparn init");
5559
+ console.error(" This will reinitialize your Sparn database.\n");
5560
+ }
5561
+ if (errorMsg.includes("EACCES") || errorMsg.includes("permission")) {
5562
+ console.error(synapseViolet2("\n\u{1F4A1} Permission issue detected:"));
5563
+ console.error(" Check file permissions in .sparn/ directory");
5564
+ console.error(" Try: chmod -R u+rw .sparn/\n");
5565
+ }
5566
+ if (errorMsg.includes("ENOENT") || errorMsg.includes("no such file")) {
5567
+ console.error(synapseViolet2("\n\u{1F4A1} File not found:"));
5568
+ console.error(" Make sure you have run: sparn init");
5569
+ console.error(" Or check that the specified file exists.\n");
5570
+ }
5571
+ if (errorMsg.includes("out of memory") || errorMsg.includes("heap")) {
5572
+ console.error(synapseViolet2("\n\u{1F4A1} Memory issue detected:"));
5573
+ console.error(" Try processing smaller chunks of context");
5574
+ console.error(" Or increase Node.js memory: NODE_OPTIONS=--max-old-space-size=4096\n");
5575
+ }
5576
+ if (process.env["SPARN_DEBUG"] === "true" && stack) {
5577
+ console.error(errorRed2("\nStack trace:"));
5578
+ console.error(stack);
5579
+ } else {
5580
+ console.error(" Run with SPARN_DEBUG=true for stack trace\n");
5581
+ }
5582
+ process.exit(1);
5583
+ }
5584
+ process.on("unhandledRejection", (reason) => {
5585
+ void handleError(reason, "Unhandled promise rejection");
5586
+ });
5587
+ process.on("uncaughtException", (error) => {
5588
+ void handleError(error, "Uncaught exception");
5589
+ });
5590
+ var program = new Command();
5591
+ program.name("sparn").description("Context optimization for AI coding agents").version(VERSION2, "-v, --version", "Output the current version").helpOption("-h, --help", "Display help for command").enablePositionalOptions();
5592
+ playStartup();
5593
+ program.hook("preAction", () => {
5594
+ playCommand();
5595
+ });
5596
+ program.hook("postAction", () => {
5597
+ playComplete();
5598
+ });
5599
+ process.on("exit", () => {
5600
+ playEnd();
5601
+ });
5602
+ process.once("SIGINT", () => {
5603
+ process.exit(0);
5604
+ });
5605
+ program.command("init").description("Initialize Sparn in the current project").option("-f, --force", "Force overwrite if .sparn/ already exists").addHelpText(
5606
+ "after",
5607
+ `
5608
+ Examples:
5609
+ $ sparn init # Initialize in current directory
5610
+ $ sparn init --force # Overwrite existing .sparn/ directory
5611
+
5612
+ Files Created:
5613
+ .sparn/config.yaml # Configuration with optimization parameters
5614
+ .sparn/memory.db # SQLite database for context storage
5615
+
5616
+ Next Steps:
5617
+ After initialization, use 'sparn optimize' to start optimizing context.
5618
+ `
5619
+ ).action(async (options) => {
5620
+ const { initCommand: initCommand2, displayInitSuccess: displayInitSuccess2 } = await Promise.resolve().then(() => (init_init(), init_exports));
5621
+ const { createInitSpinner: createInitSpinner2 } = await Promise.resolve().then(() => (init_progress(), progress_exports));
5622
+ const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
5623
+ const spinner = createInitSpinner2("\u{1F9E0} Initializing Sparn...");
5624
+ try {
5625
+ spinner.start();
5626
+ spinner.text = "\u{1F4C1} Creating .sparn/ directory...";
5627
+ const result = await initCommand2({ force: options.force });
5628
+ spinner.succeed(neuralCyan2("Sparn initialized successfully!"));
2559
5629
  displayInitSuccess2(result);
2560
5630
  } catch (error) {
2561
5631
  spinner.fail(errorRed2("Initialization failed"));
@@ -2563,7 +5633,7 @@ Next Steps:
2563
5633
  process.exit(1);
2564
5634
  }
2565
5635
  });
2566
- program.command("optimize").description("Optimize context memory using neuroscience principles").option("-i, --input <file>", "Input file path").option("-o, --output <file>", "Output file path").option("--dry-run", "Run without saving to memory").option("--verbose", "Show detailed per-entry scores").addHelpText(
5636
+ program.command("optimize").description("Optimize context memory").option("-i, --input <file>", "Input file path").option("-o, --output <file>", "Output file path").option("--dry-run", "Run without saving to memory").option("--verbose", "Show detailed per-entry scores").addHelpText(
2567
5637
  "after",
2568
5638
  `
2569
5639
  Examples:
@@ -2573,11 +5643,11 @@ Examples:
2573
5643
  $ sparn optimize -i context.txt --verbose # Show entry scores
2574
5644
 
2575
5645
  How It Works:
2576
- 1. Sparse Coding: Keeps only 2-5% most relevant context
2577
- 2. Engram Theory: Applies decay to old memories
2578
- 3. Multi-State Synapses: Classifies as silent/ready/active
2579
- 4. BTSP: Locks critical events (errors, stack traces)
2580
- 5. Sleep Replay: Consolidates and compresses
5646
+ 1. Relevance Filtering: Keeps only 2-5% most relevant context
5647
+ 2. Time-Based Decay: Fades old entries unless reinforced
5648
+ 3. Entry Classification: Classifies as silent/ready/active
5649
+ 4. Critical Event Detection: Locks errors and stack traces
5650
+ 5. Periodic Consolidation: Merges duplicates and cleans up
2581
5651
 
2582
5652
  Typical Results:
2583
5653
  \u2022 60-90% token reduction
@@ -2593,7 +5663,7 @@ Typical Results:
2593
5663
  try {
2594
5664
  spinner.start();
2595
5665
  let input2;
2596
- if (!options.input && process.stdin.isTTY === false) {
5666
+ if (!options.input && !process.stdin.isTTY) {
2597
5667
  spinner.text = "\u{1F4D6} Reading context from stdin...";
2598
5668
  const chunks = [];
2599
5669
  for await (const chunk of process.stdin) {
@@ -2604,39 +5674,42 @@ Typical Results:
2604
5674
  spinner.text = `\u{1F4D6} Reading context from ${options.input}...`;
2605
5675
  }
2606
5676
  spinner.text = "\u{1F4BE} Loading memory database...";
2607
- const dbPath = resolve2(process.cwd(), ".sparn/memory.db");
5677
+ const dbPath = resolve11(process.cwd(), ".sparn/memory.db");
2608
5678
  const memory = await createKVMemory2(dbPath);
2609
- spinner.text = "\u26A1 Applying neuroscience principles...";
2610
- const result = await optimizeCommand2({
2611
- input: input2,
2612
- inputFile: options.input,
2613
- outputFile: options.output,
2614
- memory,
2615
- dryRun: options.dryRun || false,
2616
- verbose: options.verbose || false
2617
- });
2618
- spinner.succeed(neuralCyan2(`Optimization complete in ${result.durationMs}ms!`));
2619
- showTokenSavings2(result.tokensBefore, result.tokensAfter, result.reduction);
2620
- console.log(synapseViolet2(" Entry Distribution:"));
2621
- console.log(` \u2022 Processed: ${result.entriesProcessed}`);
2622
- console.log(` \u2022 Kept: ${result.entriesKept}`);
2623
- console.log(` \u2022 Active: ${result.stateDistribution.active}`);
2624
- console.log(` \u2022 Ready: ${result.stateDistribution.ready}`);
2625
- console.log(` \u2022 Silent: ${result.stateDistribution.silent}
5679
+ try {
5680
+ spinner.text = "\u26A1 Optimizing context...";
5681
+ const result = await optimizeCommand2({
5682
+ input: input2,
5683
+ inputFile: options.input,
5684
+ outputFile: options.output,
5685
+ memory,
5686
+ dryRun: options.dryRun || false,
5687
+ verbose: options.verbose || false
5688
+ });
5689
+ spinner.succeed(neuralCyan2(`Optimization complete in ${result.durationMs}ms!`));
5690
+ showTokenSavings2(result.tokensBefore, result.tokensAfter, result.reduction);
5691
+ console.log(synapseViolet2(" Entry Distribution:"));
5692
+ console.log(` \u2022 Processed: ${result.entriesProcessed}`);
5693
+ console.log(` \u2022 Kept: ${result.entriesKept}`);
5694
+ console.log(` \u2022 Active: ${result.stateDistribution.active}`);
5695
+ console.log(` \u2022 Ready: ${result.stateDistribution.ready}`);
5696
+ console.log(` \u2022 Silent: ${result.stateDistribution.silent}
2626
5697
  `);
2627
- if (options.verbose && result.details) {
2628
- console.log(neuralCyan2(" \u{1F4CB} Entry Details:"));
2629
- for (const detail of result.details) {
2630
- console.log(
2631
- ` ${detail.id.substring(0, 8)}: score=${detail.score.toFixed(2)}, state=${detail.state}, tokens=${detail.tokens}`
2632
- );
5698
+ if (options.verbose && result.details) {
5699
+ console.log(neuralCyan2(" \u{1F4CB} Entry Details:"));
5700
+ for (const detail of result.details) {
5701
+ console.log(
5702
+ ` ${detail.id.substring(0, 8)}: score=${detail.score.toFixed(2)}, state=${detail.state}, tokens=${detail.tokens}`
5703
+ );
5704
+ }
5705
+ console.log();
2633
5706
  }
2634
- console.log();
2635
- }
2636
- if (!options.output) {
2637
- console.log(result.output);
5707
+ if (!options.output) {
5708
+ console.log(result.output);
5709
+ }
5710
+ } finally {
5711
+ await memory.close();
2638
5712
  }
2639
- await memory.close();
2640
5713
  } catch (error) {
2641
5714
  spinner.fail(errorRed2("Optimization failed"));
2642
5715
  console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
@@ -2668,7 +5741,7 @@ Tracked Metrics:
2668
5741
  try {
2669
5742
  if (spinner) spinner.start();
2670
5743
  if (spinner) spinner.text = "\u{1F4BE} Loading optimization history...";
2671
- const dbPath = resolve2(process.cwd(), ".sparn/memory.db");
5744
+ const dbPath = resolve11(process.cwd(), ".sparn/memory.db");
2672
5745
  const memory = await createKVMemory2(dbPath);
2673
5746
  let confirmReset = false;
2674
5747
  if (options.reset) {
@@ -2676,37 +5749,40 @@ Tracked Metrics:
2676
5749
  console.log(synapseViolet2("Warning: This will clear all optimization statistics."));
2677
5750
  confirmReset = true;
2678
5751
  }
2679
- if (spinner) spinner.text = "\u{1F4C8} Calculating statistics...";
2680
- const result = await statsCommand2({
2681
- memory,
2682
- graph: options.graph || false,
2683
- reset: options.reset || false,
2684
- confirmReset,
2685
- json: options.json || false
2686
- });
2687
- if (spinner) spinner.succeed(neuralCyan2("Statistics ready!"));
2688
- if (options.json) {
2689
- console.log(result.json);
2690
- } else if (options.reset && result.resetConfirmed) {
2691
- console.log(neuralCyan2("\n\u2713 Statistics cleared\n"));
2692
- } else {
2693
- console.log(neuralCyan2("\n\u{1F4CA} Optimization Statistics\n"));
2694
- console.log(` Total optimizations: ${result.totalCommands}`);
2695
- console.log(` Total tokens saved: ${result.totalTokensSaved.toLocaleString()}`);
2696
- console.log(` Average reduction: ${(result.averageReduction * 100).toFixed(1)}%`);
2697
- if (options.graph && result.graph) {
2698
- console.log(result.graph);
5752
+ try {
5753
+ if (spinner) spinner.text = "\u{1F4C8} Calculating statistics...";
5754
+ const result = await statsCommand2({
5755
+ memory,
5756
+ graph: options.graph || false,
5757
+ reset: options.reset || false,
5758
+ confirmReset,
5759
+ json: options.json || false
5760
+ });
5761
+ if (spinner) spinner.succeed(neuralCyan2("Statistics ready!"));
5762
+ if (options.json) {
5763
+ console.log(result.json);
5764
+ } else if (options.reset && result.resetConfirmed) {
5765
+ console.log(neuralCyan2("\n\u2713 Statistics cleared\n"));
5766
+ } else {
5767
+ console.log(neuralCyan2("\n\u{1F4CA} Optimization Statistics\n"));
5768
+ console.log(` Total optimizations: ${result.totalCommands}`);
5769
+ console.log(` Total tokens saved: ${result.totalTokensSaved.toLocaleString()}`);
5770
+ console.log(` Average reduction: ${(result.averageReduction * 100).toFixed(1)}%`);
5771
+ if (options.graph && result.graph) {
5772
+ console.log(result.graph);
5773
+ }
5774
+ console.log();
2699
5775
  }
2700
- console.log();
5776
+ } finally {
5777
+ await memory.close();
2701
5778
  }
2702
- await memory.close();
2703
5779
  } catch (error) {
2704
5780
  if (spinner) spinner.fail(errorRed2("Statistics failed"));
2705
5781
  console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
2706
5782
  process.exit(1);
2707
5783
  }
2708
5784
  });
2709
- program.command("relay <command> [args...]").description("Proxy a CLI command through optimization").option("--silent", "Suppress token savings summary").addHelpText(
5785
+ program.command("relay <command> [args...]").description("Proxy a CLI command through optimization").passThroughOptions().option("--silent", "Suppress token savings summary").addHelpText(
2710
5786
  "after",
2711
5787
  `
2712
5788
  Examples:
@@ -2728,22 +5804,27 @@ The relay command passes the exit code from the wrapped command.
2728
5804
  const { relayCommand: relayCommand2 } = await Promise.resolve().then(() => (init_relay(), relay_exports));
2729
5805
  const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
2730
5806
  try {
2731
- const dbPath = resolve2(process.cwd(), ".sparn/memory.db");
5807
+ const dbPath = resolve11(process.cwd(), ".sparn/memory.db");
2732
5808
  const memory = await createKVMemory2(dbPath);
2733
- const result = await relayCommand2({
2734
- command,
2735
- args: args || [],
2736
- memory,
2737
- silent: options.silent || false
2738
- });
2739
- console.log(result.optimizedOutput);
2740
- if (result.summary) {
2741
- console.error(neuralCyan2(`
5809
+ let exitCode = 1;
5810
+ try {
5811
+ const result = await relayCommand2({
5812
+ command,
5813
+ args: args || [],
5814
+ memory,
5815
+ silent: options.silent || false
5816
+ });
5817
+ console.log(result.optimizedOutput);
5818
+ if (result.summary) {
5819
+ console.error(neuralCyan2(`
2742
5820
  ${result.summary}
2743
5821
  `));
5822
+ }
5823
+ exitCode = result.exitCode;
5824
+ } finally {
5825
+ await memory.close();
2744
5826
  }
2745
- await memory.close();
2746
- process.exit(result.exitCode);
5827
+ process.exit(exitCode);
2747
5828
  } catch (error) {
2748
5829
  console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
2749
5830
  process.exit(1);
@@ -2781,24 +5862,27 @@ Typical Results:
2781
5862
  try {
2782
5863
  spinner.start();
2783
5864
  spinner.text = "\u{1F4BE} Loading memory database...";
2784
- const dbPath = resolve2(process.cwd(), ".sparn/memory.db");
5865
+ const dbPath = resolve11(process.cwd(), ".sparn/memory.db");
2785
5866
  const memory = await createKVMemory2(dbPath);
2786
- spinner.text = "\u{1F50D} Identifying decayed entries...";
2787
- const result = await consolidateCommand2({ memory });
2788
- spinner.succeed(neuralCyan2(`Consolidation complete in ${result.durationMs}ms!`));
2789
- showConsolidationSummary2(
2790
- result.entriesBefore,
2791
- result.entriesAfter,
2792
- result.decayedRemoved,
2793
- result.duplicatesRemoved,
2794
- result.durationMs
2795
- );
2796
- if (result.vacuumCompleted) {
2797
- console.log(synapseViolet2(" \u2713 Database VACUUM completed\n"));
2798
- } else {
2799
- console.log(errorRed2(" \u2717 Database VACUUM failed\n"));
5867
+ try {
5868
+ spinner.text = "\u{1F50D} Identifying decayed entries...";
5869
+ const result = await consolidateCommand2({ memory });
5870
+ spinner.succeed(neuralCyan2(`Consolidation complete in ${result.durationMs}ms!`));
5871
+ showConsolidationSummary2(
5872
+ result.entriesBefore,
5873
+ result.entriesAfter,
5874
+ result.decayedRemoved,
5875
+ result.duplicatesRemoved,
5876
+ result.durationMs
5877
+ );
5878
+ if (result.vacuumCompleted) {
5879
+ console.log(synapseViolet2(" \u2713 Database VACUUM completed\n"));
5880
+ } else {
5881
+ console.log(errorRed2(" \u2717 Database VACUUM failed\n"));
5882
+ }
5883
+ } finally {
5884
+ await memory.close();
2800
5885
  }
2801
- await memory.close();
2802
5886
  } catch (error) {
2803
5887
  spinner.fail(errorRed2("Consolidation failed"));
2804
5888
  console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
@@ -2815,8 +5899,8 @@ Examples:
2815
5899
  $ sparn config --json # View full config as JSON
2816
5900
 
2817
5901
  Configuration Keys:
2818
- pruning.threshold # Sparse coding threshold (2-5%)
2819
- decay.halfLife # Engram decay half-life (hours)
5902
+ pruning.threshold # Relevance threshold (2-5%)
5903
+ decay.halfLife # Decay half-life (hours)
2820
5904
  decay.minScore # Minimum decay score (0.0-1.0)
2821
5905
  states.activeThreshold # Active state threshold
2822
5906
  states.readyThreshold # Ready state threshold
@@ -2829,7 +5913,7 @@ The config file is located at .sparn/config.yaml
2829
5913
  const { configCommand: configCommand2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
2830
5914
  const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
2831
5915
  try {
2832
- const configPath = resolve2(process.cwd(), ".sparn/config.yaml");
5916
+ const configPath = resolve11(process.cwd(), ".sparn/config.yaml");
2833
5917
  const result = await configCommand2({
2834
5918
  configPath,
2835
5919
  subcommand,
@@ -2842,11 +5926,14 @@ The config file is located at .sparn/config.yaml
2842
5926
  process.exit(1);
2843
5927
  }
2844
5928
  if (result.editorPath && !options.json) {
2845
- const editor = process.env["EDITOR"] || "vim";
5929
+ const editorEnv = process.env["EDITOR"] || "vim";
5930
+ const editorParts = editorEnv.split(/\s+/);
5931
+ const editorCmd = editorParts[0] || "vim";
5932
+ const editorArgs = [...editorParts.slice(1), result.editorPath];
2846
5933
  console.log(neuralCyan2(`
2847
- \u{1F4DD} Opening config in ${editor}...
5934
+ \u{1F4DD} Opening config in ${editorCmd}...
2848
5935
  `));
2849
- const child = spawn2(editor, [result.editorPath], {
5936
+ const child = spawn3(editorCmd, editorArgs, {
2850
5937
  stdio: "inherit"
2851
5938
  });
2852
5939
  child.on("close", (code) => {
@@ -2888,8 +5975,8 @@ optimizes contexts when they exceed the configured threshold.
2888
5975
  const { createDaemonCommand: createDaemonCommand2 } = await Promise.resolve().then(() => (init_daemon_process(), daemon_process_exports));
2889
5976
  const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
2890
5977
  try {
2891
- const configPath = resolve2(process.cwd(), ".sparn/config.yaml");
2892
- const configYAML = readFileSync6(configPath, "utf-8");
5978
+ const configPath = resolve11(process.cwd(), ".sparn/config.yaml");
5979
+ const configYAML = readFileSync10(configPath, "utf-8");
2893
5980
  const config = parseYAML3(configYAML);
2894
5981
  const daemon = createDaemonCommand2();
2895
5982
  switch (subcommand) {
@@ -2984,6 +6071,7 @@ and compress verbose tool results after execution.
2984
6071
  console.log("\nHook paths:");
2985
6072
  console.log(` pre-prompt: ${result.hookPaths.prePrompt}`);
2986
6073
  console.log(` post-tool-result: ${result.hookPaths.postToolResult}`);
6074
+ console.log(` stop-docs-refresh: ${result.hookPaths.stopDocsRefresh}`);
2987
6075
  }
2988
6076
  console.log();
2989
6077
  } else {
@@ -3022,19 +6110,284 @@ and configuring Sparn without memorizing CLI flags.
3022
6110
  const { interactiveCommand: interactiveCommand2 } = await Promise.resolve().then(() => (init_interactive(), interactive_exports));
3023
6111
  const { errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
3024
6112
  try {
3025
- const dbPath = resolve2(process.cwd(), ".sparn/memory.db");
6113
+ const dbPath = resolve11(process.cwd(), ".sparn/memory.db");
3026
6114
  const memory = await createKVMemory2(dbPath);
3027
- const configPath = resolve2(process.cwd(), ".sparn/config.yaml");
3028
- await interactiveCommand2({
3029
- memory,
3030
- configPath
6115
+ try {
6116
+ const configPath = resolve11(process.cwd(), ".sparn/config.yaml");
6117
+ await interactiveCommand2({
6118
+ memory,
6119
+ configPath
6120
+ });
6121
+ } finally {
6122
+ await memory.close();
6123
+ }
6124
+ } catch (error) {
6125
+ console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
6126
+ process.exit(1);
6127
+ }
6128
+ });
6129
+ program.command("graph").description("Analyze dependency graph of the project").option("--entry <file>", "Start from a specific entry point").option("--depth <n>", "Limit traversal depth", (v) => Number.parseInt(v, 10)).option("--focus <pattern>", "Focus on modules matching pattern").option("--json", "Output as JSON").action(async (options) => {
6130
+ const { graphCommand: graphCommand2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
6131
+ const { neuralCyan: neuralCyan2, synapseViolet: synapseViolet2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
6132
+ try {
6133
+ const result = await graphCommand2({
6134
+ entry: options.entry,
6135
+ depth: options.depth,
6136
+ focus: options.focus,
6137
+ json: options.json
6138
+ });
6139
+ if (options.json) {
6140
+ console.log(result.json);
6141
+ } else {
6142
+ console.log(neuralCyan2("\n\u{1F4CA} Dependency Graph Analysis\n"));
6143
+ console.log(` Files analyzed: ${result.nodeCount}`);
6144
+ console.log(` Entry points: ${result.analysis.entryPoints.length}`);
6145
+ console.log(` Orphaned files: ${result.analysis.orphans.length}`);
6146
+ console.log(` Total tokens: ${result.analysis.totalTokens.toLocaleString()}`);
6147
+ if (result.analysis.hotPaths.length > 0) {
6148
+ console.log(synapseViolet2("\n Hot paths (most imported):"));
6149
+ for (const path2 of result.analysis.hotPaths.slice(0, 5)) {
6150
+ console.log(` ${path2}`);
6151
+ }
6152
+ }
6153
+ if (result.analysis.entryPoints.length > 0) {
6154
+ console.log(synapseViolet2("\n Entry points:"));
6155
+ for (const path2 of result.analysis.entryPoints.slice(0, 5)) {
6156
+ console.log(` ${path2}`);
6157
+ }
6158
+ }
6159
+ console.log();
6160
+ }
6161
+ } catch (error) {
6162
+ console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
6163
+ process.exit(1);
6164
+ }
6165
+ });
6166
+ program.command("search [query]").description("Search codebase using FTS5 + ripgrep").option("--glob <pattern>", "Filter by file glob pattern").option("--max <n>", "Max results (default: 10)", (v) => Number.parseInt(v, 10)).option("--json", "Output as JSON").action(async (query, options) => {
6167
+ const { searchCommand: searchCommand2 } = await Promise.resolve().then(() => (init_search(), search_exports));
6168
+ const { neuralCyan: neuralCyan2, synapseViolet: synapseViolet2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
6169
+ try {
6170
+ let subcommand;
6171
+ if (query === "init") subcommand = "init";
6172
+ if (query === "refresh") subcommand = "refresh";
6173
+ const result = await searchCommand2({
6174
+ query: subcommand ? void 0 : query,
6175
+ subcommand,
6176
+ glob: options.glob,
6177
+ maxResults: options.max,
6178
+ json: options.json
6179
+ });
6180
+ if (options.json && result.json) {
6181
+ console.log(result.json);
6182
+ } else if (result.message) {
6183
+ console.log(neuralCyan2(`
6184
+ \u2713 ${result.message}
6185
+ `));
6186
+ }
6187
+ if (result.results && !options.json) {
6188
+ console.log(neuralCyan2(`
6189
+ \u{1F50D} ${result.results.length} results
6190
+ `));
6191
+ for (const r of result.results) {
6192
+ console.log(synapseViolet2(` ${r.filePath}:${r.lineNumber}`));
6193
+ console.log(` ${r.content.trim()}`);
6194
+ console.log();
6195
+ }
6196
+ }
6197
+ } catch (error) {
6198
+ console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
6199
+ process.exit(1);
6200
+ }
6201
+ });
6202
+ program.command("plan <task>").description("Create an execution plan for a task").option("--files <files...>", "Files needed for the task").option("--searches <queries...>", "Search queries to run").option("--max-reads <n>", "Max file reads (default: 5)", (v) => Number.parseInt(v, 10)).option("--json", "Output as JSON").action(async (task, options) => {
6203
+ const { planCommand: planCommand2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
6204
+ const { neuralCyan: neuralCyan2, synapseViolet: synapseViolet2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
6205
+ try {
6206
+ if (task === "list") {
6207
+ const { planListCommand: planListCommand2 } = await Promise.resolve().then(() => (init_plan(), plan_exports));
6208
+ const result2 = await planListCommand2({ json: options.json });
6209
+ if (options.json) {
6210
+ console.log(result2.json);
6211
+ } else {
6212
+ console.log(neuralCyan2("\n\u{1F4CB} Plans\n"));
6213
+ for (const p of result2.plans) {
6214
+ console.log(` ${p.id} [${p.status}] ${p.task}`);
6215
+ }
6216
+ if (result2.plans.length === 0) console.log(" No plans found");
6217
+ console.log();
6218
+ }
6219
+ return;
6220
+ }
6221
+ const result = await planCommand2({
6222
+ task,
6223
+ files: options.files,
6224
+ searches: options.searches,
6225
+ maxReads: options.maxReads,
6226
+ json: options.json
6227
+ });
6228
+ if (options.json) {
6229
+ console.log(result.json);
6230
+ } else {
6231
+ console.log(neuralCyan2(`
6232
+ \u2713 ${result.message}`));
6233
+ console.log(synapseViolet2("\n Steps:"));
6234
+ for (const step of result.plan.steps) {
6235
+ console.log(` ${step.order}. [${step.action}] ${step.target} \u2014 ${step.description}`);
6236
+ }
6237
+ console.log();
6238
+ }
6239
+ } catch (error) {
6240
+ console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
6241
+ process.exit(1);
6242
+ }
6243
+ });
6244
+ program.command("exec <planId>").description("Execute a plan with constraints").option("--json", "Output as JSON").action(async (planId, options) => {
6245
+ const { execCommand: execCommand2 } = await Promise.resolve().then(() => (init_exec(), exec_exports));
6246
+ const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
6247
+ try {
6248
+ const result = await execCommand2({ planId, json: options.json });
6249
+ if (options.json) {
6250
+ console.log(result.json);
6251
+ } else {
6252
+ console.log(neuralCyan2(`
6253
+ \u2713 ${result.message}
6254
+ `));
6255
+ }
6256
+ } catch (error) {
6257
+ console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
6258
+ process.exit(1);
6259
+ }
6260
+ });
6261
+ program.command("verify <planId>").description("Verify plan completion").option("--json", "Output as JSON").action(async (planId, options) => {
6262
+ const { verifyCommand: verifyCommand2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
6263
+ const { neuralCyan: neuralCyan2, synapseViolet: synapseViolet2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
6264
+ try {
6265
+ const result = await verifyCommand2({ planId, json: options.json });
6266
+ if (options.json) {
6267
+ console.log(result.json);
6268
+ } else {
6269
+ console.log(neuralCyan2(`
6270
+ ${result.message}`));
6271
+ console.log(synapseViolet2("\n Steps:"));
6272
+ for (const d of result.verification.details) {
6273
+ const icon = d.status === "completed" ? "\u2713" : d.status === "failed" ? "\u2717" : "\u25CB";
6274
+ console.log(` ${icon} ${d.step}. [${d.action}] ${d.target} \u2014 ${d.status}`);
6275
+ }
6276
+ console.log();
6277
+ }
6278
+ } catch (error) {
6279
+ console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
6280
+ process.exit(1);
6281
+ }
6282
+ });
6283
+ program.command("docs").description("Auto-generate CLAUDE.md for the project").option("-o, --output <file>", "Output file path (default: CLAUDE.md)").option("--no-graph", "Skip dependency graph analysis").option("--json", "Output content as JSON").action(async (options) => {
6284
+ const { docsCommand: docsCommand2 } = await Promise.resolve().then(() => (init_docs(), docs_exports));
6285
+ const { neuralCyan: neuralCyan2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
6286
+ try {
6287
+ const result = await docsCommand2({
6288
+ output: options.output,
6289
+ includeGraph: options.graph !== false,
6290
+ json: options.json
6291
+ });
6292
+ if (options.json) {
6293
+ console.log(JSON.stringify({ content: result.content }, null, 2));
6294
+ } else {
6295
+ console.log(neuralCyan2(`
6296
+ \u2713 ${result.message}
6297
+ `));
6298
+ }
6299
+ } catch (error) {
6300
+ console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
6301
+ process.exit(1);
6302
+ }
6303
+ });
6304
+ program.command("debt <subcommand> [description]").description("Track technical debt").option("--severity <level>", "Severity: P0, P1, P2 (default: P1)").option("--due <date>", "Repayment date (YYYY-MM-DD)").option("--files <files...>", "Affected files").option("--tokens <n>", "Token cost estimate", (v) => Number.parseInt(v, 10)).option("--id <id>", "Debt ID (for resolve/start)").option("--overdue", "Show only overdue debts").option("--json", "Output as JSON").action(async (subcommand, description, options) => {
6305
+ const { debtCommand: debtCommand2 } = await Promise.resolve().then(() => (init_debt(), debt_exports));
6306
+ const { neuralCyan: neuralCyan2, synapseViolet: synapseViolet2, errorRed: errorRed2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
6307
+ try {
6308
+ const result = await debtCommand2({
6309
+ subcommand,
6310
+ description,
6311
+ severity: options.severity,
6312
+ due: options.due,
6313
+ files: options.files,
6314
+ tokenCost: options.tokens,
6315
+ id: options.id || description,
6316
+ overdue: options.overdue,
6317
+ json: options.json
3031
6318
  });
3032
- await memory.close();
6319
+ if (options.json && result.json) {
6320
+ console.log(result.json);
6321
+ } else {
6322
+ console.log(neuralCyan2(`
6323
+ \u2713 ${result.message}`));
6324
+ if (result.debts) {
6325
+ for (const d of result.debts) {
6326
+ const due = new Date(d.repayment_date).toLocaleDateString();
6327
+ const overdue = d.repayment_date < Date.now() && d.status !== "resolved" ? " [OVERDUE]" : "";
6328
+ console.log(
6329
+ synapseViolet2(
6330
+ ` ${d.id} [${d.severity}] ${d.status}${overdue} \u2014 ${d.description} (due: ${due})`
6331
+ )
6332
+ );
6333
+ }
6334
+ }
6335
+ if (result.stats) {
6336
+ console.log(
6337
+ ` Open: ${result.stats.open} | In Progress: ${result.stats.in_progress} | Resolved: ${result.stats.resolved}`
6338
+ );
6339
+ console.log(
6340
+ ` Overdue: ${result.stats.overdue} | Repayment rate: ${(result.stats.repaymentRate * 100).toFixed(0)}%`
6341
+ );
6342
+ }
6343
+ console.log();
6344
+ }
3033
6345
  } catch (error) {
3034
6346
  console.error(errorRed2("Error:"), error instanceof Error ? error.message : String(error));
3035
6347
  process.exit(1);
3036
6348
  }
3037
6349
  });
6350
+ program.command("dashboard").alias("dash").description("Launch interactive TUI dashboard").option(
6351
+ "--refresh <ms>",
6352
+ "Refresh interval in milliseconds",
6353
+ (v) => Number.parseInt(v, 10)
6354
+ ).action(async (options) => {
6355
+ const { renderDashboard: renderDashboard2 } = await Promise.resolve().then(() => (init_app(), app_exports));
6356
+ const dbPath = resolve11(process.cwd(), ".sparn/memory.db");
6357
+ const projectRoot = process.cwd();
6358
+ renderDashboard2({
6359
+ dbPath,
6360
+ projectRoot,
6361
+ refreshInterval: options.refresh ?? 2e3
6362
+ });
6363
+ });
6364
+ program.command("status").description("Quick project status overview").action(async () => {
6365
+ const { neuralCyan: neuralCyan2, synapseViolet: synapseViolet2 } = await Promise.resolve().then(() => (init_colors(), colors_exports));
6366
+ console.log(neuralCyan2(`
6367
+ \u{1F9E0} Sparn v${VERSION2} Status
6368
+ `));
6369
+ const { existsSync: exists } = await import("fs");
6370
+ const sparnDir = resolve11(process.cwd(), ".sparn");
6371
+ const initialized = exists(sparnDir);
6372
+ console.log(` Initialized: ${initialized ? "\u2713" : "\u2717 (run sparn init)"}`);
6373
+ if (initialized) {
6374
+ const dbPath = resolve11(sparnDir, "memory.db");
6375
+ console.log(` Database: ${exists(dbPath) ? "\u2713" : "\u2717"}`);
6376
+ const searchDb = resolve11(sparnDir, "search.db");
6377
+ console.log(` Search index: ${exists(searchDb) ? "\u2713" : "\u2717 (run sparn search init)"}`);
6378
+ const plansDir = resolve11(sparnDir, "plans");
6379
+ if (exists(plansDir)) {
6380
+ const { readdirSync: readDir } = await import("fs");
6381
+ const plans = readDir(plansDir).filter((f) => f.endsWith(".json"));
6382
+ console.log(` Plans: ${plans.length}`);
6383
+ }
6384
+ const pidFile = resolve11(sparnDir, "daemon.pid");
6385
+ console.log(` Daemon: ${exists(pidFile) ? "\u2713 running" : "\u2717 stopped"}`);
6386
+ }
6387
+ console.log(
6388
+ synapseViolet2("\n Commands: graph | search | plan | exec | verify | docs | debt\n")
6389
+ );
6390
+ });
3038
6391
  program.on("option:version", () => {
3039
6392
  console.log(getBanner(VERSION2));
3040
6393
  process.exit(0);