directory-indexer 0.3.0 → 1.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"indexing.js","sources":["../src/indexing.ts"],"sourcesContent":["import { promises as fs } from 'fs';\nimport { join } from 'path';\nimport { Config } from './config.js';\nimport {\n FileInfo,\n ChunkInfo,\n normalizePath,\n getFileInfo,\n shouldIgnoreFile,\n isSupportedFileType,\n isDirectory,\n isFile\n} from './utils.js';\nimport { loadGitignoreRules } from './gitignore.js';\nimport { generateEmbedding } from './embedding.js';\nimport { initializeStorage, FileRecord } from './storage.js';\nimport { log } from './logger.js';\n\nexport interface ScanOptions {\n ignorePatterns: string[];\n maxFileSize: number;\n respectGitignore: boolean;\n}\n\nexport interface IndexResult {\n indexed: number;\n skipped: number;\n failed: number;\n deleted: number;\n errors: string[];\n}\n\nexport class IndexingError extends Error {\n constructor(message: string, public override cause?: Error) {\n super(message);\n this.name = 'IndexingError';\n }\n}\n\nexport function chunkText(content: string, chunkSize: number, overlap: number): ChunkInfo[] {\n if (content.length <= chunkSize) {\n return [{\n id: '0',\n content,\n startIndex: 0,\n endIndex: content.length\n }];\n }\n\n const chunks: ChunkInfo[] = [];\n let startIndex = 0;\n let chunkId = 0;\n\n while (startIndex < content.length) {\n const endIndex = Math.min(startIndex + chunkSize, content.length);\n const chunkContent = content.slice(startIndex, endIndex);\n\n chunks.push({\n id: chunkId.toString(),\n content: chunkContent,\n startIndex,\n endIndex\n });\n\n chunkId++;\n const nextStart = endIndex - overlap;\n\n if (nextStart <= startIndex) {\n startIndex = startIndex + Math.max(1, chunkSize - overlap);\n } else {\n startIndex = nextStart;\n }\n\n if (startIndex >= content.length) break;\n }\n\n return chunks;\n}\n\nexport async function scanDirectory(dirPath: string, options: ScanOptions): Promise<FileInfo[]> {\n const files: FileInfo[] = [];\n const visited = new Set<string>();\n const basePath = normalizePath(dirPath);\n \n // Load gitignore rules for the root directory if enabled\n const gitignoreFilter = options.respectGitignore ? await loadGitignoreRules(dirPath) : null;\n\n async function walkDirectory(currentPath: string): Promise<void> {\n const normalizedPath = normalizePath(currentPath);\n\n if (visited.has(normalizedPath)) {\n return;\n }\n visited.add(normalizedPath);\n\n try {\n // Convert to relative path for gitignore matching\n const relativePath = normalizedPath.startsWith(basePath) \n ? normalizedPath.slice(basePath.length + 1)\n : normalizedPath;\n \n if (shouldIgnoreFile(normalizedPath, relativePath, options.ignorePatterns, gitignoreFilter)) {\n return;\n }\n\n if (await isDirectory(normalizedPath)) {\n const entries = await fs.readdir(normalizedPath);\n\n for (const entry of entries) {\n const fullPath = join(normalizedPath, entry);\n await walkDirectory(fullPath);\n }\n } else if (await isFile(normalizedPath)) {\n if (!isSupportedFileType(normalizedPath)) {\n return;\n }\n\n const stats = await fs.stat(normalizedPath);\n if (stats.size > options.maxFileSize) {\n return;\n }\n\n const fileInfo = await getFileInfo(normalizedPath);\n files.push(fileInfo);\n }\n } catch (error) {\n throw new IndexingError(`Failed to scan directory: ${normalizedPath}`, error as Error);\n }\n }\n\n await walkDirectory(dirPath);\n return files;\n}\n\nexport async function getFileMetadata(filePath: string): Promise<FileInfo> {\n try {\n return await getFileInfo(filePath);\n } catch (error) {\n throw new IndexingError(`Failed to get file metadata`, error as Error);\n }\n}\n\nasync function shouldReprocessFile(filePath: string, existingRecord: FileRecord, config: Config): Promise<boolean> {\n try {\n const fs = await import('fs/promises');\n\n // Try modtime check first (fast path)\n const currentStats = await fs.stat(filePath);\n const existingModTime = new Date(existingRecord.modifiedTime);\n\n // If modtime is clearly older, likely unchanged\n if (currentStats.mtime <= existingModTime) {\n return false; // Skip processing\n }\n\n // If modtime suggests change, verify with hash\n const currentFileInfo = await getFileInfo(filePath);\n return currentFileInfo.hash !== existingRecord.hash;\n\n } catch (modtimeError) {\n // Graceful fallback: skip modtime, use hash only\n if (config.verbose) {\n console.log(`Warning: Could not check modification time for ${filePath}:`, modtimeError);\n }\n try {\n const currentFileInfo = await getFileInfo(filePath);\n return currentFileInfo.hash !== existingRecord.hash;\n } catch (hashError) {\n // If we can't hash either, assume changed to be safe\n if (config.verbose) {\n console.log(`Warning: Could not compute hash for ${filePath}:`, hashError);\n }\n return true;\n }\n }\n}\n\nexport async function indexDirectories(paths: string[], config: Config): Promise<IndexResult> {\n let indexed = 0;\n let skipped = 0;\n let failed = 0;\n let deleted = 0;\n const errors: string[] = [];\n\n const scanOptions: ScanOptions = {\n ignorePatterns: config.indexing.ignorePatterns,\n maxFileSize: config.indexing.maxFileSize,\n respectGitignore: config.indexing.respectGitignore\n };\n\n // Initialize storage\n const { sqlite, qdrant } = await initializeStorage(config);\n\n // First pass: scan all directories to get total file count\n let totalFiles = 0;\n for (const path of paths) {\n try {\n if (config.verbose) {\n console.log(`Scanning directory: ${path}`);\n }\n const files = await scanDirectory(path, scanOptions);\n totalFiles += files.length;\n if (config.verbose) {\n console.log(`Found ${files.length} files to process in ${path}`);\n }\n } catch {\n // Continue with other directories even if one fails to scan\n }\n }\n\n if (!config.verbose && totalFiles > 0) {\n // Add a blank line for spacing in the console output\n console.log('');\n console.log(`Found ${totalFiles} files to process (checking for changes...)`);\n }\n\n for (const path of paths) {\n try {\n // Mark directory as indexing\n const normalizedPath = normalizePath(path);\n await sqlite.upsertDirectory(normalizedPath, 'indexing');\n\n const files = await scanDirectory(path, scanOptions);\n\n // Track directory-specific counters\n const dirStartIndexed = indexed;\n const dirStartSkipped = skipped;\n\n // Calculate progress interval for non-verbose updates\n const progressInterval = totalFiles > 1000 ? 50 : 10;\n\n for (const file of files) {\n try {\n // Check if file already exists and needs reprocessing\n const existingFile = await sqlite.getFile(file.path);\n\n if (existingFile) {\n const needsReprocessing = await shouldReprocessFile(file.path, existingFile, config);\n if (!needsReprocessing) {\n skipped++;\n if (config.verbose) {\n console.log(` Skipped: ${file.path} (unchanged)`);\n }\n // Show periodic progress in non-verbose mode\n if (!config.verbose && (indexed + skipped) % progressInterval === 0) {\n console.log(`Progress: ${indexed + skipped}/${totalFiles} files (${skipped} skipped as unchanged)...`);\n }\n continue; // Skip unchanged file\n }\n\n // File changed - clean up old vectors first\n await qdrant.deletePointsByFilePath(file.path);\n }\n\n const rawContent = await fs.readFile(file.path, 'utf-8');\n\n // Skip non-UTF-8 files (Node.js inserts U+FFFD for invalid byte sequences)\n if (rawContent.includes('\\uFFFD')) {\n log('warning', 'Skipping non-UTF-8 file', { path: file.path });\n await sqlite.upsertFile(file, [], ['Skipped: file appears to be non-UTF-8 encoded']);\n skipped++;\n continue;\n }\n\n // Normalize CRLF to LF for consistent chunk boundaries\n const content = rawContent.replace(/\\r\\n/g, '\\n');\n const chunks = chunkText(content, config.indexing.chunkSize, config.indexing.chunkOverlap);\n\n // Store file metadata in SQLite\n await sqlite.upsertFile(file, chunks);\n\n // Generate embeddings and store in Qdrant\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i];\n const embedding = await generateEmbedding(chunk.content, config);\n // Generate a unique integer ID by combining hash and chunk index\n const hashNum = parseInt(file.hash.slice(0, 8), 16);\n const pointId = (hashNum % 1000000) * 1000 + parseInt(chunk.id);\n const point = {\n id: pointId,\n vector: embedding,\n payload: {\n filePath: file.path,\n chunkId: chunk.id,\n fileHash: file.hash,\n content: chunk.content,\n parentDirectories: file.parentDirs\n }\n };\n await qdrant.upsertPoints([point]);\n }\n\n indexed++;\n if (config.verbose) {\n console.log(` Indexed: ${file.path} (${chunks.length} chunks)`);\n }\n // Show periodic progress in non-verbose mode\n if (!config.verbose && (indexed + skipped) % progressInterval === 0) {\n console.log(`Progress: ${indexed + skipped}/${totalFiles} files (${skipped} skipped as unchanged)...`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const causeMessage = error instanceof Error && error.cause ? `: ${(error.cause as Error).message}` : '';\n const fullError = `Failed to process ${file.path}: ${errorMessage}${causeMessage}`;\n errors.push(fullError);\n failed++;\n\n // Print error immediately during processing (not just in verbose mode)\n console.error(`❌ ${fullError}`);\n }\n }\n\n // Clean up deleted files from this directory\n const indexedFiles = await sqlite.getFilesByDirectory(normalizedPath);\n const existingFilePaths = new Set(files.map(f => f.path));\n const deletedFiles = indexedFiles.filter(f => !existingFilePaths.has(f.path));\n\n for (const deletedFile of deletedFiles) {\n try {\n // Remove from Qdrant\n await qdrant.deletePointsByFilePath(deletedFile.path);\n \n // Remove from SQLite\n await sqlite.deleteFile(deletedFile.path);\n \n deleted++;\n if (config.verbose) {\n console.log(` Cleaned up deleted file: ${deletedFile.path}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const fullError = `Failed to clean up deleted file ${deletedFile.path}: ${errorMessage}`;\n errors.push(fullError);\n failed++;\n \n console.error(`❌ ${fullError}`);\n }\n }\n\n // Mark directory as completed if no errors for this directory\n const directoryErrors = errors.filter(err => err.includes(path));\n const directoryStatus = directoryErrors.length > 0 ? 'failed' : 'completed';\n await sqlite.upsertDirectory(normalizedPath, directoryStatus);\n\n // Show directory completion\n const dirFiles = files.length;\n const dirIndexed = indexed - dirStartIndexed;\n const dirSkipped = skipped - dirStartSkipped;\n if (config.verbose) {\n console.log(`Directory ${path} completed: ${dirIndexed} indexed, ${dirSkipped} skipped`);\n } else {\n console.log(`Directory ${path} completed: ${dirFiles} files processed`);\n }\n\n } catch (error) {\n const normalizedPath = normalizePath(path);\n await sqlite.upsertDirectory(normalizedPath, 'failed');\n errors.push(`Failed to scan directory ${path}: ${(error as Error).message}`);\n }\n }\n\n return { indexed, skipped, failed, deleted, errors };\n}"],"names":["fs"],"mappings":"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"}
1
+ {"version":3,"file":"indexing.js","sources":["../src/indexing.ts"],"sourcesContent":["import { promises as fs } from 'fs';\nimport { createHash } from 'node:crypto';\nimport { join } from 'path';\nimport { Config } from './config.js';\nimport {\n FileInfo,\n ChunkInfo,\n normalizePath,\n getFileInfo,\n shouldIgnoreFile,\n isSupportedFileType,\n isDirectory,\n isFile\n} from './utils.js';\nimport { loadGitignoreRules } from './gitignore.js';\nimport { generateEmbedding } from './embedding.js';\nimport { initializeStorage, ensureCollectionDimensions, FileRecord } from './storage.js';\nimport { log } from './logger.js';\n\nexport interface ScanOptions {\n ignorePatterns: string[];\n maxFileSize: number;\n respectGitignore: boolean;\n}\n\nexport interface IndexResult {\n indexed: number;\n skipped: number;\n failed: number;\n deleted: number;\n errors: string[];\n}\n\nexport interface IndexReporters {\n progress?: (message: string) => void;\n error?: (message: string) => void;\n}\n\nexport class IndexingError extends Error {\n constructor(message: string, public override cause?: Error) {\n super(message);\n this.name = 'IndexingError';\n }\n}\n\nexport function pointIdFor(filePath: string, chunkId: string): string {\n const hex = createHash('sha256').update(`${filePath}:${chunkId}`).digest('hex').slice(0, 32);\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\nexport function chunkText(content: string, chunkSize: number, overlap: number): ChunkInfo[] {\n if (content.length <= chunkSize) {\n return [{\n id: '0',\n content,\n startIndex: 0,\n endIndex: content.length\n }];\n }\n\n const chunks: ChunkInfo[] = [];\n let startIndex = 0;\n let chunkId = 0;\n\n while (startIndex < content.length) {\n const endIndex = Math.min(startIndex + chunkSize, content.length);\n const chunkContent = content.slice(startIndex, endIndex);\n\n chunks.push({\n id: chunkId.toString(),\n content: chunkContent,\n startIndex,\n endIndex\n });\n\n chunkId++;\n const nextStart = endIndex - overlap;\n\n if (nextStart <= startIndex) {\n startIndex = startIndex + Math.max(1, chunkSize - overlap);\n } else {\n startIndex = nextStart;\n }\n\n if (startIndex >= content.length) break;\n }\n\n return chunks;\n}\n\nexport async function scanDirectory(dirPath: string, options: ScanOptions): Promise<FileInfo[]> {\n const files: FileInfo[] = [];\n const visited = new Set<string>();\n const basePath = normalizePath(dirPath);\n \n // Load gitignore rules for the root directory if enabled\n const gitignoreFilter = options.respectGitignore ? await loadGitignoreRules(dirPath) : null;\n\n async function walkDirectory(currentPath: string): Promise<void> {\n const normalizedPath = normalizePath(currentPath);\n\n let realPath = normalizedPath;\n try {\n realPath = normalizePath(await fs.realpath(normalizedPath));\n } catch {\n realPath = normalizedPath;\n }\n\n if (visited.has(realPath)) {\n return;\n }\n visited.add(realPath);\n\n try {\n // Convert to relative path for gitignore matching\n const relativePath = normalizedPath.startsWith(basePath) \n ? normalizedPath.slice(basePath.length + 1)\n : normalizedPath;\n \n if (shouldIgnoreFile(normalizedPath, relativePath, options.ignorePatterns, gitignoreFilter)) {\n return;\n }\n\n if (await isDirectory(normalizedPath)) {\n const entries = await fs.readdir(normalizedPath);\n\n for (const entry of entries) {\n const fullPath = join(normalizedPath, entry);\n await walkDirectory(fullPath);\n }\n } else if (await isFile(normalizedPath)) {\n if (!isSupportedFileType(normalizedPath)) {\n return;\n }\n\n const stats = await fs.stat(normalizedPath);\n if (stats.size > options.maxFileSize) {\n return;\n }\n\n const fileInfo = await getFileInfo(normalizedPath);\n files.push(fileInfo);\n }\n } catch (error) {\n throw new IndexingError(`Failed to scan directory: ${normalizedPath}`, error as Error);\n }\n }\n\n await walkDirectory(dirPath);\n return files;\n}\n\nexport async function getFileMetadata(filePath: string): Promise<FileInfo> {\n try {\n return await getFileInfo(filePath);\n } catch (error) {\n throw new IndexingError(`Failed to get file metadata`, error as Error);\n }\n}\n\nasync function shouldReprocessFile(filePath: string, existingRecord: FileRecord, config: Config): Promise<boolean> {\n try {\n const fs = await import('fs/promises');\n\n // Try modtime check first (fast path)\n const currentStats = await fs.stat(filePath);\n const existingModTime = new Date(existingRecord.modifiedTime);\n\n // An exactly-unchanged mtime skips; older or newer mtimes verify via hash\n // so restored or backed-up files with stale timestamps are re-examined\n if (currentStats.mtime.getTime() === existingModTime.getTime()) {\n return false;\n }\n\n // If modtime suggests change, verify with hash\n const currentFileInfo = await getFileInfo(filePath);\n return currentFileInfo.hash !== existingRecord.hash;\n\n } catch (modtimeError) {\n // Graceful fallback: skip modtime, use hash only\n if (config.verbose) {\n log('warning', `Could not check modification time for ${filePath}`, {\n error: modtimeError instanceof Error ? modtimeError.message : String(modtimeError)\n });\n }\n try {\n const currentFileInfo = await getFileInfo(filePath);\n return currentFileInfo.hash !== existingRecord.hash;\n } catch (hashError) {\n // If we can't hash either, assume changed to be safe\n if (config.verbose) {\n log('warning', `Could not compute hash for ${filePath}`, {\n error: hashError instanceof Error ? hashError.message : String(hashError)\n });\n }\n return true;\n }\n }\n}\n\nexport async function indexDirectories(paths: string[], config: Config, reporters: IndexReporters = {}): Promise<IndexResult> {\n const progress = reporters.progress ?? ((message: string) => log('info', message));\n const reportError = reporters.error ?? ((message: string) => log('error', message));\n let indexed = 0;\n let skipped = 0;\n let failed = 0;\n let deleted = 0;\n let collectionEnsured = false;\n const errors: string[] = [];\n\n const scanOptions: ScanOptions = {\n ignorePatterns: config.indexing.ignorePatterns,\n maxFileSize: config.indexing.maxFileSize,\n respectGitignore: config.indexing.respectGitignore\n };\n\n // Initialize storage\n const { sqlite, qdrant } = await initializeStorage(config);\n\n // First pass: scan all directories to get total file count\n let totalFiles = 0;\n for (const path of paths) {\n try {\n if (config.verbose) {\n progress(`Scanning directory: ${path}`);\n }\n const files = await scanDirectory(path, scanOptions);\n totalFiles += files.length;\n if (config.verbose) {\n progress(`Found ${files.length} files to process in ${path}`);\n }\n } catch {\n // Continue with other directories even if one fails to scan\n }\n }\n\n if (!config.verbose && totalFiles > 0) {\n // Add a blank line for spacing in the console output\n progress('');\n progress(`Found ${totalFiles} files to process (checking for changes...)`);\n }\n\n for (const path of paths) {\n try {\n // Mark directory as indexing\n const normalizedPath = normalizePath(path);\n await sqlite.upsertDirectory(normalizedPath, 'indexing');\n\n const files = await scanDirectory(path, scanOptions);\n\n // Track directory-specific counters\n const dirStartIndexed = indexed;\n const dirStartSkipped = skipped;\n let dirHadFailure = false;\n\n // Calculate progress interval for non-verbose updates\n const progressInterval = totalFiles > 1000 ? 50 : 10;\n\n for (const file of files) {\n let chunks: ChunkInfo[] = [];\n try {\n // Check if file already exists and needs reprocessing\n const existingFile = await sqlite.getFile(file.path);\n\n if (existingFile) {\n const hasStoredFailure =\n existingFile.errors?.some(error => error.startsWith('Failed to process')) ?? false;\n const needsReprocessing =\n hasStoredFailure || (await shouldReprocessFile(file.path, existingFile, config));\n if (!needsReprocessing) {\n skipped++;\n if (config.verbose) {\n progress(` Skipped: ${file.path} (unchanged)`);\n }\n // Show periodic progress in non-verbose mode\n if (!config.verbose && (indexed + skipped) % progressInterval === 0) {\n progress(`Progress: ${indexed + skipped}/${totalFiles} files (${skipped} skipped as unchanged)...`);\n }\n continue; // Skip unchanged file\n }\n\n // File changed - clean up old vectors first\n await qdrant.deletePointsByFilePath(file.path);\n }\n\n const rawContent = await fs.readFile(file.path, 'utf-8');\n\n // Skip non-UTF-8 files (Node.js inserts U+FFFD for invalid byte sequences)\n if (rawContent.includes('\\uFFFD')) {\n log('warning', 'Skipping non-UTF-8 file', { path: file.path });\n await sqlite.upsertFile(file, [], ['Skipped: file appears to be non-UTF-8 encoded']);\n skipped++;\n continue;\n }\n\n // Normalize CRLF to LF for consistent chunk boundaries\n const content = rawContent.replace(/\\r\\n/g, '\\n');\n chunks = chunkText(content, config.indexing.chunkSize, config.indexing.chunkOverlap);\n\n // Store file metadata in SQLite\n await sqlite.upsertFile(file, chunks);\n\n // Generate embeddings and store in Qdrant\n for (let i = 0; i < chunks.length; i++) {\n const chunk = chunks[i];\n const embedding = await generateEmbedding(chunk.content, config);\n if (!collectionEnsured) {\n await ensureCollectionDimensions(sqlite, qdrant, config, embedding.length);\n collectionEnsured = true;\n }\n const point = {\n id: pointIdFor(file.path, chunk.id),\n vector: embedding,\n payload: {\n filePath: file.path,\n chunkId: chunk.id,\n fileHash: file.hash,\n content: chunk.content,\n parentDirectories: file.parentDirs\n }\n };\n await qdrant.upsertPoints([point]);\n }\n\n indexed++;\n if (config.verbose) {\n progress(` Indexed: ${file.path} (${chunks.length} chunks)`);\n }\n // Show periodic progress in non-verbose mode\n if (!config.verbose && (indexed + skipped) % progressInterval === 0) {\n progress(`Progress: ${indexed + skipped}/${totalFiles} files (${skipped} skipped as unchanged)...`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const causeMessage = error instanceof Error && error.cause ? `: ${(error.cause as Error).message}` : '';\n const fullError = `Failed to process ${file.path}: ${errorMessage}${causeMessage}`;\n errors.push(fullError);\n failed++;\n dirHadFailure = true;\n\n // Print error immediately during processing (not just in verbose mode)\n reportError(`❌ ${fullError}`);\n\n // Record the failure on the file row so a later run reprocesses it\n // instead of skipping it as unchanged\n await sqlite.upsertFile(file, chunks, [fullError]);\n }\n }\n\n // Clean up deleted files from this directory\n const indexedFiles = await sqlite.getFilesByDirectory(normalizedPath);\n const existingFilePaths = new Set(files.map(f => f.path));\n const deletedFiles = indexedFiles.filter(f => !existingFilePaths.has(f.path));\n\n for (const deletedFile of deletedFiles) {\n try {\n // Remove from Qdrant\n await qdrant.deletePointsByFilePath(deletedFile.path);\n \n // Remove from SQLite\n await sqlite.deleteFile(deletedFile.path);\n \n deleted++;\n if (config.verbose) {\n progress(` Cleaned up deleted file: ${deletedFile.path}`);\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const fullError = `Failed to clean up deleted file ${deletedFile.path}: ${errorMessage}`;\n errors.push(fullError);\n failed++;\n dirHadFailure = true;\n\n reportError(`❌ ${fullError}`);\n }\n }\n\n // Mark directory as completed unless a file or cleanup failure occurred\n const directoryStatus = dirHadFailure ? 'failed' : 'completed';\n await sqlite.upsertDirectory(normalizedPath, directoryStatus);\n\n // Show directory completion\n const dirFiles = files.length;\n const dirIndexed = indexed - dirStartIndexed;\n const dirSkipped = skipped - dirStartSkipped;\n if (config.verbose) {\n progress(`Directory ${path} completed: ${dirIndexed} indexed, ${dirSkipped} skipped`);\n } else {\n progress(`Directory ${path} completed: ${dirFiles} files processed`);\n }\n\n } catch (error) {\n const normalizedPath = normalizePath(path);\n await sqlite.upsertDirectory(normalizedPath, 'failed');\n errors.push(`Failed to scan directory ${path}: ${(error as Error).message}`);\n }\n }\n\n return { indexed, skipped, failed, deleted, errors };\n}"],"names":["fs"],"mappings":"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"}
@@ -1,8 +1,9 @@
1
1
  import { indexDirectories } from "./indexing.js";
2
2
  import { getChunkContent, getFileContent, searchContent, findSimilarFiles } from "./search.js";
3
- import { initializeStorage, getIndexStatus, SQLiteStorage } from "./storage.js";
4
- import { validateIndexPrerequisites, validateSearchPrerequisites } from "./prerequisites.js";
3
+ import { getIndexStatus, initializeStorage } from "./storage.js";
4
+ import { validateSearchPrerequisites, validateIndexPrerequisites } from "./prerequisites.js";
5
5
  import { validatePathWithinIndexedDirs, resolveIndexedDirectories } from "./path-validation.js";
6
+ import { normalizePath } from "./utils.js";
6
7
  import { log } from "./logger.js";
7
8
  import { resolve, sep } from "path";
8
9
  import { realpathSync } from "fs";
@@ -10,6 +11,10 @@ let mcpServer = null;
10
11
  function setMcpServer(server) {
11
12
  mcpServer = server;
12
13
  }
14
+ function notifyClient(level, data) {
15
+ mcpServer?.sendLoggingMessage({ level, data })?.catch(() => {
16
+ });
17
+ }
13
18
  function normalizeMutexKey(dirPath) {
14
19
  let normalized;
15
20
  try {
@@ -23,6 +28,28 @@ function normalizeMutexKey(dirPath) {
23
28
  return normalized;
24
29
  }
25
30
  const indexingMutex = /* @__PURE__ */ new Map();
31
+ async function withDirectoryMutex(keys, fn) {
32
+ const holders = keys.map((key) => indexingMutex.get(key));
33
+ const prior = Promise.all(holders.map((holder) => holder ?? Promise.resolve()));
34
+ let release;
35
+ const done = new Promise((resolve2) => {
36
+ release = resolve2;
37
+ });
38
+ const ticket = prior.then(() => done);
39
+ for (const key of keys) indexingMutex.set(key, ticket);
40
+ if (holders.some(Boolean)) {
41
+ log("info", "Waiting for ongoing indexing", { directories: keys });
42
+ }
43
+ await prior;
44
+ try {
45
+ return await fn();
46
+ } finally {
47
+ release();
48
+ for (const key of keys) {
49
+ if (indexingMutex.get(key) === ticket) indexingMutex.delete(key);
50
+ }
51
+ }
52
+ }
26
53
  let indexedDirsCache = /* @__PURE__ */ new Set();
27
54
  let indexedDirsCacheInitialized = false;
28
55
  function refreshIndexedDirsCache(storage) {
@@ -31,12 +58,8 @@ function refreshIndexedDirsCache(storage) {
31
58
  }
32
59
  async function ensureIndexedDirsCache(config) {
33
60
  if (!indexedDirsCacheInitialized) {
34
- const sqlite = new SQLiteStorage(config);
35
- try {
36
- refreshIndexedDirsCache(sqlite);
37
- } finally {
38
- sqlite.close();
39
- }
61
+ const { sqlite } = await initializeStorage(config);
62
+ refreshIndexedDirsCache(sqlite);
40
63
  }
41
64
  }
42
65
  function isIndexToolArgs(args) {
@@ -61,78 +84,58 @@ async function handleIndexTool(args, config) {
61
84
  if (!isIndexToolArgs(args)) {
62
85
  throw new Error("directory_paths is required and must be an array");
63
86
  }
64
- await validateIndexPrerequisites(config);
65
87
  const paths = args.directory_paths.map((p) => p.trim());
66
88
  const mutexKeys = paths.map(normalizeMutexKey);
67
- log("info", "Index start", { directories: paths });
68
- mcpServer?.sendLoggingMessage({ level: "info", data: { event: "index_start", directories: paths } });
69
- for (const key of mutexKeys) {
70
- const existing = indexingMutex.get(key);
71
- if (existing) {
72
- log("info", "Waiting for ongoing indexing", { directory: key });
73
- await existing;
74
- }
75
- }
76
- let resolveIndexing;
77
- const indexingPromise = new Promise((resolve2) => {
78
- resolveIndexing = resolve2;
79
- });
80
- for (const key of mutexKeys) {
81
- indexingMutex.set(key, indexingPromise);
82
- }
83
- try {
84
- const result = await indexDirectories(paths, config);
85
- const { sqlite } = await initializeStorage(config);
89
+ return withDirectoryMutex(mutexKeys, async () => {
90
+ await validateIndexPrerequisites(config);
91
+ log("info", "Index start", { directories: paths });
92
+ notifyClient("info", { event: "index_start", directories: paths });
86
93
  try {
94
+ const result = await indexDirectories(paths, config);
95
+ const { sqlite } = await initializeStorage(config);
87
96
  refreshIndexedDirsCache(sqlite);
88
- } finally {
89
- sqlite.close();
90
- }
91
- log("info", "Index complete", { result });
92
- mcpServer?.sendLoggingMessage({ level: "info", data: { event: "index_complete", result } });
93
- let responseText = `Indexed ${result.indexed} files, skipped ${result.skipped} files, cleaned up ${result.deleted} deleted files, ${result.failed} failed`;
94
- if (result.errors.length > 0) {
95
- responseText += `
97
+ log("info", "Index complete", { result });
98
+ notifyClient("info", { event: "index_complete", result });
99
+ let responseText = `Indexed ${result.indexed} files, skipped ${result.skipped} files, cleaned up ${result.deleted} deleted files, ${result.failed} failed`;
100
+ if (result.errors.length > 0) {
101
+ responseText += `
96
102
  Errors: [
97
103
  `;
98
- result.errors.forEach((error) => {
99
- responseText += ` '${error}'
104
+ result.errors.forEach((error) => {
105
+ responseText += ` '${error}'
100
106
  `;
101
- });
102
- responseText += `]`;
103
- }
104
- return {
105
- content: [
106
- {
107
- type: "text",
108
- text: responseText
109
- }
110
- ]
111
- };
112
- } catch (error) {
113
- const errorMessage = error instanceof Error ? error.message : String(error);
114
- log("error", "Index error", { error: errorMessage, directories: paths });
115
- mcpServer?.sendLoggingMessage({ level: "error", data: { event: "index_error", error: errorMessage } });
116
- throw new Error(
117
- `Indexing failed for ${paths.join(", ")}. Verify the directory exists and is readable. Use 'server_info' to check current status.`
118
- );
119
- } finally {
120
- resolveIndexing();
121
- for (const key of mutexKeys) {
122
- indexingMutex.delete(key);
107
+ });
108
+ responseText += `]`;
109
+ }
110
+ return {
111
+ content: [
112
+ {
113
+ type: "text",
114
+ text: responseText
115
+ }
116
+ ]
117
+ };
118
+ } catch (error) {
119
+ const errorMessage = error instanceof Error ? error.message : String(error);
120
+ log("error", "Index error", { error: errorMessage, directories: paths });
121
+ notifyClient("error", { event: "index_error", error: errorMessage });
122
+ throw new Error(
123
+ `Indexing failed for ${paths.join(", ")}. Verify the directory exists and is readable. Use 'server_info' to check current status.`
124
+ );
123
125
  }
124
- }
126
+ });
125
127
  }
126
128
  async function validateWorkspace(workspace) {
127
- if (!workspace) return { workspace };
129
+ if (!workspace) return workspace;
128
130
  const config = (await import("./config.js")).loadConfig();
129
131
  const { getAvailableWorkspaces } = await import("./config.js");
130
132
  const availableWorkspaces = getAvailableWorkspaces(config);
131
133
  if (availableWorkspaces.includes(workspace)) {
132
- return { workspace };
134
+ return workspace;
133
135
  }
134
- const message = availableWorkspaces.length > 0 ? `Note: Workspace '${workspace}' not found. Searching all content instead. Available workspaces: ${availableWorkspaces.join(", ")}. Use server_info tool to see workspace details.` : `Note: Workspace '${workspace}' not found and no workspaces are configured. Searching all indexed content.`;
135
- return { workspace: void 0, message };
136
+ throw new Error(
137
+ availableWorkspaces.length > 0 ? `Workspace '${workspace}' not found. Available workspaces: ${availableWorkspaces.join(", ")}` : `Workspace '${workspace}' not found and no workspaces are configured.`
138
+ );
136
139
  }
137
140
  async function handleSearchTool(args) {
138
141
  if (!isSearchToolArgs(args)) {
@@ -140,13 +143,10 @@ async function handleSearchTool(args) {
140
143
  }
141
144
  const config = (await import("./config.js")).loadConfig();
142
145
  await validateSearchPrerequisites(config);
143
- const { workspace, message } = await validateWorkspace(args.workspace);
146
+ const workspace = await validateWorkspace(args.workspace);
144
147
  const results = await searchContent(args.query, { limit: args.limit || 10, workspace });
145
- const response = message ? `${message}
146
-
147
- ${JSON.stringify(results, null, 2)}` : JSON.stringify(results, null, 2);
148
148
  return {
149
- content: [{ type: "text", text: response }]
149
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
150
150
  };
151
151
  }
152
152
  async function handleSimilarFilesTool(args) {
@@ -155,13 +155,12 @@ async function handleSimilarFilesTool(args) {
155
155
  }
156
156
  const config = (await import("./config.js")).loadConfig();
157
157
  await validateSearchPrerequisites(config);
158
- const { workspace, message } = await validateWorkspace(args.workspace);
158
+ await ensureIndexedDirsCache(config);
159
+ validatePathWithinIndexedDirs(args.file_path, indexedDirsCache);
160
+ const workspace = await validateWorkspace(args.workspace);
159
161
  const results = await findSimilarFiles(args.file_path, args.limit || 10, workspace);
160
- const response = message ? `${message}
161
-
162
- ${JSON.stringify(results, null, 2)}` : JSON.stringify(results, null, 2);
163
162
  return {
164
- content: [{ type: "text", text: response }]
163
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
165
164
  };
166
165
  }
167
166
  async function handleGetContentTool(args, config) {
@@ -237,27 +236,22 @@ async function handleDeleteIndexTool(args, config) {
237
236
  if (!isDeleteIndexToolArgs(args)) {
238
237
  throw new Error("directory_path is required");
239
238
  }
240
- const dirPath = args.directory_path.trim();
241
- const mutexKey = normalizeMutexKey(dirPath);
242
- const existing = indexingMutex.get(mutexKey);
243
- if (existing) {
244
- log("info", "Waiting for ongoing indexing before delete", { directory: dirPath });
245
- await existing;
246
- }
247
- let resolveDelete;
248
- const deletePromise = new Promise((r) => {
249
- resolveDelete = r;
250
- });
251
- indexingMutex.set(mutexKey, deletePromise);
252
- const { sqlite, qdrant } = await initializeStorage(config);
253
- try {
254
- const directory = await sqlite.getDirectory(dirPath);
239
+ const trimmedPath = args.directory_path.trim();
240
+ const dirPath = normalizePath(trimmedPath);
241
+ const mutexKey = normalizeMutexKey(trimmedPath);
242
+ return withDirectoryMutex([mutexKey], async () => {
243
+ const { sqlite, qdrant } = await initializeStorage(config);
244
+ let directory = await sqlite.getDirectory(dirPath);
245
+ if (!directory && process.platform === "win32") {
246
+ directory = await sqlite.getDirectoryByCaseInsensitive(dirPath);
247
+ }
255
248
  if (!directory) {
256
249
  throw new Error(
257
250
  `Directory '${dirPath}' is not indexed. Use 'server_info' to see indexed directories.`
258
251
  );
259
252
  }
260
- const files = await sqlite.getFilesByDirectory(dirPath);
253
+ const storedPath = directory.path;
254
+ const files = await sqlite.getFilesByDirectory(storedPath);
261
255
  const vectorErrors = [];
262
256
  for (const file of files) {
263
257
  try {
@@ -277,25 +271,21 @@ async function handleDeleteIndexTool(args, config) {
277
271
  ` + vectorErrors.join("\n")
278
272
  );
279
273
  }
280
- const deletedFiles = sqlite.deleteFilesByDirectory(dirPath);
281
- sqlite.deleteDirectory(dirPath);
274
+ const deletedFiles = sqlite.deleteFilesByDirectory(storedPath);
275
+ sqlite.deleteDirectory(storedPath);
282
276
  refreshIndexedDirsCache(sqlite);
283
277
  const chunksCount = files.reduce((sum, f) => sum + (f.chunks?.length || 0), 0);
284
- log("info", "Index deleted", { directory: dirPath, files: deletedFiles, chunks: chunksCount });
285
- mcpServer?.sendLoggingMessage({ level: "info", data: { event: "index_deleted", directory: dirPath } });
278
+ log("info", "Index deleted", { directory: storedPath, files: deletedFiles, chunks: chunksCount });
279
+ notifyClient("info", { event: "index_deleted", directory: storedPath });
286
280
  return {
287
281
  content: [
288
282
  {
289
283
  type: "text",
290
- text: `Deleted index for ${dirPath}: removed ${deletedFiles} files and ${chunksCount} chunks`
284
+ text: `Deleted index for ${storedPath}: removed ${deletedFiles} files and ${chunksCount} chunks`
291
285
  }
292
286
  ]
293
287
  };
294
- } finally {
295
- resolveDelete();
296
- indexingMutex.delete(mutexKey);
297
- sqlite.close();
298
- }
288
+ });
299
289
  }
300
290
  function formatErrorResponse(error) {
301
291
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-handlers.js","sources":["../src/mcp-handlers.ts"],"sourcesContent":["import { Config } from './config.js';\nimport { indexDirectories } from './indexing.js';\nimport { searchContent, findSimilarFiles, getFileContent, getChunkContent } from './search.js';\nimport { getIndexStatus, SQLiteStorage, initializeStorage } from './storage.js';\nimport { validateIndexPrerequisites, validateSearchPrerequisites } from './prerequisites.js';\nimport { validatePathWithinIndexedDirs, resolveIndexedDirectories } from './path-validation.js';\nimport { log } from './logger.js';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { resolve, sep } from 'path';\nimport { realpathSync } from 'fs';\n\n// MCP server reference for sending client-visible log notifications\nlet mcpServer: Server | null = null;\n\n/**\n * Set the MCP server reference for logging notifications.\n * Called from startMcpServer() after server creation.\n */\nexport function setMcpServer(server: Server): void {\n mcpServer = server;\n}\n\n/**\n * Normalize a directory path for use as a mutex key.\n * Resolves symlinks, makes absolute, and strips trailing separators\n * so that '/repo', '/repo/', and symlinks all map to the same key.\n */\nfunction normalizeMutexKey(dirPath: string): string {\n let normalized: string;\n try {\n normalized = realpathSync(resolve(dirPath));\n } catch {\n // Directory may not exist yet; fall back to resolve only\n normalized = resolve(dirPath);\n }\n // Strip trailing separator (but keep root '/' or 'C:\\')\n while (normalized.length > 1 && normalized.endsWith(sep)) {\n normalized = normalized.slice(0, -sep.length);\n }\n return normalized;\n}\n\n// Workspace-level indexing mutex: keyed by normalized directory path\nconst indexingMutex = new Map<string, Promise<void>>();\n\n// Cached set of resolved indexed directory paths for path validation\nlet indexedDirsCache: Set<string> = new Set();\nlet indexedDirsCacheInitialized = false;\n\n/**\n * Refresh the indexed directories cache from storage.\n * Exported for testability.\n */\nexport function refreshIndexedDirsCache(storage: SQLiteStorage): void {\n indexedDirsCache = resolveIndexedDirectories(storage);\n indexedDirsCacheInitialized = true;\n}\n\n/**\n * Ensure the cache is populated, lazily initializing from SQLite only (no Qdrant).\n * Uses a boolean flag so an empty result doesn't re-trigger initialization.\n */\nasync function ensureIndexedDirsCache(config: Config): Promise<void> {\n if (!indexedDirsCacheInitialized) {\n const sqlite = new SQLiteStorage(config);\n try {\n refreshIndexedDirsCache(sqlite);\n } finally {\n sqlite.close();\n }\n }\n}\n\n// Type-safe interfaces for MCP tool arguments\ninterface IndexToolArgs {\n directory_paths: string[];\n}\n\ninterface SearchToolArgs {\n query: string;\n limit?: number;\n workspace?: string;\n}\n\ninterface SimilarFilesToolArgs {\n file_path: string;\n limit?: number;\n workspace?: string;\n}\n\ninterface GetContentToolArgs {\n file_path: string;\n chunks?: string;\n}\n\ninterface GetChunkToolArgs {\n file_path: string;\n chunk_id: string;\n}\n\ninterface DeleteIndexToolArgs {\n directory_path: string;\n}\n\n// Type guard functions\nfunction isIndexToolArgs(args: unknown): args is IndexToolArgs {\n return typeof args === 'object' && args !== null &&\n Array.isArray((args as IndexToolArgs).directory_paths);\n}\n\nfunction isSearchToolArgs(args: unknown): args is SearchToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as SearchToolArgs).query === 'string';\n}\n\nfunction isSimilarFilesToolArgs(args: unknown): args is SimilarFilesToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as SimilarFilesToolArgs).file_path === 'string';\n}\n\nfunction isGetContentToolArgs(args: unknown): args is GetContentToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as GetContentToolArgs).file_path === 'string';\n}\n\nfunction isGetChunkToolArgs(args: unknown): args is GetChunkToolArgs {\n return typeof args === 'object' && args !== null &&\n typeof (args as GetChunkToolArgs).file_path === 'string' &&\n typeof (args as GetChunkToolArgs).chunk_id === 'string';\n}\n\nfunction isDeleteIndexToolArgs(args: unknown): args is DeleteIndexToolArgs {\n return typeof args === 'object' && args !== null &&\n typeof (args as DeleteIndexToolArgs).directory_path === 'string';\n}\n\nexport async function handleIndexTool(args: unknown, config: Config): Promise<CallToolResult> {\n if (!isIndexToolArgs(args)) {\n throw new Error('directory_paths is required and must be an array');\n }\n\n // Validate prerequisites before proceeding\n await validateIndexPrerequisites(config);\n\n const paths = args.directory_paths.map((p: string) => p.trim());\n const mutexKeys = paths.map(normalizeMutexKey);\n\n log('info', 'Index start', { directories: paths });\n mcpServer?.sendLoggingMessage({ level: 'info', data: { event: 'index_start', directories: paths } });\n\n // Per-directory mutex: serialize concurrent calls targeting the same directory\n for (const key of mutexKeys) {\n const existing = indexingMutex.get(key);\n if (existing) {\n log('info', 'Waiting for ongoing indexing', { directory: key });\n await existing;\n }\n }\n\n // Create a deferred promise for this indexing operation\n let resolveIndexing: () => void;\n const indexingPromise = new Promise<void>((resolve) => { resolveIndexing = resolve; });\n for (const key of mutexKeys) {\n indexingMutex.set(key, indexingPromise);\n }\n\n try {\n const result = await indexDirectories(paths, config);\n\n // Refresh the indexed directories cache after successful indexing\n const { sqlite } = await initializeStorage(config);\n try {\n refreshIndexedDirsCache(sqlite);\n } finally {\n sqlite.close();\n }\n\n log('info', 'Index complete', { result });\n mcpServer?.sendLoggingMessage({ level: 'info', data: { event: 'index_complete', result } });\n\n let responseText = `Indexed ${result.indexed} files, skipped ${result.skipped} files, cleaned up ${result.deleted} deleted files, ${result.failed} failed`;\n\n if (result.errors.length > 0) {\n responseText += `\\nErrors: [\\n`;\n result.errors.forEach(error => {\n responseText += ` '${error}'\\n`;\n });\n responseText += `]`;\n }\n\n return {\n content: [\n {\n type: 'text',\n text: responseText\n }\n ]\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n log('error', 'Index error', { error: errorMessage, directories: paths });\n mcpServer?.sendLoggingMessage({ level: 'error', data: { event: 'index_error', error: errorMessage } });\n throw new Error(\n `Indexing failed for ${paths.join(', ')}. Verify the directory exists and is readable. Use 'server_info' to check current status.`\n );\n } finally {\n resolveIndexing!();\n for (const key of mutexKeys) {\n indexingMutex.delete(key);\n }\n }\n}\n\nasync function validateWorkspace(workspace?: string): Promise<{ workspace?: string; message?: string }> {\n if (!workspace) return { workspace };\n \n const config = (await import('./config.js')).loadConfig();\n const { getAvailableWorkspaces } = await import('./config.js');\n const availableWorkspaces = getAvailableWorkspaces(config);\n \n if (availableWorkspaces.includes(workspace)) {\n return { workspace };\n }\n \n // Invalid workspace - search all content with informative message\n const message = availableWorkspaces.length > 0\n ? `Note: Workspace '${workspace}' not found. Searching all content instead. Available workspaces: ${availableWorkspaces.join(', ')}. Use server_info tool to see workspace details.`\n : `Note: Workspace '${workspace}' not found and no workspaces are configured. Searching all indexed content.`;\n \n return { workspace: undefined, message };\n}\n\nexport async function handleSearchTool(args: unknown): Promise<CallToolResult> {\n if (!isSearchToolArgs(args)) {\n throw new Error('query is required');\n }\n \n // Validate prerequisites before proceeding\n const config = (await import('./config.js')).loadConfig();\n await validateSearchPrerequisites(config);\n \n const { workspace, message } = await validateWorkspace(args.workspace);\n const results = await searchContent(args.query, { limit: args.limit || 10, workspace });\n \n const response = message \n ? `${message}\\n\\n${JSON.stringify(results, null, 2)}`\n : JSON.stringify(results, null, 2);\n \n return {\n content: [{ type: 'text', text: response }]\n };\n}\n\nexport async function handleSimilarFilesTool(args: unknown): Promise<CallToolResult> {\n if (!isSimilarFilesToolArgs(args)) {\n throw new Error('file_path is required');\n }\n \n // Validate prerequisites before proceeding\n const config = (await import('./config.js')).loadConfig();\n await validateSearchPrerequisites(config);\n \n const { workspace, message } = await validateWorkspace(args.workspace);\n const results = await findSimilarFiles(args.file_path, args.limit || 10, workspace);\n \n const response = message \n ? `${message}\\n\\n${JSON.stringify(results, null, 2)}`\n : JSON.stringify(results, null, 2);\n \n return {\n content: [{ type: 'text', text: response }]\n };\n}\n\nexport async function handleGetContentTool(args: unknown, config?: Config): Promise<CallToolResult> {\n if (!isGetContentToolArgs(args)) {\n throw new Error('file_path is required');\n }\n\n // Lazily populate cache and validate path\n const resolvedConfig = config || (await import('./config.js')).loadConfig();\n await ensureIndexedDirsCache(resolvedConfig);\n validatePathWithinIndexedDirs(args.file_path, indexedDirsCache);\n\n try {\n const content = await getFileContent(args.file_path, args.chunks);\n\n return {\n content: [\n {\n type: 'text',\n text: content\n }\n ]\n };\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n if (msg.includes('ENOENT') || msg.toLowerCase().includes('not found') || msg.toLowerCase().includes('no such file')) {\n throw new Error(\n `File not found: ${args.file_path}. The file may have been moved or deleted. Use 'search' to find similar content.`\n );\n }\n throw error;\n }\n}\n\nexport async function handleGetChunkTool(args: unknown, config?: Config): Promise<CallToolResult> {\n if (!isGetChunkToolArgs(args)) {\n throw new Error('file_path and chunk_id are required');\n }\n\n // Lazily populate cache and validate path\n const resolvedConfig = config || (await import('./config.js')).loadConfig();\n await ensureIndexedDirsCache(resolvedConfig);\n validatePathWithinIndexedDirs(args.file_path, indexedDirsCache);\n\n try {\n const content = await getChunkContent(args.file_path, args.chunk_id);\n\n return {\n content: [\n {\n type: 'text',\n text: content\n }\n ]\n };\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n if (msg.includes('ENOENT') || msg.toLowerCase().includes('not found') || msg.toLowerCase().includes('no such file')) {\n throw new Error(\n `File not found: ${args.file_path}. The file may have been moved or deleted. Use 'search' to find similar content.`\n );\n }\n throw error;\n }\n}\n\nexport async function handleServerInfoTool(version: string): Promise<CallToolResult> {\n const status = await getIndexStatus();\n \n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({\n name: 'directory-indexer',\n version: version,\n status: status\n }, null, 2)\n }\n ]\n };\n}\n\nexport async function handleDeleteIndexTool(args: unknown, config: Config): Promise<CallToolResult> {\n if (!isDeleteIndexToolArgs(args)) {\n throw new Error('directory_path is required');\n }\n\n const dirPath = args.directory_path.trim();\n const mutexKey = normalizeMutexKey(dirPath);\n\n // Acquire per-directory mutex to avoid racing with in-flight indexing\n const existing = indexingMutex.get(mutexKey);\n if (existing) {\n log('info', 'Waiting for ongoing indexing before delete', { directory: dirPath });\n await existing;\n }\n\n let resolveDelete: () => void;\n const deletePromise = new Promise<void>((r) => { resolveDelete = r; });\n indexingMutex.set(mutexKey, deletePromise);\n\n const { sqlite, qdrant } = await initializeStorage(config);\n\n try {\n // Check if the directory is actually indexed\n const directory = await sqlite.getDirectory(dirPath);\n if (!directory) {\n throw new Error(\n `Directory '${dirPath}' is not indexed. Use 'server_info' to see indexed directories.`\n );\n }\n\n // Get files for this directory to clean up Qdrant points\n const files = await sqlite.getFilesByDirectory(dirPath);\n const vectorErrors: string[] = [];\n for (const file of files) {\n try {\n await qdrant.deletePointsByFilePath(file.path);\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n log('warning', 'Failed to delete Qdrant points for file', {\n path: file.path,\n error: msg\n });\n vectorErrors.push(`${file.path}: ${msg}`);\n }\n }\n\n // If any vector deletions failed, abort to prevent orphaned vectors\n if (vectorErrors.length > 0) {\n throw new Error(\n `Aborting delete: failed to remove vector embeddings for ${vectorErrors.length} file(s). ` +\n `SQLite rows were NOT deleted to avoid orphaned vectors.\\n` +\n vectorErrors.join('\\n')\n );\n }\n\n // Delete file records and directory record from SQLite\n const deletedFiles = sqlite.deleteFilesByDirectory(dirPath);\n sqlite.deleteDirectory(dirPath);\n\n // Refresh the indexed directories cache\n refreshIndexedDirsCache(sqlite);\n\n const chunksCount = files.reduce((sum, f) => sum + (f.chunks?.length || 0), 0);\n\n log('info', 'Index deleted', { directory: dirPath, files: deletedFiles, chunks: chunksCount });\n mcpServer?.sendLoggingMessage({ level: 'info', data: { event: 'index_deleted', directory: dirPath } });\n\n return {\n content: [\n {\n type: 'text',\n text: `Deleted index for ${dirPath}: removed ${deletedFiles} files and ${chunksCount} chunks`\n }\n ]\n };\n } finally {\n resolveDelete!();\n indexingMutex.delete(mutexKey);\n sqlite.close();\n }\n}\n\nexport function formatErrorResponse(error: unknown): CallToolResult {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n log('error', 'Tool error', { error: errorMessage });\n return {\n content: [\n {\n type: 'text',\n text: `Error: ${errorMessage}`\n }\n ],\n isError: true\n };\n}"],"names":["resolve"],"mappings":";;;;;;;;AAaA,IAAI,YAA2B;AAMxB,SAAS,aAAa,QAAsB;AACjD,cAAY;AACd;AAOA,SAAS,kBAAkB,SAAyB;AAClD,MAAI;AACJ,MAAI;AACF,iBAAa,aAAa,QAAQ,OAAO,CAAC;AAAA,EAC5C,QAAQ;AAEN,iBAAa,QAAQ,OAAO;AAAA,EAC9B;AAEA,SAAO,WAAW,SAAS,KAAK,WAAW,SAAS,GAAG,GAAG;AACxD,iBAAa,WAAW,MAAM,GAAG,CAAC,IAAI,MAAM;AAAA,EAC9C;AACA,SAAO;AACT;AAGA,MAAM,oCAAoB,IAAA;AAG1B,IAAI,uCAAoC,IAAA;AACxC,IAAI,8BAA8B;AAM3B,SAAS,wBAAwB,SAA8B;AACpE,qBAAmB,0BAA0B,OAAO;AACpD,gCAA8B;AAChC;AAMA,eAAe,uBAAuB,QAA+B;AACnE,MAAI,CAAC,6BAA6B;AAChC,UAAM,SAAS,IAAI,cAAc,MAAM;AACvC,QAAI;AACF,8BAAwB,MAAM;AAAA,IAChC,UAAA;AACE,aAAO,MAAA;AAAA,IACT;AAAA,EACF;AACF;AAkCA,SAAS,gBAAgB,MAAsC;AAC7D,SAAO,OAAO,SAAS,YAAY,SAAS,QACrC,MAAM,QAAS,KAAuB,eAAe;AAC9D;AAEA,SAAS,iBAAiB,MAAuC;AAC/D,SAAO,OAAO,SAAS,YAAY,SAAS,QACrC,OAAQ,KAAwB,UAAU;AACnD;AAEA,SAAS,uBAAuB,MAA6C;AAC3E,SAAO,OAAO,SAAS,YAAY,SAAS,QACrC,OAAQ,KAA8B,cAAc;AAC7D;AAEA,SAAS,qBAAqB,MAA2C;AACvE,SAAO,OAAO,SAAS,YAAY,SAAS,QACrC,OAAQ,KAA4B,cAAc;AAC3D;AAEA,SAAS,mBAAmB,MAAyC;AACnE,SAAO,OAAO,SAAS,YAAY,SAAS,QACrC,OAAQ,KAA0B,cAAc,YAChD,OAAQ,KAA0B,aAAa;AACxD;AAEA,SAAS,sBAAsB,MAA4C;AACzE,SAAO,OAAO,SAAS,YAAY,SAAS,QACrC,OAAQ,KAA6B,mBAAmB;AACjE;AAEA,eAAsB,gBAAgB,MAAe,QAAyC;AAC5F,MAAI,CAAC,gBAAgB,IAAI,GAAG;AAC1B,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AAGA,QAAM,2BAA2B,MAAM;AAEvC,QAAM,QAAQ,KAAK,gBAAgB,IAAI,CAAC,MAAc,EAAE,MAAM;AAC9D,QAAM,YAAY,MAAM,IAAI,iBAAiB;AAE7C,MAAI,QAAQ,eAAe,EAAE,aAAa,OAAO;AACjD,aAAW,mBAAmB,EAAE,OAAO,QAAQ,MAAM,EAAE,OAAO,eAAe,aAAa,MAAA,EAAM,CAAG;AAGnG,aAAW,OAAO,WAAW;AAC3B,UAAM,WAAW,cAAc,IAAI,GAAG;AACtC,QAAI,UAAU;AACZ,UAAI,QAAQ,gCAAgC,EAAE,WAAW,KAAK;AAC9D,YAAM;AAAA,IACR;AAAA,EACF;AAGA,MAAI;AACJ,QAAM,kBAAkB,IAAI,QAAc,CAACA,aAAY;AAAE,sBAAkBA;AAAAA,EAAS,CAAC;AACrF,aAAW,OAAO,WAAW;AAC3B,kBAAc,IAAI,KAAK,eAAe;AAAA,EACxC;AAEA,MAAI;AACF,UAAM,SAAS,MAAM,iBAAiB,OAAO,MAAM;AAGnD,UAAM,EAAE,OAAA,IAAW,MAAM,kBAAkB,MAAM;AACjD,QAAI;AACF,8BAAwB,MAAM;AAAA,IAChC,UAAA;AACE,aAAO,MAAA;AAAA,IACT;AAEA,QAAI,QAAQ,kBAAkB,EAAE,OAAA,CAAQ;AACxC,eAAW,mBAAmB,EAAE,OAAO,QAAQ,MAAM,EAAE,OAAO,kBAAkB,OAAA,GAAU;AAE1F,QAAI,eAAe,WAAW,OAAO,OAAO,mBAAmB,OAAO,OAAO,sBAAsB,OAAO,OAAO,mBAAmB,OAAO,MAAM;AAEjJ,QAAI,OAAO,OAAO,SAAS,GAAG;AAC5B,sBAAgB;AAAA;AAAA;AAChB,aAAO,OAAO,QAAQ,CAAA,UAAS;AAC7B,wBAAgB,MAAM,KAAK;AAAA;AAAA,MAC7B,CAAC;AACD,sBAAgB;AAAA,IAClB;AAEA,WAAO;AAAA,MACL,SAAS;AAAA,QACP;AAAA,UACE,MAAM;AAAA,UACN,MAAM;AAAA,QAAA;AAAA,MACR;AAAA,IACF;AAAA,EAEJ,SAAS,OAAO;AACd,UAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,QAAI,SAAS,eAAe,EAAE,OAAO,cAAc,aAAa,OAAO;AACvE,eAAW,mBAAmB,EAAE,OAAO,SAAS,MAAM,EAAE,OAAO,eAAe,OAAO,aAAA,EAAa,CAAG;AACrG,UAAM,IAAI;AAAA,MACR,uBAAuB,MAAM,KAAK,IAAI,CAAC;AAAA,IAAA;AAAA,EAE3C,UAAA;AACE,oBAAA;AACA,eAAW,OAAO,WAAW;AAC3B,oBAAc,OAAO,GAAG;AAAA,IAC1B;AAAA,EACF;AACF;AAEA,eAAe,kBAAkB,WAAuE;AACtG,MAAI,CAAC,UAAW,QAAO,EAAE,UAAA;AAEzB,QAAM,UAAU,MAAM,OAAO,aAAa,GAAG,WAAA;AAC7C,QAAM,EAAE,uBAAA,IAA2B,MAAM,OAAO,aAAa;AAC7D,QAAM,sBAAsB,uBAAuB,MAAM;AAEzD,MAAI,oBAAoB,SAAS,SAAS,GAAG;AAC3C,WAAO,EAAE,UAAA;AAAA,EACX;AAGA,QAAM,UAAU,oBAAoB,SAAS,IACzC,oBAAoB,SAAS,qEAAqE,oBAAoB,KAAK,IAAI,CAAC,qDAChI,oBAAoB,SAAS;AAEjC,SAAO,EAAE,WAAW,QAAW,QAAA;AACjC;AAEA,eAAsB,iBAAiB,MAAwC;AAC7E,MAAI,CAAC,iBAAiB,IAAI,GAAG;AAC3B,UAAM,IAAI,MAAM,mBAAmB;AAAA,EACrC;AAGA,QAAM,UAAU,MAAM,OAAO,aAAa,GAAG,WAAA;AAC7C,QAAM,4BAA4B,MAAM;AAExC,QAAM,EAAE,WAAW,QAAA,IAAY,MAAM,kBAAkB,KAAK,SAAS;AACrE,QAAM,UAAU,MAAM,cAAc,KAAK,OAAO,EAAE,OAAO,KAAK,SAAS,IAAI,UAAA,CAAW;AAEtF,QAAM,WAAW,UACb,GAAG,OAAO;AAAA;AAAA,EAAO,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC,KACjD,KAAK,UAAU,SAAS,MAAM,CAAC;AAEnC,SAAO;AAAA,IACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,UAAU;AAAA,EAAA;AAE9C;AAEA,eAAsB,uBAAuB,MAAwC;AACnF,MAAI,CAAC,uBAAuB,IAAI,GAAG;AACjC,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AAGA,QAAM,UAAU,MAAM,OAAO,aAAa,GAAG,WAAA;AAC7C,QAAM,4BAA4B,MAAM;AAExC,QAAM,EAAE,WAAW,QAAA,IAAY,MAAM,kBAAkB,KAAK,SAAS;AACrE,QAAM,UAAU,MAAM,iBAAiB,KAAK,WAAW,KAAK,SAAS,IAAI,SAAS;AAElF,QAAM,WAAW,UACb,GAAG,OAAO;AAAA;AAAA,EAAO,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC,KACjD,KAAK,UAAU,SAAS,MAAM,CAAC;AAEnC,SAAO;AAAA,IACL,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,UAAU;AAAA,EAAA;AAE9C;AAEA,eAAsB,qBAAqB,MAAe,QAA0C;AAClG,MAAI,CAAC,qBAAqB,IAAI,GAAG;AAC/B,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AAGA,QAAM,iBAAiB,WAAW,MAAM,OAAO,aAAa,GAAG,WAAA;AAC/D,QAAM,uBAAuB,cAAc;AAC3C,gCAA8B,KAAK,WAAW,gBAAgB;AAE9D,MAAI;AACF,UAAM,UAAU,MAAM,eAAe,KAAK,WAAW,KAAK,MAAM;AAEhE,WAAO;AAAA,MACL,SAAS;AAAA,QACP;AAAA,UACE,MAAM;AAAA,UACN,MAAM;AAAA,QAAA;AAAA,MACR;AAAA,IACF;AAAA,EAEJ,SAAS,OAAO;AACd,UAAM,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACjE,QAAI,IAAI,SAAS,QAAQ,KAAK,IAAI,YAAA,EAAc,SAAS,WAAW,KAAK,IAAI,YAAA,EAAc,SAAS,cAAc,GAAG;AACnH,YAAM,IAAI;AAAA,QACR,mBAAmB,KAAK,SAAS;AAAA,MAAA;AAAA,IAErC;AACA,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,mBAAmB,MAAe,QAA0C;AAChG,MAAI,CAAC,mBAAmB,IAAI,GAAG;AAC7B,UAAM,IAAI,MAAM,qCAAqC;AAAA,EACvD;AAGA,QAAM,iBAAiB,WAAW,MAAM,OAAO,aAAa,GAAG,WAAA;AAC/D,QAAM,uBAAuB,cAAc;AAC3C,gCAA8B,KAAK,WAAW,gBAAgB;AAE9D,MAAI;AACF,UAAM,UAAU,MAAM,gBAAgB,KAAK,WAAW,KAAK,QAAQ;AAEnE,WAAO;AAAA,MACL,SAAS;AAAA,QACP;AAAA,UACE,MAAM;AAAA,UACN,MAAM;AAAA,QAAA;AAAA,MACR;AAAA,IACF;AAAA,EAEJ,SAAS,OAAO;AACd,UAAM,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACjE,QAAI,IAAI,SAAS,QAAQ,KAAK,IAAI,YAAA,EAAc,SAAS,WAAW,KAAK,IAAI,YAAA,EAAc,SAAS,cAAc,GAAG;AACnH,YAAM,IAAI;AAAA,QACR,mBAAmB,KAAK,SAAS;AAAA,MAAA;AAAA,IAErC;AACA,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,qBAAqB,SAA0C;AACnF,QAAM,SAAS,MAAM,eAAA;AAErB,SAAO;AAAA,IACL,SAAS;AAAA,MACP;AAAA,QACE,MAAM;AAAA,QACN,MAAM,KAAK,UAAU;AAAA,UACnB,MAAM;AAAA,UACN;AAAA,UACA;AAAA,QAAA,GACC,MAAM,CAAC;AAAA,MAAA;AAAA,IACZ;AAAA,EACF;AAEJ;AAEA,eAAsB,sBAAsB,MAAe,QAAyC;AAClG,MAAI,CAAC,sBAAsB,IAAI,GAAG;AAChC,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AAEA,QAAM,UAAU,KAAK,eAAe,KAAA;AACpC,QAAM,WAAW,kBAAkB,OAAO;AAG1C,QAAM,WAAW,cAAc,IAAI,QAAQ;AAC3C,MAAI,UAAU;AACZ,QAAI,QAAQ,8CAA8C,EAAE,WAAW,SAAS;AAChF,UAAM;AAAA,EACR;AAEA,MAAI;AACJ,QAAM,gBAAgB,IAAI,QAAc,CAAC,MAAM;AAAE,oBAAgB;AAAA,EAAG,CAAC;AACrE,gBAAc,IAAI,UAAU,aAAa;AAEzC,QAAM,EAAE,QAAQ,OAAA,IAAW,MAAM,kBAAkB,MAAM;AAEzD,MAAI;AAEF,UAAM,YAAY,MAAM,OAAO,aAAa,OAAO;AACnD,QAAI,CAAC,WAAW;AACd,YAAM,IAAI;AAAA,QACR,cAAc,OAAO;AAAA,MAAA;AAAA,IAEzB;AAGA,UAAM,QAAQ,MAAM,OAAO,oBAAoB,OAAO;AACtD,UAAM,eAAyB,CAAA;AAC/B,eAAW,QAAQ,OAAO;AACxB,UAAI;AACF,cAAM,OAAO,uBAAuB,KAAK,IAAI;AAAA,MAC/C,SAAS,OAAO;AACd,cAAM,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACjE,YAAI,WAAW,2CAA2C;AAAA,UACxD,MAAM,KAAK;AAAA,UACX,OAAO;AAAA,QAAA,CACR;AACD,qBAAa,KAAK,GAAG,KAAK,IAAI,KAAK,GAAG,EAAE;AAAA,MAC1C;AAAA,IACF;AAGA,QAAI,aAAa,SAAS,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR,2DAA2D,aAAa,MAAM;AAAA,IAE9E,aAAa,KAAK,IAAI;AAAA,MAAA;AAAA,IAE1B;AAGA,UAAM,eAAe,OAAO,uBAAuB,OAAO;AAC1D,WAAO,gBAAgB,OAAO;AAG9B,4BAAwB,MAAM;AAE9B,UAAM,cAAc,MAAM,OAAO,CAAC,KAAK,MAAM,OAAO,EAAE,QAAQ,UAAU,IAAI,CAAC;AAE7E,QAAI,QAAQ,iBAAiB,EAAE,WAAW,SAAS,OAAO,cAAc,QAAQ,aAAa;AAC7F,eAAW,mBAAmB,EAAE,OAAO,QAAQ,MAAM,EAAE,OAAO,iBAAiB,WAAW,QAAA,EAAQ,CAAG;AAErG,WAAO;AAAA,MACL,SAAS;AAAA,QACP;AAAA,UACE,MAAM;AAAA,UACN,MAAM,qBAAqB,OAAO,aAAa,YAAY,cAAc,WAAW;AAAA,QAAA;AAAA,MACtF;AAAA,IACF;AAAA,EAEJ,UAAA;AACE,kBAAA;AACA,kBAAc,OAAO,QAAQ;AAC7B,WAAO,MAAA;AAAA,EACT;AACF;AAEO,SAAS,oBAAoB,OAAgC;AAClE,QAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU;AAC9D,MAAI,SAAS,cAAc,EAAE,OAAO,cAAc;AAClD,SAAO;AAAA,IACL,SAAS;AAAA,MACP;AAAA,QACE,MAAM;AAAA,QACN,MAAM,UAAU,YAAY;AAAA,MAAA;AAAA,IAC9B;AAAA,IAEF,SAAS;AAAA,EAAA;AAEb;"}
1
+ {"version":3,"file":"mcp-handlers.js","sources":["../src/mcp-handlers.ts"],"sourcesContent":["import { Config } from './config.js';\nimport { indexDirectories } from './indexing.js';\nimport { searchContent, findSimilarFiles, getFileContent, getChunkContent } from './search.js';\nimport { getIndexStatus, SQLiteStorage, initializeStorage } from './storage.js';\nimport { validateIndexPrerequisites, validateSearchPrerequisites } from './prerequisites.js';\nimport { validatePathWithinIndexedDirs, resolveIndexedDirectories } from './path-validation.js';\nimport { normalizePath } from './utils.js';\nimport { log } from './logger.js';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\nimport type { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { resolve, sep } from 'path';\nimport { realpathSync } from 'fs';\n\n// MCP server reference for sending client-visible log notifications\nlet mcpServer: Server | null = null;\n\n/**\n * Set the MCP server reference for logging notifications.\n * Called from startMcpServer() after server creation.\n */\nexport function setMcpServer(server: Server): void {\n mcpServer = server;\n}\n\nfunction notifyClient(level: 'info' | 'error', data: Record<string, unknown>): void {\n mcpServer?.sendLoggingMessage({ level, data })?.catch(() => {});\n}\n\n/**\n * Normalize a directory path for use as a mutex key.\n * Resolves symlinks, makes absolute, and strips trailing separators\n * so that '/repo', '/repo/', and symlinks all map to the same key.\n */\nfunction normalizeMutexKey(dirPath: string): string {\n let normalized: string;\n try {\n normalized = realpathSync(resolve(dirPath));\n } catch {\n // Directory may not exist yet; fall back to resolve only\n normalized = resolve(dirPath);\n }\n // Strip trailing separator (but keep root '/' or 'C:\\')\n while (normalized.length > 1 && normalized.endsWith(sep)) {\n normalized = normalized.slice(0, -sep.length);\n }\n return normalized;\n}\n\n// Workspace-level indexing mutex: keyed by normalized directory path\nconst indexingMutex = new Map<string, Promise<void>>();\n\n/**\n * Serialize an operation against prior holders of the given mutex keys.\n * The registered ticket resolves only when this operation's `fn` finishes,\n * so later arrivals chain behind the whole operation instead of acquiring\n * at check time; `fn`'s rejection propagates to its own caller only, and\n * release runs unconditionally in the finally.\n */\nasync function withDirectoryMutex<T>(keys: string[], fn: () => Promise<T>): Promise<T> {\n const holders = keys.map(key => indexingMutex.get(key));\n const prior = Promise.all(holders.map(holder => holder ?? Promise.resolve()));\n let release!: () => void;\n const done = new Promise<void>(resolve => { release = resolve; });\n const ticket = prior.then(() => done);\n for (const key of keys) indexingMutex.set(key, ticket);\n if (holders.some(Boolean)) {\n log('info', 'Waiting for ongoing indexing', { directories: keys });\n }\n await prior;\n try {\n return await fn();\n } finally {\n release();\n for (const key of keys) {\n if (indexingMutex.get(key) === ticket) indexingMutex.delete(key);\n }\n }\n}\n\n// Cached set of resolved indexed directory paths for path validation\nlet indexedDirsCache: Set<string> = new Set();\nlet indexedDirsCacheInitialized = false;\n\n/**\n * Refresh the indexed directories cache from storage.\n * Exported for testability.\n */\nexport function refreshIndexedDirsCache(storage: SQLiteStorage): void {\n indexedDirsCache = resolveIndexedDirectories(storage);\n indexedDirsCacheInitialized = true;\n}\n\n/**\n * Ensure the cache is populated, lazily initializing from SQLite only (no Qdrant).\n * Uses a boolean flag so an empty result doesn't re-trigger initialization.\n */\nasync function ensureIndexedDirsCache(config: Config): Promise<void> {\n if (!indexedDirsCacheInitialized) {\n const { sqlite } = await initializeStorage(config);\n refreshIndexedDirsCache(sqlite);\n }\n}\n\n// Type-safe interfaces for MCP tool arguments\ninterface IndexToolArgs {\n directory_paths: string[];\n}\n\ninterface SearchToolArgs {\n query: string;\n limit?: number;\n workspace?: string;\n}\n\ninterface SimilarFilesToolArgs {\n file_path: string;\n limit?: number;\n workspace?: string;\n}\n\ninterface GetContentToolArgs {\n file_path: string;\n chunks?: string;\n}\n\ninterface GetChunkToolArgs {\n file_path: string;\n chunk_id: string;\n}\n\ninterface DeleteIndexToolArgs {\n directory_path: string;\n}\n\n// Type guard functions\nfunction isIndexToolArgs(args: unknown): args is IndexToolArgs {\n return typeof args === 'object' && args !== null &&\n Array.isArray((args as IndexToolArgs).directory_paths);\n}\n\nfunction isSearchToolArgs(args: unknown): args is SearchToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as SearchToolArgs).query === 'string';\n}\n\nfunction isSimilarFilesToolArgs(args: unknown): args is SimilarFilesToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as SimilarFilesToolArgs).file_path === 'string';\n}\n\nfunction isGetContentToolArgs(args: unknown): args is GetContentToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as GetContentToolArgs).file_path === 'string';\n}\n\nfunction isGetChunkToolArgs(args: unknown): args is GetChunkToolArgs {\n return typeof args === 'object' && args !== null &&\n typeof (args as GetChunkToolArgs).file_path === 'string' &&\n typeof (args as GetChunkToolArgs).chunk_id === 'string';\n}\n\nfunction isDeleteIndexToolArgs(args: unknown): args is DeleteIndexToolArgs {\n return typeof args === 'object' && args !== null &&\n typeof (args as DeleteIndexToolArgs).directory_path === 'string';\n}\n\nexport async function handleIndexTool(args: unknown, config: Config): Promise<CallToolResult> {\n if (!isIndexToolArgs(args)) {\n throw new Error('directory_paths is required and must be an array');\n }\n\n const paths = args.directory_paths.map((p: string) => p.trim());\n const mutexKeys = paths.map(normalizeMutexKey);\n\n return withDirectoryMutex(mutexKeys, async () => {\n // Validate prerequisites before proceeding\n await validateIndexPrerequisites(config);\n\n log('info', 'Index start', { directories: paths });\n notifyClient('info', { event: 'index_start', directories: paths });\n\n try {\n const result = await indexDirectories(paths, config);\n\n // Refresh the indexed directories cache after successful indexing\n const { sqlite } = await initializeStorage(config);\n refreshIndexedDirsCache(sqlite);\n\n log('info', 'Index complete', { result });\n notifyClient('info', { event: 'index_complete', result });\n\n let responseText = `Indexed ${result.indexed} files, skipped ${result.skipped} files, cleaned up ${result.deleted} deleted files, ${result.failed} failed`;\n\n if (result.errors.length > 0) {\n responseText += `\\nErrors: [\\n`;\n result.errors.forEach(error => {\n responseText += ` '${error}'\\n`;\n });\n responseText += `]`;\n }\n\n return {\n content: [\n {\n type: 'text',\n text: responseText\n }\n ]\n };\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n log('error', 'Index error', { error: errorMessage, directories: paths });\n notifyClient('error', { event: 'index_error', error: errorMessage });\n throw new Error(\n `Indexing failed for ${paths.join(', ')}. Verify the directory exists and is readable. Use 'server_info' to check current status.`\n );\n }\n });\n}\n\nasync function validateWorkspace(workspace?: string): Promise<string | undefined> {\n if (!workspace) return workspace;\n\n const config = (await import('./config.js')).loadConfig();\n const { getAvailableWorkspaces } = await import('./config.js');\n const availableWorkspaces = getAvailableWorkspaces(config);\n\n if (availableWorkspaces.includes(workspace)) {\n return workspace;\n }\n\n throw new Error(\n availableWorkspaces.length > 0\n ? `Workspace '${workspace}' not found. Available workspaces: ${availableWorkspaces.join(', ')}`\n : `Workspace '${workspace}' not found and no workspaces are configured.`\n );\n}\n\nexport async function handleSearchTool(args: unknown): Promise<CallToolResult> {\n if (!isSearchToolArgs(args)) {\n throw new Error('query is required');\n }\n\n // Validate prerequisites before proceeding\n const config = (await import('./config.js')).loadConfig();\n await validateSearchPrerequisites(config);\n\n const workspace = await validateWorkspace(args.workspace);\n const results = await searchContent(args.query, { limit: args.limit || 10, workspace });\n\n return {\n content: [{ type: 'text', text: JSON.stringify(results, null, 2) }]\n };\n}\n\nexport async function handleSimilarFilesTool(args: unknown): Promise<CallToolResult> {\n if (!isSimilarFilesToolArgs(args)) {\n throw new Error('file_path is required');\n }\n\n // Validate prerequisites before proceeding\n const config = (await import('./config.js')).loadConfig();\n await validateSearchPrerequisites(config);\n\n await ensureIndexedDirsCache(config);\n validatePathWithinIndexedDirs(args.file_path, indexedDirsCache);\n\n const workspace = await validateWorkspace(args.workspace);\n const results = await findSimilarFiles(args.file_path, args.limit || 10, workspace);\n\n return {\n content: [{ type: 'text', text: JSON.stringify(results, null, 2) }]\n };\n}\n\nexport async function handleGetContentTool(args: unknown, config?: Config): Promise<CallToolResult> {\n if (!isGetContentToolArgs(args)) {\n throw new Error('file_path is required');\n }\n\n // Lazily populate cache and validate path\n const resolvedConfig = config || (await import('./config.js')).loadConfig();\n await ensureIndexedDirsCache(resolvedConfig);\n validatePathWithinIndexedDirs(args.file_path, indexedDirsCache);\n\n try {\n const content = await getFileContent(args.file_path, args.chunks);\n\n return {\n content: [\n {\n type: 'text',\n text: content\n }\n ]\n };\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n if (msg.includes('ENOENT') || msg.toLowerCase().includes('not found') || msg.toLowerCase().includes('no such file')) {\n throw new Error(\n `File not found: ${args.file_path}. The file may have been moved or deleted. Use 'search' to find similar content.`\n );\n }\n throw error;\n }\n}\n\nexport async function handleGetChunkTool(args: unknown, config?: Config): Promise<CallToolResult> {\n if (!isGetChunkToolArgs(args)) {\n throw new Error('file_path and chunk_id are required');\n }\n\n // Lazily populate cache and validate path\n const resolvedConfig = config || (await import('./config.js')).loadConfig();\n await ensureIndexedDirsCache(resolvedConfig);\n validatePathWithinIndexedDirs(args.file_path, indexedDirsCache);\n\n try {\n const content = await getChunkContent(args.file_path, args.chunk_id);\n\n return {\n content: [\n {\n type: 'text',\n text: content\n }\n ]\n };\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n if (msg.includes('ENOENT') || msg.toLowerCase().includes('not found') || msg.toLowerCase().includes('no such file')) {\n throw new Error(\n `File not found: ${args.file_path}. The file may have been moved or deleted. Use 'search' to find similar content.`\n );\n }\n throw error;\n }\n}\n\nexport async function handleServerInfoTool(version: string): Promise<CallToolResult> {\n const status = await getIndexStatus();\n \n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({\n name: 'directory-indexer',\n version: version,\n status: status\n }, null, 2)\n }\n ]\n };\n}\n\nexport async function handleDeleteIndexTool(args: unknown, config: Config): Promise<CallToolResult> {\n if (!isDeleteIndexToolArgs(args)) {\n throw new Error('directory_path is required');\n }\n\n const trimmedPath = args.directory_path.trim();\n const dirPath = normalizePath(trimmedPath);\n const mutexKey = normalizeMutexKey(trimmedPath);\n\n return withDirectoryMutex([mutexKey], async () => {\n const { sqlite, qdrant } = await initializeStorage(config);\n\n // Check if the directory is actually indexed\n let directory = await sqlite.getDirectory(dirPath);\n if (!directory && process.platform === 'win32') {\n directory = await sqlite.getDirectoryByCaseInsensitive(dirPath);\n }\n if (!directory) {\n throw new Error(\n `Directory '${dirPath}' is not indexed. Use 'server_info' to see indexed directories.`\n );\n }\n\n const storedPath = directory.path;\n\n // Get files for this directory to clean up Qdrant points\n const files = await sqlite.getFilesByDirectory(storedPath);\n const vectorErrors: string[] = [];\n for (const file of files) {\n try {\n await qdrant.deletePointsByFilePath(file.path);\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n log('warning', 'Failed to delete Qdrant points for file', {\n path: file.path,\n error: msg\n });\n vectorErrors.push(`${file.path}: ${msg}`);\n }\n }\n\n // If any vector deletions failed, abort to prevent orphaned vectors\n if (vectorErrors.length > 0) {\n throw new Error(\n `Aborting delete: failed to remove vector embeddings for ${vectorErrors.length} file(s). ` +\n `SQLite rows were NOT deleted to avoid orphaned vectors.\\n` +\n vectorErrors.join('\\n')\n );\n }\n\n // Delete file records and directory record from SQLite\n const deletedFiles = sqlite.deleteFilesByDirectory(storedPath);\n sqlite.deleteDirectory(storedPath);\n\n // Refresh the indexed directories cache\n refreshIndexedDirsCache(sqlite);\n\n const chunksCount = files.reduce((sum, f) => sum + (f.chunks?.length || 0), 0);\n\n log('info', 'Index deleted', { directory: storedPath, files: deletedFiles, chunks: chunksCount });\n notifyClient('info', { event: 'index_deleted', directory: storedPath });\n\n return {\n content: [\n {\n type: 'text',\n text: `Deleted index for ${storedPath}: removed ${deletedFiles} files and ${chunksCount} chunks`\n }\n ]\n };\n });\n}\n\nexport function formatErrorResponse(error: unknown): CallToolResult {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n log('error', 'Tool error', { error: errorMessage });\n return {\n content: [\n {\n type: 'text',\n text: `Error: ${errorMessage}`\n }\n ],\n isError: true\n };\n}"],"names":["resolve"],"mappings":"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"}
package/dist/mcp.js CHANGED
@@ -6,7 +6,7 @@ import { dirname, join } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { closeAllStorage } from "./storage.js";
8
8
  import { initLogLevel, log } from "./logger.js";
9
- import { setMcpServer, handleDeleteIndexTool, handleServerInfoTool, handleGetChunkTool, handleGetContentTool, handleSimilarFilesTool, handleSearchTool, handleIndexTool, formatErrorResponse } from "./mcp-handlers.js";
9
+ import { handleDeleteIndexTool, handleServerInfoTool, handleGetChunkTool, handleGetContentTool, handleSimilarFilesTool, handleSearchTool, handleIndexTool, formatErrorResponse, setMcpServer } from "./mcp-handlers.js";
10
10
  const __dirname$1 = dirname(fileURLToPath(import.meta.url));
11
11
  const packageJsonPath = join(__dirname$1, "../package.json");
12
12
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
@@ -139,7 +139,7 @@ Example queries:
139
139
  },
140
140
  workspace: {
141
141
  type: "string",
142
- description: "Optional workspace name to filter search results. Only files within the workspace directories will be searched. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results."
142
+ description: "Optional workspace name to filter search results. Only files within the workspace directories will be searched. IMPORTANT: Use server_info tool first to discover available workspace names - invalid workspace names are rejected with an error listing the available workspaces."
143
143
  }
144
144
  },
145
145
  required: ["query"]
@@ -186,7 +186,7 @@ Returns file paths with similarity scores. Use get_content to read full files or
186
186
  },
187
187
  workspace: {
188
188
  type: "string",
189
- description: "Optional workspace name to filter results. Only files within the workspace directories will be considered. IMPORTANT: Use server_info tool first to discover available workspace names - using invalid workspace names will result in empty results."
189
+ description: "Optional workspace name to filter results. Only files within the workspace directories will be considered. IMPORTANT: Use server_info tool first to discover available workspace names - invalid workspace names are rejected with an error listing the available workspaces."
190
190
  }
191
191
  },
192
192
  required: ["file_path"]