@sylphx/flow 1.1.0 → 1.2.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/CHANGELOG.md +33 -0
- package/package.json +12 -2
- package/src/commands/hook-command.ts +10 -230
- package/src/composables/index.ts +0 -1
- package/src/config/servers.ts +35 -78
- package/src/core/interfaces.ts +0 -33
- package/src/domains/index.ts +0 -2
- package/src/index.ts +0 -4
- package/src/services/mcp-service.ts +0 -16
- package/src/targets/claude-code.ts +3 -9
- package/src/targets/functional/claude-code-logic.ts +4 -22
- package/src/targets/opencode.ts +0 -6
- package/src/types/mcp.types.ts +29 -38
- package/src/types/target.types.ts +0 -2
- package/src/types.ts +0 -1
- package/src/commands/codebase-command.ts +0 -168
- package/src/commands/knowledge-command.ts +0 -161
- package/src/composables/useTargetConfig.ts +0 -45
- package/src/core/formatting/bytes.test.ts +0 -115
- package/src/core/validation/limit.test.ts +0 -155
- package/src/core/validation/query.test.ts +0 -44
- package/src/domains/codebase/index.ts +0 -5
- package/src/domains/codebase/tools.ts +0 -139
- package/src/domains/knowledge/index.ts +0 -10
- package/src/domains/knowledge/resources.ts +0 -537
- package/src/domains/knowledge/tools.ts +0 -174
- package/src/services/search/base-indexer.ts +0 -156
- package/src/services/search/codebase-indexer-types.ts +0 -38
- package/src/services/search/codebase-indexer.ts +0 -647
- package/src/services/search/embeddings-provider.ts +0 -455
- package/src/services/search/embeddings.ts +0 -316
- package/src/services/search/functional-indexer.ts +0 -323
- package/src/services/search/index.ts +0 -27
- package/src/services/search/indexer.ts +0 -380
- package/src/services/search/knowledge-indexer.ts +0 -422
- package/src/services/search/semantic-search.ts +0 -244
- package/src/services/search/tfidf.ts +0 -559
- package/src/services/search/unified-search-service.ts +0 -888
- package/src/services/storage/cache-storage.ts +0 -487
- package/src/services/storage/drizzle-storage.ts +0 -581
- package/src/services/storage/index.ts +0 -15
- package/src/services/storage/lancedb-vector-storage.ts +0 -494
- package/src/services/storage/memory-storage.ts +0 -268
- package/src/services/storage/separated-storage.ts +0 -467
- package/src/services/storage/vector-storage.ts +0 -13
|
@@ -1,647 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codebase indexing with .gitignore support
|
|
3
|
-
* Runtime indexing with intelligent caching
|
|
4
|
-
*
|
|
5
|
-
* Features:
|
|
6
|
-
* - Optional file watching for automatic re-indexing (controlled by MCP server)
|
|
7
|
-
* - Respects .gitignore patterns
|
|
8
|
-
* - Debounced re-indexing (5 seconds after last change)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import path from 'node:path';
|
|
13
|
-
import chokidar from 'chokidar';
|
|
14
|
-
import type { Ignore } from 'ignore';
|
|
15
|
-
import { isTextFile, loadGitignore, scanFiles, simpleHash } from '../../utils/codebase-helpers.js';
|
|
16
|
-
import { logger } from '../../utils/logger.js';
|
|
17
|
-
import { SeparatedMemoryStorage } from '../storage/separated-storage.js';
|
|
18
|
-
import { type VectorDocument, VectorStorage } from '../storage/vector-storage.js';
|
|
19
|
-
import type {
|
|
20
|
-
CodebaseFile,
|
|
21
|
-
CodebaseIndexerOptions,
|
|
22
|
-
IndexCache,
|
|
23
|
-
IndexingStatus,
|
|
24
|
-
} from './codebase-indexer-types.js';
|
|
25
|
-
import type { EmbeddingProvider } from './embeddings.js';
|
|
26
|
-
import { buildSearchIndex, type SearchIndex } from './tfidf.js';
|
|
27
|
-
|
|
28
|
-
export class CodebaseIndexer {
|
|
29
|
-
private codebaseRoot: string;
|
|
30
|
-
private cacheDir: string;
|
|
31
|
-
private cache: IndexCache | null = null;
|
|
32
|
-
private ig: Ignore;
|
|
33
|
-
private db: SeparatedMemoryStorage;
|
|
34
|
-
private watcher?: chokidar.FSWatcher;
|
|
35
|
-
private reindexTimer?: NodeJS.Timeout;
|
|
36
|
-
private status: IndexingStatus = {
|
|
37
|
-
isIndexing: false,
|
|
38
|
-
progress: 0,
|
|
39
|
-
totalFiles: 0,
|
|
40
|
-
indexedFiles: 0,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
constructor(options: CodebaseIndexerOptions = {}) {
|
|
44
|
-
this.options = { batchSize: 100, ...options };
|
|
45
|
-
this.codebaseRoot = options.codebaseRoot || process.cwd();
|
|
46
|
-
this.cacheDir = options.cacheDir || path.join(this.codebaseRoot, '.sylphx-flow', 'cache');
|
|
47
|
-
this.ig = loadGitignore(this.codebaseRoot);
|
|
48
|
-
this.db = new SeparatedMemoryStorage();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Get current indexing status
|
|
53
|
-
*/
|
|
54
|
-
getStatus(): IndexingStatus {
|
|
55
|
-
return { ...this.status };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Build TF-IDF index from database
|
|
60
|
-
*/
|
|
61
|
-
private async buildTFIDFIndexFromDB(): Promise<SearchIndex | undefined> {
|
|
62
|
-
try {
|
|
63
|
-
// Get all documents from database
|
|
64
|
-
const documents = [];
|
|
65
|
-
const dbFiles = await this.db.getAllCodebaseFiles();
|
|
66
|
-
|
|
67
|
-
for (const file of dbFiles) {
|
|
68
|
-
const tfidfDoc = await this.db.getTFIDFDocument(file.path);
|
|
69
|
-
if (tfidfDoc) {
|
|
70
|
-
// Parse rawTerms from JSON string to object
|
|
71
|
-
let rawTermsObj = {};
|
|
72
|
-
if (tfidfDoc.rawTerms) {
|
|
73
|
-
if (typeof tfidfDoc.rawTerms === 'string') {
|
|
74
|
-
try {
|
|
75
|
-
rawTermsObj = JSON.parse(tfidfDoc.rawTerms);
|
|
76
|
-
} catch (error) {
|
|
77
|
-
logger.warn('Failed to parse rawTerms', { path: file.path, error });
|
|
78
|
-
rawTermsObj = {};
|
|
79
|
-
}
|
|
80
|
-
} else if (typeof tfidfDoc.rawTerms === 'object') {
|
|
81
|
-
rawTermsObj = tfidfDoc.rawTerms;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const terms = new Map<string, number>();
|
|
86
|
-
const rawTermsMap = new Map<string, number>();
|
|
87
|
-
|
|
88
|
-
for (const [term, freq] of Object.entries(rawTermsObj)) {
|
|
89
|
-
terms.set(term, freq as number);
|
|
90
|
-
rawTermsMap.set(term, freq as number);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
documents.push({
|
|
94
|
-
uri: `file://${file.path}`,
|
|
95
|
-
terms,
|
|
96
|
-
rawTerms: rawTermsMap,
|
|
97
|
-
magnitude: tfidfDoc.magnitude,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Get IDF values
|
|
103
|
-
const idfRecords = await this.db.getIDFValues();
|
|
104
|
-
const idf = new Map<string, number>();
|
|
105
|
-
for (const [term, value] of Object.entries(idfRecords)) {
|
|
106
|
-
idf.set(term, value as number);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (documents.length === 0) {
|
|
110
|
-
return undefined;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
documents,
|
|
115
|
-
idf,
|
|
116
|
-
totalDocuments: documents.length,
|
|
117
|
-
metadata: {
|
|
118
|
-
generatedAt: new Date().toISOString(),
|
|
119
|
-
version: '1.0.0',
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
} catch (error) {
|
|
123
|
-
logger.error('Failed to build TF-IDF index from database', { error });
|
|
124
|
-
return undefined;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Load cache from database
|
|
130
|
-
*/
|
|
131
|
-
private async loadCache(): Promise<IndexCache | null> {
|
|
132
|
-
try {
|
|
133
|
-
// Initialize database
|
|
134
|
-
await this.db.initialize();
|
|
135
|
-
|
|
136
|
-
// Get metadata
|
|
137
|
-
const version = await this.db.getCodebaseMetadata('version');
|
|
138
|
-
const codebaseRoot = await this.db.getCodebaseMetadata('codebaseRoot');
|
|
139
|
-
const indexedAt = await this.db.getCodebaseMetadata('indexedAt');
|
|
140
|
-
const fileCount = await this.db.getCodebaseMetadata('fileCount');
|
|
141
|
-
|
|
142
|
-
if (!version || !codebaseRoot || !indexedAt || !fileCount) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Get all files
|
|
147
|
-
const dbFiles = await this.db.getAllCodebaseFiles();
|
|
148
|
-
const files = new Map<string, { mtime: number; hash: string }>();
|
|
149
|
-
|
|
150
|
-
for (const file of dbFiles) {
|
|
151
|
-
files.set(file.path, { mtime: file.mtime, hash: file.hash });
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Build TF-IDF index from database
|
|
155
|
-
const tfidfIndex = await this.buildTFIDFIndexFromDB();
|
|
156
|
-
|
|
157
|
-
// Get vector index path
|
|
158
|
-
const vectorIndexPath = await this.db.getCodebaseMetadata('vectorIndexPath');
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
version,
|
|
162
|
-
codebaseRoot,
|
|
163
|
-
indexedAt,
|
|
164
|
-
fileCount: Number.parseInt(fileCount, 10),
|
|
165
|
-
files,
|
|
166
|
-
tfidfIndex,
|
|
167
|
-
vectorIndexPath,
|
|
168
|
-
};
|
|
169
|
-
} catch (error) {
|
|
170
|
-
logger.error('Failed to load cache from database', { error });
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Save cache to database
|
|
177
|
-
*/
|
|
178
|
-
private async saveCache(cache: IndexCache): Promise<void> {
|
|
179
|
-
try {
|
|
180
|
-
// Initialize tables
|
|
181
|
-
await this.db.initialize();
|
|
182
|
-
|
|
183
|
-
// Save metadata
|
|
184
|
-
await this.db.setCodebaseMetadata('version', cache.version);
|
|
185
|
-
await this.db.setCodebaseMetadata('codebaseRoot', cache.codebaseRoot);
|
|
186
|
-
await this.db.setCodebaseMetadata('indexedAt', cache.indexedAt);
|
|
187
|
-
await this.db.setCodebaseMetadata('fileCount', cache.fileCount.toString());
|
|
188
|
-
if (cache.vectorIndexPath) {
|
|
189
|
-
await this.db.setCodebaseMetadata('vectorIndexPath', cache.vectorIndexPath);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Save files
|
|
193
|
-
for (const [filePath, fileInfo] of cache.files.entries()) {
|
|
194
|
-
await this.db.upsertCodebaseFile({
|
|
195
|
-
path: filePath,
|
|
196
|
-
mtime: fileInfo.mtime,
|
|
197
|
-
hash: fileInfo.hash,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Save TF-IDF index if available
|
|
202
|
-
if (cache.tfidfIndex) {
|
|
203
|
-
// Save documents
|
|
204
|
-
for (const doc of cache.tfidfIndex.documents) {
|
|
205
|
-
const filePath = doc.uri.replace('file://', '');
|
|
206
|
-
// Convert rawTerms Map to Record
|
|
207
|
-
const rawTermsRecord: Record<string, number> = {};
|
|
208
|
-
for (const [term, freq] of doc.rawTerms.entries()) {
|
|
209
|
-
rawTermsRecord[term] = freq;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
await this.db.upsertTFIDFDocument(filePath, {
|
|
213
|
-
magnitude: doc.magnitude,
|
|
214
|
-
termCount: doc.terms.size,
|
|
215
|
-
rawTerms: rawTermsRecord,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
// Save terms
|
|
219
|
-
const terms: Record<string, number> = {};
|
|
220
|
-
for (const [term, freq] of doc.terms.entries()) {
|
|
221
|
-
terms[term] = freq;
|
|
222
|
-
}
|
|
223
|
-
await this.db.setTFIDFTerms(filePath, terms);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Save IDF values
|
|
227
|
-
const idfValues: Record<string, number> = {};
|
|
228
|
-
for (const [term, value] of cache.tfidfIndex.idf.entries()) {
|
|
229
|
-
idfValues[term] = value;
|
|
230
|
-
}
|
|
231
|
-
await this.db.setIDFValues(idfValues);
|
|
232
|
-
}
|
|
233
|
-
} catch (error) {
|
|
234
|
-
logger.error('Failed to save cache to database', { error });
|
|
235
|
-
throw error;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Index codebase (with incremental updates)
|
|
241
|
-
*/
|
|
242
|
-
async indexCodebase(
|
|
243
|
-
options: {
|
|
244
|
-
force?: boolean; // Force full reindex
|
|
245
|
-
embeddingProvider?: EmbeddingProvider; // Optional embeddings
|
|
246
|
-
onProgress?: (progress: {
|
|
247
|
-
current: number;
|
|
248
|
-
total: number;
|
|
249
|
-
fileName: string;
|
|
250
|
-
status: 'processing' | 'completed' | 'skipped';
|
|
251
|
-
}) => void;
|
|
252
|
-
} = {}
|
|
253
|
-
): Promise<{
|
|
254
|
-
tfidfIndex: SearchIndex;
|
|
255
|
-
vectorStorage?: VectorStorage;
|
|
256
|
-
stats: {
|
|
257
|
-
totalFiles: number;
|
|
258
|
-
indexedFiles: number;
|
|
259
|
-
skippedFiles: number;
|
|
260
|
-
cacheHit: boolean;
|
|
261
|
-
};
|
|
262
|
-
}> {
|
|
263
|
-
let { force = false } = options;
|
|
264
|
-
const { embeddingProvider } = options;
|
|
265
|
-
|
|
266
|
-
// Set indexing status
|
|
267
|
-
this.status.isIndexing = true;
|
|
268
|
-
this.status.progress = 0;
|
|
269
|
-
this.status.indexedFiles = 0;
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
// Load existing cache
|
|
273
|
-
this.cache = await this.loadCache();
|
|
274
|
-
|
|
275
|
-
// Scan codebase (silent during reindex for clean UI)
|
|
276
|
-
const files = scanFiles(this.codebaseRoot, {
|
|
277
|
-
codebaseRoot: this.codebaseRoot,
|
|
278
|
-
ignoreFilter: this.ig,
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
this.status.totalFiles = files.length;
|
|
282
|
-
|
|
283
|
-
// Validate cache (check if file count changed significantly)
|
|
284
|
-
if (this.cache && !force) {
|
|
285
|
-
const fileCountDiff = Math.abs(files.length - this.cache.fileCount);
|
|
286
|
-
const fileCountChangePercent = (fileCountDiff / this.cache.fileCount) * 100;
|
|
287
|
-
|
|
288
|
-
// If file count changed by >20%, force full reindex for safety
|
|
289
|
-
if (fileCountChangePercent > 20) {
|
|
290
|
-
force = true;
|
|
291
|
-
this.cache = null;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Detect changes (new, modified, deleted files)
|
|
296
|
-
const changedFiles: CodebaseFile[] = [];
|
|
297
|
-
const deletedFiles: string[] = [];
|
|
298
|
-
const fileMap = new Map<string, { mtime: number; hash: string }>();
|
|
299
|
-
|
|
300
|
-
// Check for new/modified files
|
|
301
|
-
for (const file of files) {
|
|
302
|
-
const hash = simpleHash(file.content);
|
|
303
|
-
fileMap.set(file.path, { mtime: file.mtime, hash });
|
|
304
|
-
|
|
305
|
-
const cached = this.cache?.files.get(file.path);
|
|
306
|
-
if (force || !cached || cached.hash !== hash) {
|
|
307
|
-
changedFiles.push(file);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Check for deleted files
|
|
312
|
-
if (this.cache?.files) {
|
|
313
|
-
for (const cachedPath of this.cache.files.keys()) {
|
|
314
|
-
if (!fileMap.has(cachedPath)) {
|
|
315
|
-
deletedFiles.push(cachedPath);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const hasChanges = changedFiles.length > 0 || deletedFiles.length > 0;
|
|
321
|
-
const cacheHit = !force && !hasChanges;
|
|
322
|
-
|
|
323
|
-
if (cacheHit && this.cache?.tfidfIndex) {
|
|
324
|
-
// Cache hit - no indexing needed
|
|
325
|
-
this.status.progress = 100;
|
|
326
|
-
this.status.indexedFiles = 0;
|
|
327
|
-
this.status.isIndexing = false;
|
|
328
|
-
|
|
329
|
-
return {
|
|
330
|
-
tfidfIndex: this.cache.tfidfIndex,
|
|
331
|
-
stats: {
|
|
332
|
-
totalFiles: files.length,
|
|
333
|
-
indexedFiles: 0,
|
|
334
|
-
skippedFiles: files.length,
|
|
335
|
-
cacheHit: true,
|
|
336
|
-
},
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Store files in database for content retrieval
|
|
341
|
-
await this.db.initialize();
|
|
342
|
-
let processedCount = 0;
|
|
343
|
-
for (const file of files) {
|
|
344
|
-
await this.db.upsertCodebaseFile({
|
|
345
|
-
path: file.path,
|
|
346
|
-
mtime: file.mtime,
|
|
347
|
-
hash: simpleHash(file.content),
|
|
348
|
-
content: file.content,
|
|
349
|
-
language: file.language,
|
|
350
|
-
size: file.size,
|
|
351
|
-
indexedAt: new Date().toISOString(),
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// Update progress
|
|
355
|
-
processedCount++;
|
|
356
|
-
this.status.indexedFiles = processedCount;
|
|
357
|
-
this.status.currentFile = file.path;
|
|
358
|
-
this.status.progress = Math.floor((processedCount / files.length) * 100);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Build TF-IDF index
|
|
362
|
-
const documents = files.map((file) => ({
|
|
363
|
-
uri: `file://${file.path}`,
|
|
364
|
-
content: file.content,
|
|
365
|
-
}));
|
|
366
|
-
|
|
367
|
-
const tfidfIndex = await buildSearchIndex(documents, options.onProgress);
|
|
368
|
-
|
|
369
|
-
// Build vector index if embedding provider is available
|
|
370
|
-
let vectorStorage: VectorStorage | undefined;
|
|
371
|
-
if (embeddingProvider) {
|
|
372
|
-
const vectorIndexPath = path.join(this.cacheDir, 'codebase-vectors.hnsw');
|
|
373
|
-
vectorStorage = new VectorStorage(vectorIndexPath, embeddingProvider.dimensions);
|
|
374
|
-
|
|
375
|
-
// Generate embeddings in batches
|
|
376
|
-
const batchSize = 10;
|
|
377
|
-
for (let i = 0; i < files.length; i += batchSize) {
|
|
378
|
-
const batch = files.slice(i, i + batchSize);
|
|
379
|
-
const texts = batch.map((f) => f.content);
|
|
380
|
-
const embeddings = await embeddingProvider.generateEmbeddings(texts);
|
|
381
|
-
|
|
382
|
-
for (let j = 0; j < batch.length; j++) {
|
|
383
|
-
const file = batch[j];
|
|
384
|
-
const embedding = embeddings[j];
|
|
385
|
-
|
|
386
|
-
const doc: VectorDocument = {
|
|
387
|
-
id: `file://${file.path}`,
|
|
388
|
-
embedding,
|
|
389
|
-
metadata: {
|
|
390
|
-
type: 'code',
|
|
391
|
-
language: file.language || '',
|
|
392
|
-
content: file.content.slice(0, 500), // Store snippet
|
|
393
|
-
category: '',
|
|
394
|
-
},
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
vectorStorage.addDocument(doc);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
vectorStorage.save();
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Update cache
|
|
405
|
-
this.cache = {
|
|
406
|
-
version: '1.0.0',
|
|
407
|
-
codebaseRoot: this.codebaseRoot,
|
|
408
|
-
indexedAt: new Date().toISOString(),
|
|
409
|
-
fileCount: files.length,
|
|
410
|
-
files: fileMap,
|
|
411
|
-
tfidfIndex,
|
|
412
|
-
vectorIndexPath: vectorStorage
|
|
413
|
-
? path.join(this.cacheDir, 'codebase-vectors.hnsw')
|
|
414
|
-
: undefined,
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
await this.saveCache(this.cache);
|
|
418
|
-
|
|
419
|
-
// Update status
|
|
420
|
-
this.status.indexedFiles = changedFiles.length;
|
|
421
|
-
this.status.progress = 100;
|
|
422
|
-
|
|
423
|
-
return {
|
|
424
|
-
tfidfIndex,
|
|
425
|
-
vectorStorage,
|
|
426
|
-
stats: {
|
|
427
|
-
totalFiles: files.length,
|
|
428
|
-
indexedFiles: changedFiles.length,
|
|
429
|
-
skippedFiles: files.length - changedFiles.length,
|
|
430
|
-
cacheHit: false,
|
|
431
|
-
},
|
|
432
|
-
};
|
|
433
|
-
} finally {
|
|
434
|
-
// Reset indexing status
|
|
435
|
-
this.status.isIndexing = false;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Get cache statistics
|
|
441
|
-
*/
|
|
442
|
-
async getCacheStats(): Promise<{
|
|
443
|
-
exists: boolean;
|
|
444
|
-
fileCount: number;
|
|
445
|
-
indexedAt?: string;
|
|
446
|
-
}> {
|
|
447
|
-
try {
|
|
448
|
-
await this.db.initialize();
|
|
449
|
-
const stats = await this.db.getCodebaseIndexStats();
|
|
450
|
-
return {
|
|
451
|
-
exists: stats.totalFiles > 0,
|
|
452
|
-
fileCount: stats.totalFiles,
|
|
453
|
-
indexedAt: stats.indexedAt,
|
|
454
|
-
};
|
|
455
|
-
} catch (error) {
|
|
456
|
-
logger.error('Failed to get cache stats', { error });
|
|
457
|
-
return {
|
|
458
|
-
exists: false,
|
|
459
|
-
fileCount: 0,
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Search codebase using TF-IDF
|
|
466
|
-
*/
|
|
467
|
-
async search(
|
|
468
|
-
query: string,
|
|
469
|
-
options: {
|
|
470
|
-
limit?: number;
|
|
471
|
-
minScore?: number;
|
|
472
|
-
includeContent?: boolean;
|
|
473
|
-
} = {}
|
|
474
|
-
): Promise<
|
|
475
|
-
Array<{
|
|
476
|
-
path: string;
|
|
477
|
-
score: number;
|
|
478
|
-
content?: string;
|
|
479
|
-
language?: string;
|
|
480
|
-
}>
|
|
481
|
-
> {
|
|
482
|
-
const { limit = 10, minScore = 0, includeContent = true } = options;
|
|
483
|
-
|
|
484
|
-
// Load TF-IDF index from cache
|
|
485
|
-
const index = await this.buildTFIDFIndexFromDB();
|
|
486
|
-
if (!index) {
|
|
487
|
-
throw new Error('No search index available. Please run indexCodebase first.');
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Import search function
|
|
491
|
-
const { searchDocuments } = await import('./tfidf.js');
|
|
492
|
-
|
|
493
|
-
// Perform search
|
|
494
|
-
const searchResults = searchDocuments(query, index, {
|
|
495
|
-
limit,
|
|
496
|
-
minScore,
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
// Convert results and optionally include content
|
|
500
|
-
const results = [];
|
|
501
|
-
for (const result of searchResults) {
|
|
502
|
-
const filePath = result.uri?.replace('file://', '') || '';
|
|
503
|
-
let content: string | undefined;
|
|
504
|
-
|
|
505
|
-
if (includeContent) {
|
|
506
|
-
try {
|
|
507
|
-
const file = await this.db.getCodebaseFile(filePath);
|
|
508
|
-
content = file?.content;
|
|
509
|
-
} catch {
|
|
510
|
-
// Fallback: don't include content if unable to retrieve
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
results.push({
|
|
515
|
-
path: filePath,
|
|
516
|
-
score: result.score || 0,
|
|
517
|
-
content,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return results;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Start watching codebase directory for changes
|
|
526
|
-
* OPTIONAL: Only started when codebase tools are enabled in MCP server
|
|
527
|
-
*/
|
|
528
|
-
startWatching(): void {
|
|
529
|
-
if (this.watcher) {
|
|
530
|
-
logger.info('Codebase watcher already running');
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
try {
|
|
535
|
-
// Watch all files in codebase root, respecting .gitignore
|
|
536
|
-
this.watcher = chokidar.watch(this.codebaseRoot, {
|
|
537
|
-
ignored: [
|
|
538
|
-
/(^|[/\\])\../, // Ignore dotfiles
|
|
539
|
-
/node_modules/, // Ignore node_modules
|
|
540
|
-
/\.git\//, // Ignore .git
|
|
541
|
-
/\.sylphx-flow\//, // Ignore our own directory
|
|
542
|
-
'**/dist/**',
|
|
543
|
-
'**/build/**',
|
|
544
|
-
'**/coverage/**',
|
|
545
|
-
],
|
|
546
|
-
persistent: true,
|
|
547
|
-
ignoreInitial: true,
|
|
548
|
-
awaitWriteFinish: {
|
|
549
|
-
stabilityThreshold: 500,
|
|
550
|
-
pollInterval: 100,
|
|
551
|
-
},
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
this.watcher.on('all', (event, filePath) => {
|
|
555
|
-
// Only process text files
|
|
556
|
-
if (!isTextFile(filePath)) {
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Check against .gitignore
|
|
561
|
-
const relativePath = path.relative(this.codebaseRoot, filePath);
|
|
562
|
-
if (this.ig.ignores(relativePath)) {
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
logger.debug('Codebase file changed', { event, file: path.basename(filePath) });
|
|
567
|
-
|
|
568
|
-
// Debounce: Wait 5 seconds after last change before re-indexing
|
|
569
|
-
// (Longer than knowledge because code files save more frequently)
|
|
570
|
-
if (this.reindexTimer) {
|
|
571
|
-
clearTimeout(this.reindexTimer);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
this.reindexTimer = setTimeout(async () => {
|
|
575
|
-
logger.info('Re-indexing codebase due to file changes');
|
|
576
|
-
try {
|
|
577
|
-
await this.index();
|
|
578
|
-
logger.info('Codebase re-indexing complete');
|
|
579
|
-
} catch (error) {
|
|
580
|
-
logger.error('Codebase re-indexing failed', { error });
|
|
581
|
-
}
|
|
582
|
-
}, 5000);
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
logger.info('Watching codebase directory for changes', { codebaseRoot: this.codebaseRoot });
|
|
586
|
-
} catch (error) {
|
|
587
|
-
logger.error('Failed to start codebase file watching', { error });
|
|
588
|
-
// Don't throw - indexing can still work without watching
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Stop watching (for cleanup)
|
|
594
|
-
*/
|
|
595
|
-
stopWatching(): void {
|
|
596
|
-
if (this.reindexTimer) {
|
|
597
|
-
clearTimeout(this.reindexTimer);
|
|
598
|
-
this.reindexTimer = undefined;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if (this.watcher) {
|
|
602
|
-
this.watcher.close();
|
|
603
|
-
this.watcher = undefined;
|
|
604
|
-
logger.info('Stopped watching codebase directory');
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Clear cache
|
|
610
|
-
*/
|
|
611
|
-
async clearCache(): Promise<void> {
|
|
612
|
-
// Stop any pending reindex
|
|
613
|
-
if (this.reindexTimer) {
|
|
614
|
-
clearTimeout(this.reindexTimer);
|
|
615
|
-
this.reindexTimer = undefined;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
try {
|
|
619
|
-
// Clear database tables
|
|
620
|
-
await this.db.initialize();
|
|
621
|
-
await this.db.clearCodebaseIndex();
|
|
622
|
-
|
|
623
|
-
// Also clean up any old JSON files
|
|
624
|
-
const cachePath = path.join(this.cacheDir, 'codebase-index.json');
|
|
625
|
-
if (fs.existsSync(cachePath)) {
|
|
626
|
-
fs.unlinkSync(cachePath);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const vectorPath = path.join(this.cacheDir, 'codebase-vectors.hnsw');
|
|
630
|
-
if (fs.existsSync(vectorPath)) {
|
|
631
|
-
fs.unlinkSync(vectorPath);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
logger.info('Cache cleared from database and files');
|
|
635
|
-
} catch (error) {
|
|
636
|
-
logger.error('Failed to clear cache', { error });
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Convenience method that delegates to indexCodebase
|
|
642
|
-
* Used by file watcher
|
|
643
|
-
*/
|
|
644
|
-
private async index(): Promise<void> {
|
|
645
|
-
await this.indexCodebase();
|
|
646
|
-
}
|
|
647
|
-
}
|