@sylphx/flow 1.1.1 → 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +1 -1
  3. package/src/commands/hook-command.ts +10 -230
  4. package/src/composables/index.ts +0 -1
  5. package/src/config/servers.ts +35 -78
  6. package/src/core/interfaces.ts +0 -33
  7. package/src/domains/index.ts +0 -2
  8. package/src/index.ts +0 -4
  9. package/src/services/mcp-service.ts +0 -16
  10. package/src/targets/claude-code.ts +3 -9
  11. package/src/targets/functional/claude-code-logic.ts +4 -22
  12. package/src/targets/opencode.ts +0 -6
  13. package/src/types/mcp.types.ts +29 -38
  14. package/src/types/target.types.ts +0 -2
  15. package/src/types.ts +0 -1
  16. package/src/commands/codebase-command.ts +0 -168
  17. package/src/commands/knowledge-command.ts +0 -161
  18. package/src/composables/useTargetConfig.ts +0 -45
  19. package/src/core/formatting/bytes.test.ts +0 -115
  20. package/src/core/validation/limit.test.ts +0 -155
  21. package/src/core/validation/query.test.ts +0 -44
  22. package/src/domains/codebase/index.ts +0 -5
  23. package/src/domains/codebase/tools.ts +0 -139
  24. package/src/domains/knowledge/index.ts +0 -10
  25. package/src/domains/knowledge/resources.ts +0 -537
  26. package/src/domains/knowledge/tools.ts +0 -174
  27. package/src/services/search/base-indexer.ts +0 -156
  28. package/src/services/search/codebase-indexer-types.ts +0 -38
  29. package/src/services/search/codebase-indexer.ts +0 -647
  30. package/src/services/search/embeddings-provider.ts +0 -455
  31. package/src/services/search/embeddings.ts +0 -316
  32. package/src/services/search/functional-indexer.ts +0 -323
  33. package/src/services/search/index.ts +0 -27
  34. package/src/services/search/indexer.ts +0 -380
  35. package/src/services/search/knowledge-indexer.ts +0 -422
  36. package/src/services/search/semantic-search.ts +0 -244
  37. package/src/services/search/tfidf.ts +0 -559
  38. package/src/services/search/unified-search-service.ts +0 -888
  39. package/src/services/storage/cache-storage.ts +0 -487
  40. package/src/services/storage/drizzle-storage.ts +0 -581
  41. package/src/services/storage/index.ts +0 -15
  42. package/src/services/storage/lancedb-vector-storage.ts +0 -494
  43. package/src/services/storage/memory-storage.ts +0 -268
  44. package/src/services/storage/separated-storage.ts +0 -467
  45. 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
- }