directory-indexer 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-handlers.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/indexing.js +10 -2
- package/dist/indexing.js.map +1 -1
- package/dist/logger.js +43 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp-handlers.js +215 -42
- package/dist/mcp-handlers.js.map +1 -1
- package/dist/mcp.js +200 -106
- package/dist/mcp.js.map +1 -1
- package/dist/path-validation.js +47 -0
- package/dist/path-validation.js.map +1 -0
- package/dist/storage.js +46 -8
- package/dist/storage.js.map +1 -1
- package/dist/utils.js +2 -0
- package/dist/utils.js.map +1 -1
- package/package.json +14 -10
package/dist/cli-handlers.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { indexDirectories } from "./indexing.js";
|
|
2
|
-
import { searchContent, findSimilarFiles
|
|
2
|
+
import { getFileContent, searchContent, findSimilarFiles } from "./search.js";
|
|
3
3
|
import { loadConfig } from "./config.js";
|
|
4
4
|
import { getIndexStatus } from "./storage.js";
|
|
5
5
|
import { startMcpServer } from "./mcp.js";
|
package/dist/cli.js
CHANGED
|
@@ -4,8 +4,8 @@ import { fileURLToPath } from "url";
|
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
5
|
import { dirname, join } from "path";
|
|
6
6
|
import { handleIndex, handleSearch, handleSimilar, handleGet, handleServe, handleReset, handleStatus } from "./cli-handlers.js";
|
|
7
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
-
const packageJsonPath = join(__dirname, "../package.json");
|
|
7
|
+
const __dirname$1 = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const packageJsonPath = join(__dirname$1, "../package.json");
|
|
9
9
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
10
10
|
const VERSION = packageJson.version;
|
|
11
11
|
async function main() {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport { fileURLToPath } from 'url';\nimport { readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { \n handleIndex, \n handleSearch, \n handleSimilar, \n handleGet, \n handleServe, \n handleReset, \n handleStatus \n} from './cli-handlers.js';\n\n// Read version from package.json\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst packageJsonPath = join(__dirname, '../package.json');\nconst packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));\nconst VERSION = packageJson.version;\n\nexport async function main() {\n const program = new Command();\n \n program\n .name('directory-indexer')\n .description('AI-powered directory indexing with semantic search')\n .version(VERSION);\n\n program\n .command('index')\n .description('Index directories for semantic search')\n .argument('<paths...>', 'Directory paths to index')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (paths: string[], options) => {\n try {\n await handleIndex(paths, options);\n } catch (error) {\n console.error('Error indexing directories:', error);\n process.exit(1);\n }\n });\n\n program\n .command('search')\n .description('Search indexed content semantically')\n .argument('<query>', 'Search query')\n .option('-l, --limit <number>', 'Maximum number of results', '10')\n .option('-c, --show-chunks', 'Show individual chunk scores and IDs')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (query: string, options) => {\n try {\n await handleSearch(query, {\n limit: parseInt(options.limit),\n showChunks: options.showChunks,\n verbose: options.verbose\n });\n } catch (error) {\n console.error('Error searching content:', error);\n process.exit(1);\n }\n });\n\n program\n .command('similar')\n .description('Find files similar to a given file')\n .argument('<file>', 'File path to find similar files for')\n .option('-l, --limit <number>', 'Maximum number of results', '10')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (filePath: string, options) => {\n try {\n await handleSimilar(filePath, {\n limit: parseInt(options.limit),\n verbose: options.verbose\n });\n } catch (error) {\n console.error('Error finding similar files:', error);\n process.exit(1);\n }\n });\n\n program\n .command('get')\n .description('Get file content')\n .argument('<file>', 'File path to retrieve')\n .option('-c, --chunks <range>', 'Chunk range (e.g., \"2-5\")')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (filePath: string, options) => {\n try {\n await handleGet(filePath, options);\n } catch (error) {\n console.error('Error getting file content:', error);\n process.exit(1);\n }\n });\n\n program\n .command('serve')\n .description('Start MCP server')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (options) => {\n try {\n await handleServe(options);\n } catch (error) {\n console.error('Error starting MCP server:', error);\n process.exit(1);\n }\n });\n\n program\n .command('reset')\n .description('Reset directory-indexer data (database and vector collection)')\n .option('--force', 'Skip confirmation prompt')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (options) => {\n try {\n await handleReset(options);\n } catch (error) {\n if (error instanceof Error && error.message === 'Reset cancelled by user') {\n console.log('\\nReset cancelled.');\n process.exit(0);\n }\n console.error('Error during reset:', error);\n process.exit(1);\n }\n });\n\n program\n .command('status')\n .description('Show indexing status')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (options) => {\n try {\n await handleStatus(options);\n } catch (error) {\n console.error('Error getting status:', error);\n process.exit(1);\n }\n });\n\n await program.parseAsync();\n}\n\n// Main function is already exported above"],"names":[],"mappings":";;;;;;AAiBA,
|
|
1
|
+
{"version":3,"file":"cli.js","sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport { fileURLToPath } from 'url';\nimport { readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { \n handleIndex, \n handleSearch, \n handleSimilar, \n handleGet, \n handleServe, \n handleReset, \n handleStatus \n} from './cli-handlers.js';\n\n// Read version from package.json\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst packageJsonPath = join(__dirname, '../package.json');\nconst packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));\nconst VERSION = packageJson.version;\n\nexport async function main() {\n const program = new Command();\n \n program\n .name('directory-indexer')\n .description('AI-powered directory indexing with semantic search')\n .version(VERSION);\n\n program\n .command('index')\n .description('Index directories for semantic search')\n .argument('<paths...>', 'Directory paths to index')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (paths: string[], options) => {\n try {\n await handleIndex(paths, options);\n } catch (error) {\n console.error('Error indexing directories:', error);\n process.exit(1);\n }\n });\n\n program\n .command('search')\n .description('Search indexed content semantically')\n .argument('<query>', 'Search query')\n .option('-l, --limit <number>', 'Maximum number of results', '10')\n .option('-c, --show-chunks', 'Show individual chunk scores and IDs')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (query: string, options) => {\n try {\n await handleSearch(query, {\n limit: parseInt(options.limit),\n showChunks: options.showChunks,\n verbose: options.verbose\n });\n } catch (error) {\n console.error('Error searching content:', error);\n process.exit(1);\n }\n });\n\n program\n .command('similar')\n .description('Find files similar to a given file')\n .argument('<file>', 'File path to find similar files for')\n .option('-l, --limit <number>', 'Maximum number of results', '10')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (filePath: string, options) => {\n try {\n await handleSimilar(filePath, {\n limit: parseInt(options.limit),\n verbose: options.verbose\n });\n } catch (error) {\n console.error('Error finding similar files:', error);\n process.exit(1);\n }\n });\n\n program\n .command('get')\n .description('Get file content')\n .argument('<file>', 'File path to retrieve')\n .option('-c, --chunks <range>', 'Chunk range (e.g., \"2-5\")')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (filePath: string, options) => {\n try {\n await handleGet(filePath, options);\n } catch (error) {\n console.error('Error getting file content:', error);\n process.exit(1);\n }\n });\n\n program\n .command('serve')\n .description('Start MCP server')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (options) => {\n try {\n await handleServe(options);\n } catch (error) {\n console.error('Error starting MCP server:', error);\n process.exit(1);\n }\n });\n\n program\n .command('reset')\n .description('Reset directory-indexer data (database and vector collection)')\n .option('--force', 'Skip confirmation prompt')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (options) => {\n try {\n await handleReset(options);\n } catch (error) {\n if (error instanceof Error && error.message === 'Reset cancelled by user') {\n console.log('\\nReset cancelled.');\n process.exit(0);\n }\n console.error('Error during reset:', error);\n process.exit(1);\n }\n });\n\n program\n .command('status')\n .description('Show indexing status')\n .option('-v, --verbose', 'Enable verbose logging')\n .action(async (options) => {\n try {\n await handleStatus(options);\n } catch (error) {\n console.error('Error getting status:', error);\n process.exit(1);\n }\n });\n\n await program.parseAsync();\n}\n\n// Main function is already exported above"],"names":["__dirname"],"mappings":"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"}
|
package/dist/indexing.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { promises } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import {
|
|
3
|
+
import { getFileInfo, normalizePath, shouldIgnoreFile, isDirectory, isFile, isSupportedFileType } from "./utils.js";
|
|
4
4
|
import { loadGitignoreRules } from "./gitignore.js";
|
|
5
5
|
import { generateEmbedding } from "./embedding.js";
|
|
6
6
|
import { initializeStorage } from "./storage.js";
|
|
7
|
+
import { log } from "./logger.js";
|
|
7
8
|
class IndexingError extends Error {
|
|
8
9
|
constructor(message, cause) {
|
|
9
10
|
super(message);
|
|
@@ -170,7 +171,14 @@ async function indexDirectories(paths, config) {
|
|
|
170
171
|
}
|
|
171
172
|
await qdrant.deletePointsByFilePath(file.path);
|
|
172
173
|
}
|
|
173
|
-
const
|
|
174
|
+
const rawContent = await promises.readFile(file.path, "utf-8");
|
|
175
|
+
if (rawContent.includes("�")) {
|
|
176
|
+
log("warning", "Skipping non-UTF-8 file", { path: file.path });
|
|
177
|
+
await sqlite.upsertFile(file, [], ["Skipped: file appears to be non-UTF-8 encoded"]);
|
|
178
|
+
skipped++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const content = rawContent.replace(/\r\n/g, "\n");
|
|
174
182
|
const chunks = chunkText(content, config.indexing.chunkSize, config.indexing.chunkOverlap);
|
|
175
183
|
await sqlite.upsertFile(file, chunks);
|
|
176
184
|
for (let i = 0; i < chunks.length; i++) {
|
package/dist/indexing.js.map
CHANGED
|
@@ -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';\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 content = await fs.readFile(file.path, 'utf-8');\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 { 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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"}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const LEVEL_ORDER = {
|
|
2
|
+
debug: 0,
|
|
3
|
+
info: 1,
|
|
4
|
+
warning: 2,
|
|
5
|
+
error: 3
|
|
6
|
+
};
|
|
7
|
+
let currentLevel = "info";
|
|
8
|
+
function setLogLevel(level) {
|
|
9
|
+
currentLevel = level;
|
|
10
|
+
}
|
|
11
|
+
function getLogLevel() {
|
|
12
|
+
return currentLevel;
|
|
13
|
+
}
|
|
14
|
+
function initLogLevel() {
|
|
15
|
+
const envLevel = process.env.LOG_LEVEL?.toLowerCase();
|
|
16
|
+
if (!envLevel) return;
|
|
17
|
+
if (envLevel in LEVEL_ORDER) {
|
|
18
|
+
currentLevel = envLevel;
|
|
19
|
+
} else {
|
|
20
|
+
process.stderr.write(
|
|
21
|
+
`Warning: invalid LOG_LEVEL "${process.env.LOG_LEVEL}", defaulting to "info"
|
|
22
|
+
`
|
|
23
|
+
);
|
|
24
|
+
currentLevel = "info";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function log(level, message, data) {
|
|
28
|
+
if (LEVEL_ORDER[level] < LEVEL_ORDER[currentLevel]) return;
|
|
29
|
+
const entry = {
|
|
30
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
31
|
+
level,
|
|
32
|
+
message,
|
|
33
|
+
...data
|
|
34
|
+
};
|
|
35
|
+
process.stderr.write(JSON.stringify(entry) + "\n");
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
getLogLevel,
|
|
39
|
+
initLogLevel,
|
|
40
|
+
log,
|
|
41
|
+
setLogLevel
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sources":["../src/logger.ts"],"sourcesContent":["export type LogLevel = 'debug' | 'info' | 'warning' | 'error';\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warning: 2,\n error: 3,\n};\n\nlet currentLevel: LogLevel = 'info';\n\n/**\n * Set the minimum log level. Messages below this level are discarded.\n */\nexport function setLogLevel(level: LogLevel): void {\n currentLevel = level;\n}\n\n/**\n * Get the current log level.\n */\nexport function getLogLevel(): LogLevel {\n return currentLevel;\n}\n\n/**\n * Initialize the log level from the LOG_LEVEL environment variable.\n * Invalid values default to 'info' with a warning on stderr.\n */\nexport function initLogLevel(): void {\n const envLevel = process.env.LOG_LEVEL?.toLowerCase();\n if (!envLevel) return;\n\n if (envLevel in LEVEL_ORDER) {\n currentLevel = envLevel as LogLevel;\n } else {\n process.stderr.write(\n `Warning: invalid LOG_LEVEL \"${process.env.LOG_LEVEL}\", defaulting to \"info\"\\n`\n );\n currentLevel = 'info';\n }\n}\n\n/**\n * Write a structured JSON log line to stderr.\n * Never writes to stdout (reserved for the MCP protocol channel).\n */\nexport function log(level: LogLevel, message: string, data?: Record<string, unknown>): void {\n if (LEVEL_ORDER[level] < LEVEL_ORDER[currentLevel]) return;\n\n const entry: Record<string, unknown> = {\n timestamp: new Date().toISOString(),\n level,\n message,\n ...data,\n };\n\n process.stderr.write(JSON.stringify(entry) + '\\n');\n}\n"],"names":[],"mappings":"AAEA,MAAM,cAAwC;AAAA,EAC5C,OAAO;AAAA,EACP,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AACT;AAEA,IAAI,eAAyB;AAKtB,SAAS,YAAY,OAAuB;AACjD,iBAAe;AACjB;AAKO,SAAS,cAAwB;AACtC,SAAO;AACT;AAMO,SAAS,eAAqB;AACnC,QAAM,WAAW,QAAQ,IAAI,WAAW,YAAA;AACxC,MAAI,CAAC,SAAU;AAEf,MAAI,YAAY,aAAa;AAC3B,mBAAe;AAAA,EACjB,OAAO;AACL,YAAQ,OAAO;AAAA,MACb,+BAA+B,QAAQ,IAAI,SAAS;AAAA;AAAA,IAAA;AAEtD,mBAAe;AAAA,EACjB;AACF;AAMO,SAAS,IAAI,OAAiB,SAAiB,MAAsC;AAC1F,MAAI,YAAY,KAAK,IAAI,YAAY,YAAY,EAAG;AAEpD,QAAM,QAAiC;AAAA,IACrC,YAAW,oBAAI,KAAA,GAAO,YAAA;AAAA,IACtB;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA;AAGL,UAAQ,OAAO,MAAM,KAAK,UAAU,KAAK,IAAI,IAAI;AACnD;"}
|
package/dist/mcp-handlers.js
CHANGED
|
@@ -1,9 +1,46 @@
|
|
|
1
1
|
import { indexDirectories } from "./indexing.js";
|
|
2
|
-
import {
|
|
3
|
-
import { getIndexStatus } from "./storage.js";
|
|
2
|
+
import { getChunkContent, getFileContent, searchContent, findSimilarFiles } from "./search.js";
|
|
3
|
+
import { initializeStorage, getIndexStatus, SQLiteStorage } from "./storage.js";
|
|
4
4
|
import { validateIndexPrerequisites, validateSearchPrerequisites } from "./prerequisites.js";
|
|
5
|
+
import { validatePathWithinIndexedDirs, resolveIndexedDirectories } from "./path-validation.js";
|
|
6
|
+
import { log } from "./logger.js";
|
|
7
|
+
import { resolve, sep } from "path";
|
|
8
|
+
import { realpathSync } from "fs";
|
|
9
|
+
let mcpServer = null;
|
|
10
|
+
function setMcpServer(server) {
|
|
11
|
+
mcpServer = server;
|
|
12
|
+
}
|
|
13
|
+
function normalizeMutexKey(dirPath) {
|
|
14
|
+
let normalized;
|
|
15
|
+
try {
|
|
16
|
+
normalized = realpathSync(resolve(dirPath));
|
|
17
|
+
} catch {
|
|
18
|
+
normalized = resolve(dirPath);
|
|
19
|
+
}
|
|
20
|
+
while (normalized.length > 1 && normalized.endsWith(sep)) {
|
|
21
|
+
normalized = normalized.slice(0, -sep.length);
|
|
22
|
+
}
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
const indexingMutex = /* @__PURE__ */ new Map();
|
|
26
|
+
let indexedDirsCache = /* @__PURE__ */ new Set();
|
|
27
|
+
let indexedDirsCacheInitialized = false;
|
|
28
|
+
function refreshIndexedDirsCache(storage) {
|
|
29
|
+
indexedDirsCache = resolveIndexedDirectories(storage);
|
|
30
|
+
indexedDirsCacheInitialized = true;
|
|
31
|
+
}
|
|
32
|
+
async function ensureIndexedDirsCache(config) {
|
|
33
|
+
if (!indexedDirsCacheInitialized) {
|
|
34
|
+
const sqlite = new SQLiteStorage(config);
|
|
35
|
+
try {
|
|
36
|
+
refreshIndexedDirsCache(sqlite);
|
|
37
|
+
} finally {
|
|
38
|
+
sqlite.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
5
42
|
function isIndexToolArgs(args) {
|
|
6
|
-
return typeof args === "object" && args !== null &&
|
|
43
|
+
return typeof args === "object" && args !== null && Array.isArray(args.directory_paths);
|
|
7
44
|
}
|
|
8
45
|
function isSearchToolArgs(args) {
|
|
9
46
|
return typeof args === "object" && args !== null && typeof args.query === "string";
|
|
@@ -17,32 +54,74 @@ function isGetContentToolArgs(args) {
|
|
|
17
54
|
function isGetChunkToolArgs(args) {
|
|
18
55
|
return typeof args === "object" && args !== null && typeof args.file_path === "string" && typeof args.chunk_id === "string";
|
|
19
56
|
}
|
|
57
|
+
function isDeleteIndexToolArgs(args) {
|
|
58
|
+
return typeof args === "object" && args !== null && typeof args.directory_path === "string";
|
|
59
|
+
}
|
|
20
60
|
async function handleIndexTool(args, config) {
|
|
21
61
|
if (!isIndexToolArgs(args)) {
|
|
22
|
-
throw new Error("
|
|
62
|
+
throw new Error("directory_paths is required and must be an array");
|
|
23
63
|
}
|
|
24
64
|
await validateIndexPrerequisites(config);
|
|
25
|
-
const paths = args.
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
65
|
+
const paths = args.directory_paths.map((p) => p.trim());
|
|
66
|
+
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);
|
|
86
|
+
try {
|
|
87
|
+
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 += `
|
|
30
96
|
Errors: [
|
|
31
97
|
`;
|
|
32
|
-
|
|
33
|
-
|
|
98
|
+
result.errors.forEach((error) => {
|
|
99
|
+
responseText += ` '${error}'
|
|
34
100
|
`;
|
|
35
|
-
|
|
36
|
-
|
|
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);
|
|
123
|
+
}
|
|
37
124
|
}
|
|
38
|
-
return {
|
|
39
|
-
content: [
|
|
40
|
-
{
|
|
41
|
-
type: "text",
|
|
42
|
-
text: responseText
|
|
43
|
-
}
|
|
44
|
-
]
|
|
45
|
-
};
|
|
46
125
|
}
|
|
47
126
|
async function validateWorkspace(workspace) {
|
|
48
127
|
if (!workspace) return { workspace };
|
|
@@ -85,33 +164,59 @@ ${JSON.stringify(results, null, 2)}` : JSON.stringify(results, null, 2);
|
|
|
85
164
|
content: [{ type: "text", text: response }]
|
|
86
165
|
};
|
|
87
166
|
}
|
|
88
|
-
async function handleGetContentTool(args) {
|
|
167
|
+
async function handleGetContentTool(args, config) {
|
|
89
168
|
if (!isGetContentToolArgs(args)) {
|
|
90
169
|
throw new Error("file_path is required");
|
|
91
170
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
171
|
+
const resolvedConfig = config || (await import("./config.js")).loadConfig();
|
|
172
|
+
await ensureIndexedDirsCache(resolvedConfig);
|
|
173
|
+
validatePathWithinIndexedDirs(args.file_path, indexedDirsCache);
|
|
174
|
+
try {
|
|
175
|
+
const content = await getFileContent(args.file_path, args.chunks);
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: "text",
|
|
180
|
+
text: content
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
186
|
+
if (msg.includes("ENOENT") || msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("no such file")) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`File not found: ${args.file_path}. The file may have been moved or deleted. Use 'search' to find similar content.`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
101
193
|
}
|
|
102
|
-
async function handleGetChunkTool(args) {
|
|
194
|
+
async function handleGetChunkTool(args, config) {
|
|
103
195
|
if (!isGetChunkToolArgs(args)) {
|
|
104
196
|
throw new Error("file_path and chunk_id are required");
|
|
105
197
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
198
|
+
const resolvedConfig = config || (await import("./config.js")).loadConfig();
|
|
199
|
+
await ensureIndexedDirsCache(resolvedConfig);
|
|
200
|
+
validatePathWithinIndexedDirs(args.file_path, indexedDirsCache);
|
|
201
|
+
try {
|
|
202
|
+
const content = await getChunkContent(args.file_path, args.chunk_id);
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: content
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
213
|
+
if (msg.includes("ENOENT") || msg.toLowerCase().includes("not found") || msg.toLowerCase().includes("no such file")) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`File not found: ${args.file_path}. The file may have been moved or deleted. Use 'search' to find similar content.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
115
220
|
}
|
|
116
221
|
async function handleServerInfoTool(version) {
|
|
117
222
|
const status = await getIndexStatus();
|
|
@@ -128,8 +233,73 @@ async function handleServerInfoTool(version) {
|
|
|
128
233
|
]
|
|
129
234
|
};
|
|
130
235
|
}
|
|
236
|
+
async function handleDeleteIndexTool(args, config) {
|
|
237
|
+
if (!isDeleteIndexToolArgs(args)) {
|
|
238
|
+
throw new Error("directory_path is required");
|
|
239
|
+
}
|
|
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);
|
|
255
|
+
if (!directory) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Directory '${dirPath}' is not indexed. Use 'server_info' to see indexed directories.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const files = await sqlite.getFilesByDirectory(dirPath);
|
|
261
|
+
const vectorErrors = [];
|
|
262
|
+
for (const file of files) {
|
|
263
|
+
try {
|
|
264
|
+
await qdrant.deletePointsByFilePath(file.path);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
267
|
+
log("warning", "Failed to delete Qdrant points for file", {
|
|
268
|
+
path: file.path,
|
|
269
|
+
error: msg
|
|
270
|
+
});
|
|
271
|
+
vectorErrors.push(`${file.path}: ${msg}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (vectorErrors.length > 0) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Aborting delete: failed to remove vector embeddings for ${vectorErrors.length} file(s). SQLite rows were NOT deleted to avoid orphaned vectors.
|
|
277
|
+
` + vectorErrors.join("\n")
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const deletedFiles = sqlite.deleteFilesByDirectory(dirPath);
|
|
281
|
+
sqlite.deleteDirectory(dirPath);
|
|
282
|
+
refreshIndexedDirsCache(sqlite);
|
|
283
|
+
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 } });
|
|
286
|
+
return {
|
|
287
|
+
content: [
|
|
288
|
+
{
|
|
289
|
+
type: "text",
|
|
290
|
+
text: `Deleted index for ${dirPath}: removed ${deletedFiles} files and ${chunksCount} chunks`
|
|
291
|
+
}
|
|
292
|
+
]
|
|
293
|
+
};
|
|
294
|
+
} finally {
|
|
295
|
+
resolveDelete();
|
|
296
|
+
indexingMutex.delete(mutexKey);
|
|
297
|
+
sqlite.close();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
131
300
|
function formatErrorResponse(error) {
|
|
132
301
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
302
|
+
log("error", "Tool error", { error: errorMessage });
|
|
133
303
|
return {
|
|
134
304
|
content: [
|
|
135
305
|
{
|
|
@@ -142,11 +312,14 @@ function formatErrorResponse(error) {
|
|
|
142
312
|
}
|
|
143
313
|
export {
|
|
144
314
|
formatErrorResponse,
|
|
315
|
+
handleDeleteIndexTool,
|
|
145
316
|
handleGetChunkTool,
|
|
146
317
|
handleGetContentTool,
|
|
147
318
|
handleIndexTool,
|
|
148
319
|
handleSearchTool,
|
|
149
320
|
handleServerInfoTool,
|
|
150
|
-
handleSimilarFilesTool
|
|
321
|
+
handleSimilarFilesTool,
|
|
322
|
+
refreshIndexedDirsCache,
|
|
323
|
+
setMcpServer
|
|
151
324
|
};
|
|
152
325
|
//# sourceMappingURL=mcp-handlers.js.map
|
package/dist/mcp-handlers.js.map
CHANGED
|
@@ -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 } from './storage.js';\nimport { validateIndexPrerequisites, validateSearchPrerequisites } from './prerequisites.js';\nimport { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n// Type-safe interfaces for MCP tool arguments\ninterface IndexToolArgs {\n directory_path: 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\n// Type guard functions\nfunction isIndexToolArgs(args: unknown): args is IndexToolArgs {\n return typeof args === 'object' && args !== null && \n typeof (args as IndexToolArgs).directory_path === 'string';\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\nexport async function handleIndexTool(args: unknown, config: Config): Promise<CallToolResult> {\n if (!isIndexToolArgs(args)) {\n throw new Error('directory_path is required');\n }\n \n // Validate prerequisites before proceeding\n await validateIndexPrerequisites(config);\n \n const paths = args.directory_path.split(',').map((p: string) => p.trim());\n const result = await indexDirectories(paths, config);\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}\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): Promise<CallToolResult> {\n if (!isGetContentToolArgs(args)) {\n throw new Error('file_path is required');\n }\n \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}\n\nexport async function handleGetChunkTool(args: unknown): Promise<CallToolResult> {\n if (!isGetChunkToolArgs(args)) {\n throw new Error('file_path and chunk_id are required');\n }\n \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}\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 function formatErrorResponse(error: unknown): CallToolResult {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n return {\n content: [\n {\n type: 'text',\n text: `Error: ${errorMessage}`\n }\n ],\n isError: true\n };\n}"],"names":[],"mappings":"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"}
|
|
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":"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"}
|