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