@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.
- package/PRIVACY.md +1 -1
- package/README.md +136 -642
- package/SECURITY.md +1 -1
- package/dist/cli/dashboard.cjs +3977 -0
- package/dist/cli/dashboard.cjs.map +1 -0
- package/dist/cli/dashboard.d.cts +17 -0
- package/dist/cli/dashboard.d.ts +17 -0
- package/dist/cli/dashboard.js +3932 -0
- package/dist/cli/dashboard.js.map +1 -0
- package/dist/cli/index.cjs +3853 -484
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +3810 -457
- package/dist/cli/index.js.map +1 -1
- package/dist/daemon/index.cjs +411 -99
- package/dist/daemon/index.cjs.map +1 -1
- package/dist/daemon/index.js +423 -103
- package/dist/daemon/index.js.map +1 -1
- package/dist/hooks/post-tool-result.cjs +115 -266
- package/dist/hooks/post-tool-result.cjs.map +1 -1
- package/dist/hooks/post-tool-result.js +115 -266
- package/dist/hooks/post-tool-result.js.map +1 -1
- package/dist/hooks/pre-prompt.cjs +197 -268
- package/dist/hooks/pre-prompt.cjs.map +1 -1
- package/dist/hooks/pre-prompt.js +182 -268
- package/dist/hooks/pre-prompt.js.map +1 -1
- package/dist/hooks/stop-docs-refresh.cjs +123 -0
- package/dist/hooks/stop-docs-refresh.cjs.map +1 -0
- package/dist/hooks/stop-docs-refresh.d.cts +1 -0
- package/dist/hooks/stop-docs-refresh.d.ts +1 -0
- package/dist/hooks/stop-docs-refresh.js +126 -0
- package/dist/hooks/stop-docs-refresh.js.map +1 -0
- package/dist/index.cjs +1754 -337
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +539 -40
- package/dist/index.d.ts +539 -40
- package/dist/index.js +1737 -329
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.cjs +304 -71
- package/dist/mcp/index.cjs.map +1 -1
- package/dist/mcp/index.js +308 -71
- package/dist/mcp/index.js.map +1 -1
- 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}
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
394
|
-
const configPath =
|
|
395
|
-
const dbPath =
|
|
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
|
|
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/
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
|
|
750
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
837
|
-
|
|
838
|
-
content
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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((
|
|
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
|
-
|
|
1216
|
+
resolve12({
|
|
1076
1217
|
stdout,
|
|
1077
1218
|
stderr,
|
|
1078
1219
|
exitCode: code ?? 0
|
|
1079
1220
|
});
|
|
1080
1221
|
});
|
|
1081
1222
|
child.on("error", (error) => {
|
|
1082
|
-
|
|
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
|
|
1194
|
-
vec1[word] =
|
|
1195
|
-
|
|
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 {
|
|
1643
|
-
import { existsSync as
|
|
1644
|
-
import { dirname as
|
|
1645
|
-
import { fileURLToPath as
|
|
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 (!
|
|
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 =
|
|
1670
|
-
if (!
|
|
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 (
|
|
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 =
|
|
1693
|
-
const __dirname2 =
|
|
1694
|
-
const daemonPath =
|
|
1695
|
-
const
|
|
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.
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
|
1817
|
-
import { fileURLToPath as
|
|
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 ?
|
|
1821
|
-
const __filename2 =
|
|
1822
|
-
const __dirname2 =
|
|
1823
|
-
const hooksDir =
|
|
1824
|
-
const prePromptPath =
|
|
1825
|
-
const postToolResultPath =
|
|
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
|
|
1902
|
+
return installHooks(
|
|
1903
|
+
settingsPath,
|
|
1904
|
+
prePromptPath,
|
|
1905
|
+
postToolResultPath,
|
|
1906
|
+
stopDocsRefreshPath,
|
|
1907
|
+
global
|
|
1908
|
+
);
|
|
1829
1909
|
case "uninstall":
|
|
1830
|
-
return
|
|
1910
|
+
return uninstallHooks(settingsPath, global);
|
|
1831
1911
|
case "status":
|
|
1832
|
-
return
|
|
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
|
-
|
|
1921
|
+
function installHooks(settingsPath, prePromptPath, postToolResultPath, stopDocsRefreshPath, global) {
|
|
1842
1922
|
try {
|
|
1843
|
-
if (!
|
|
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 (!
|
|
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 (
|
|
1945
|
+
if (existsSync4(settingsPath)) {
|
|
1859
1946
|
const settingsJson = readFileSync4(settingsPath, "utf-8");
|
|
1860
1947
|
settings = JSON.parse(settingsJson);
|
|
1861
1948
|
} else {
|
|
1862
|
-
const claudeDir =
|
|
1863
|
-
if (!
|
|
1949
|
+
const claudeDir = dirname4(settingsPath);
|
|
1950
|
+
if (!existsSync4(claudeDir)) {
|
|
1864
1951
|
mkdirSync2(claudeDir, { recursive: true });
|
|
1865
1952
|
}
|
|
1866
1953
|
}
|
|
1867
|
-
settings["hooks"]
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
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
|
-
|
|
2013
|
+
function uninstallHooks(settingsPath, global) {
|
|
1891
2014
|
try {
|
|
1892
|
-
if (!
|
|
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
|
-
|
|
1904
|
-
|
|
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
|
-
|
|
2050
|
+
function hooksStatus(settingsPath, global) {
|
|
1924
2051
|
try {
|
|
1925
|
-
if (!
|
|
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
|
|
1935
|
-
|
|
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:
|
|
1949
|
-
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/
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
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 (
|
|
2027
|
-
{ name: "\u23F3 Decay (
|
|
2028
|
-
{ name: "\u{1F3AF} 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("
|
|
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("
|
|
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("
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
-
|
|
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/
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
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
|
|
2482
|
-
|
|
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
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
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
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
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
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
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
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
-
|
|
2520
|
-
|
|
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
|
-
|
|
2968
|
+
return {
|
|
2969
|
+
build,
|
|
2970
|
+
analyze,
|
|
2971
|
+
focus,
|
|
2972
|
+
getFilesFromEntry,
|
|
2973
|
+
getNodes
|
|
2974
|
+
};
|
|
2526
2975
|
}
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
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
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
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
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
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
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
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
|
|
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.
|
|
2577
|
-
2.
|
|
2578
|
-
3.
|
|
2579
|
-
4.
|
|
2580
|
-
5.
|
|
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
|
|
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 =
|
|
5677
|
+
const dbPath = resolve11(process.cwd(), ".sparn/memory.db");
|
|
2608
5678
|
const memory = await createKVMemory2(dbPath);
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
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
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
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
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
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 =
|
|
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
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
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
|
-
|
|
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 =
|
|
5807
|
+
const dbPath = resolve11(process.cwd(), ".sparn/memory.db");
|
|
2732
5808
|
const memory = await createKVMemory2(dbPath);
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
console.
|
|
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
|
-
|
|
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 =
|
|
5865
|
+
const dbPath = resolve11(process.cwd(), ".sparn/memory.db");
|
|
2785
5866
|
const memory = await createKVMemory2(dbPath);
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
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 #
|
|
2819
|
-
decay.halfLife #
|
|
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 =
|
|
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
|
|
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 ${
|
|
5934
|
+
\u{1F4DD} Opening config in ${editorCmd}...
|
|
2848
5935
|
`));
|
|
2849
|
-
const child =
|
|
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 =
|
|
2892
|
-
const configYAML =
|
|
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 =
|
|
6113
|
+
const dbPath = resolve11(process.cwd(), ".sparn/memory.db");
|
|
3026
6114
|
const memory = await createKVMemory2(dbPath);
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
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
|
-
|
|
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);
|