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.
package/dist/mcp.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.js","sources":["../src/mcp.ts"],"sourcesContent":["import { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { \n CallToolRequestSchema, \n ListToolsRequestSchema,\n Tool\n} from '@modelcontextprotocol/sdk/types.js';\nimport { readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport { Config } from './config.js';\nimport { closeAllStorage } from './storage.js';\nimport { initLogLevel, log } from './logger.js';\nimport {\n handleIndexTool,\n handleSearchTool,\n handleSimilarFilesTool,\n handleGetContentTool,\n handleGetChunkTool,\n handleServerInfoTool,\n handleDeleteIndexTool,\n formatErrorResponse,\n setMcpServer\n} from './mcp-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\nconst DELETE_INDEX_TOOL: Tool = {\n name: 'delete_index',\n description: `Remove the index for a directory. Deletes all file records, vector embeddings, and the directory entry from the database.\n\nWhen to use this tool:\n- User wants to remove a directory from the search index\n- Cleaning up old or irrelevant indexed content\n- Freeing database space by removing unused indexes\n\nHow it works:\n- Removes all file records for the specified directory from SQLite\n- Deletes corresponding vector embeddings from Qdrant\n- Removes the directory entry from the directories table\n- Does NOT delete the actual files on disk\n\nExamples:\n- Remove old project: directory_path=\"/home/user/old-project\"\n- Clean up test data: directory_path=\"/home/user/test-files\"\n\nUse server_info to see what directories are currently indexed before removing.`,\n inputSchema: {\n type: 'object',\n properties: {\n directory_path: {\n type: 'string',\n description: 'Absolute path of the directory whose index should be removed'\n }\n },\n required: ['directory_path']\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: true,\n idempotentHint: true,\n openWorldHint: false\n }\n};\n\nexport function getMcpTools(): Tool[] {\n const tools: Tool[] = [\n {\n name: 'index',\n description: `Index directories to make their files searchable. Processes files to create vector embeddings for semantic search.\n\nWhen to use this tool:\n- User specifically requests indexing a directory as a knowledge base\n- Adding new documentation, code repositories, or file collections to search\n- Updating index when many files have changed\n\nHow it works:\n- Recursively scans directories for supported file types\n- Extracts text content and splits into chunks\n- Generates vector embeddings for semantic similarity\n- Stores in database for fast retrieval\n\nExamples:\n- Index documentation: [\"/home/user/docs/project-wiki\"]\n- Index codebase: [\"/home/user/projects/api-server\"]\n- Index multiple directories: [\"/home/user/docs\", \"/home/user/configs\"]\n\nIndexing can take several minutes for large directories. Most users will already have directories indexed and can directly use search tool. Use server_info to check current indexing status first.`,\n inputSchema: {\n type: 'object',\n properties: {\n directory_paths: {\n type: 'array',\n items: { type: 'string' },\n description: 'Array of absolute directory paths to index. Must be absolute paths since MCP server runs independently. Examples: [\"/home/user/projects\"] (Unix) or [\"C:\\\\\\\\Users\\\\\\\\user\\\\\\\\projects\"] (Windows)'\n }\n },\n required: ['directory_paths']\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false\n }\n },\n {\n name: 'search',\n description: `Search indexed files using natural language queries. Finds files containing content semantically similar to the query.\n\nWhen to use this tool:\n- Find documentation, guides, or explanations about specific topics\n- Locate code files implementing certain functionality or patterns\n- Discover configuration files, scripts, or settings related to a topic\n- Search for files covering specific concepts or technologies\n\nHow it works:\n- Converts query to vector embedding using semantic similarity\n- Searches all indexed file chunks for relevant content\n- Groups results by file and calculates average relevance scores\n- Returns files ranked by relevance score\n\nExamples:\n- \"database configuration and connection pooling setup\" - finds config files, documentation about DB setup\n- \"comprehensive error handling patterns and exception management\" - finds code files with exception handling\n- \"JWT authentication implementation and session management\" - finds auth-related code and docs\n- \"REST API documentation and endpoint specifications\" - finds API guides, endpoint definitions\n- \"Docker deployment scripts and CI/CD pipeline configuration\" - finds deployment automation\n\nReturns files with similarity scores and chunk information. Use get_content to retrieve full file content or get_chunk to retrieve specific chunk content by chunk ID.\n- Groups results by file to avoid duplicates from multiple matching sections\n\nResponse format:\n- Returns lightweight metadata including file paths, relevance scores, and chunk IDs\n- Use 'get_chunk' or 'get_content' tools to fetch actual content from search results\n- Chunks are sorted by relevance score within each file\n- Average similarity score calculated across all matching chunks per file\n\nExample queries:\n- \"error handling patterns and exception management strategies\" (finds try/catch, error classes, logging)\n- \"database migration scripts and schema versioning approaches\" (finds SQL, schema changes, migration files)\n- \"authentication middleware and JWT token validation logic\" (finds auth logic, JWT handling, middleware functions)`,\n inputSchema: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description: 'Natural language search query describing what you are looking for. Can be concepts, functionality, or specific technical terms.'\n },\n limit: {\n type: 'number',\n description: 'Maximum number of files to return (default: 10). Each file may contain multiple matching chunks.',\n default: 10\n },\n workspace: {\n type: 'string',\n 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.'\n }\n },\n required: ['query']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'similar_files',\n description: `Find files with content similar to a reference file. Uses semantic similarity to find related documents, code files, or any text content.\n\nWhen to use this tool:\n- Find documentation similar to a specific guide or README\n- Locate related code files, configuration files, or scripts\n- Discover alternative implementations or approaches\n- Find files covering similar topics or concepts\n\nHow it works:\n- Analyzes the semantic content of the reference file\n- Compares against all indexed files using vector similarity\n- Returns files ranked by content similarity score\n\nExamples:\n- Given \"deployment-guide.md\" - finds other deployment docs, CI/CD guides, infrastructure setup\n- Given \"troubleshooting.md\" - finds other troubleshooting guides, FAQ files, error documentation\n- Given \"config.yaml\" - finds other configuration files, settings, environment setups\n- Given \"auth.py\" - finds other authentication modules, security code, middleware\n\nReturns file paths with similarity scores. Use get_content to read full files or get_chunk for specific sections.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the reference file. This file must have been previously indexed.'\n },\n limit: {\n type: 'number',\n description: 'Maximum number of similar files to return (default: 10). Results are sorted by similarity score.',\n default: 10\n },\n workspace: {\n type: 'string',\n 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.'\n }\n },\n required: ['file_path']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'get_content',\n description: `Retrieve the full content of a file or specific chunks. Reads files directly from the filesystem.\n\nWhen to use this tool:\n- Get complete file content after finding files through search\n- Read documentation, code files, or configuration files for analysis\n- Extract specific sections of large files using chunk ranges\n- Access any text-based file content\n\nHow it works:\n- Reads files directly from filesystem (not from search index)\n- Returns entire file by default\n- Can return specific chunk ranges for indexed files\n- Preserves original formatting and content\n\nExamples:\n- Get full file: file_path=\"/home/user/docs/api.md\"\n- Get specific chunks: file_path=\"/home/user/code/main.py\", chunks=\"2-5\"\n- Get single chunk: file_path=\"/home/user/config.json\", chunks=\"1\"\n\nReturns file content as text. Use this after search or similar_files to read actual content.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the file to retrieve. File must be readable and text-based.'\n },\n chunks: {\n type: 'string',\n description: 'Optional chunk range specification. Examples: \"3\" (single chunk), \"2-5\" (chunks 2 through 5), \"1-3\" (first three chunks). Only works for indexed files.'\n }\n },\n required: ['file_path']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'get_chunk',\n description: `Retrieve content of a specific chunk from an indexed file. Gets exact text segments identified during search.\n\nWhen to use this tool:\n- Get specific relevant sections after performing a search\n- Access only the most pertinent parts of large files\n- Retrieve content from high-scoring chunks identified in search results\n- Avoid reading entire files when only specific sections are needed\n\nHow it works:\n- Files are split into overlapping text chunks during indexing\n- Each chunk has a sequential ID (\"0\", \"1\", \"2\", etc.)\n- Search results include chunk IDs for relevant sections\n- Returns the exact content that was semantically matched\n\nExamples:\n- After search returns chunk \"3\" from \"api-docs.md\" with high score\n- Get chunk content: file_path=\"/docs/api-docs.md\", chunk_id=\"3\"\n- Returns the specific text segment that matched your query\n\nReturns chunk content as text. Use this with chunk IDs from search results to get precise content sections.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the indexed file containing the desired chunk.'\n },\n chunk_id: {\n type: 'string',\n description: 'ID of the specific chunk to retrieve. This is typically obtained from search results and is a sequential string like \"0\", \"1\", \"2\", etc.'\n }\n },\n required: ['file_path', 'chunk_id']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'server_info',\n description: `Get information about server status and indexed content. Shows what directories and files are available for search.\n\nWhen to use this tool:\n- REQUIRED: Check available workspace names before using workspace parameter in search or similar_files tools\n- Check what content is already indexed before performing searches\n- Verify system is working properly\n- See indexing statistics and status\n- Understand scope of available searchable content\n\nHow it works:\n- Reports total indexed directories, files, and chunks\n- Shows database size and last indexing time\n- Lists all indexed directories with file counts\n- Lists all configured workspaces with their paths and file counts\n- Reports any errors or issues\n\nExamples:\n- Check workspaces before searching: \"What workspaces are available?\"\n- Check before searching: \"What content is indexed?\"\n- Verify after indexing: \"Did the indexing complete successfully?\"\n- Monitor system: \"How many files are searchable?\"\n\nReturns server version, indexing statistics, directory list, workspace information, and any errors. IMPORTANT: Always use this tool first to discover available workspace names when you need to search within specific workspaces.`,\n inputSchema: {\n type: 'object',\n properties: {},\n additionalProperties: false\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n }\n ];\n\n // Include delete_index tool unless DISABLE_DESTRUCTIVE is set\n if (process.env.DISABLE_DESTRUCTIVE !== 'true') {\n tools.push(DELETE_INDEX_TOOL);\n }\n\n return tools;\n}\n\nexport async function startMcpServer(config: Config): Promise<void> {\n initLogLevel();\n\n const server = new Server(\n {\n name: 'directory-indexer',\n version: VERSION\n },\n {\n capabilities: {\n tools: {},\n logging: {}\n }\n }\n );\n\n setMcpServer(server);\n\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: getMcpTools()\n };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n switch (name) {\n case 'index':\n return await handleIndexTool(args, config);\n \n case 'search':\n return await handleSearchTool(args);\n \n case 'similar_files':\n return await handleSimilarFilesTool(args);\n \n case 'get_content':\n return await handleGetContentTool(args, config);\n\n case 'get_chunk':\n return await handleGetChunkTool(args, config);\n \n case 'server_info':\n return await handleServerInfoTool(VERSION);\n\n case 'delete_index':\n if (process.env.DISABLE_DESTRUCTIVE === 'true') {\n throw new Error('delete_index is disabled via DISABLE_DESTRUCTIVE environment variable');\n }\n return await handleDeleteIndexTool(args, config);\n\n default:\n throw new Error(`Unknown tool: ${name}`);\n }\n } catch (error) {\n return formatErrorResponse(error);\n }\n });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n // Graceful shutdown: close all SQLite connections before exiting\n const cleanup = () => {\n log('info', 'Shutting down MCP server');\n closeAllStorage();\n process.exit(0);\n };\n process.on('SIGTERM', cleanup);\n process.on('SIGINT', cleanup);\n\n if (config.verbose) {\n console.error('MCP server started successfully');\n }\n\n log('info', 'MCP server started', { version: VERSION });\n}"],"names":["__dirname"],"mappings":"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"}
1
+ {"version":3,"file":"mcp.js","sources":["../src/mcp.ts"],"sourcesContent":["import { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { \n CallToolRequestSchema, \n ListToolsRequestSchema,\n Tool\n} from '@modelcontextprotocol/sdk/types.js';\nimport { readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport { Config } from './config.js';\nimport { closeAllStorage } from './storage.js';\nimport { initLogLevel, log } from './logger.js';\nimport {\n handleIndexTool,\n handleSearchTool,\n handleSimilarFilesTool,\n handleGetContentTool,\n handleGetChunkTool,\n handleServerInfoTool,\n handleDeleteIndexTool,\n formatErrorResponse,\n setMcpServer\n} from './mcp-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\nconst DELETE_INDEX_TOOL: Tool = {\n name: 'delete_index',\n description: `Remove the index for a directory. Deletes all file records, vector embeddings, and the directory entry from the database.\n\nWhen to use this tool:\n- User wants to remove a directory from the search index\n- Cleaning up old or irrelevant indexed content\n- Freeing database space by removing unused indexes\n\nHow it works:\n- Removes all file records for the specified directory from SQLite\n- Deletes corresponding vector embeddings from Qdrant\n- Removes the directory entry from the directories table\n- Does NOT delete the actual files on disk\n\nExamples:\n- Remove old project: directory_path=\"/home/user/old-project\"\n- Clean up test data: directory_path=\"/home/user/test-files\"\n\nUse server_info to see what directories are currently indexed before removing.`,\n inputSchema: {\n type: 'object',\n properties: {\n directory_path: {\n type: 'string',\n description: 'Absolute path of the directory whose index should be removed'\n }\n },\n required: ['directory_path']\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: true,\n idempotentHint: true,\n openWorldHint: false\n }\n};\n\nexport function getMcpTools(): Tool[] {\n const tools: Tool[] = [\n {\n name: 'index',\n description: `Index directories to make their files searchable. Processes files to create vector embeddings for semantic search.\n\nWhen to use this tool:\n- User specifically requests indexing a directory as a knowledge base\n- Adding new documentation, code repositories, or file collections to search\n- Updating index when many files have changed\n\nHow it works:\n- Recursively scans directories for supported file types\n- Extracts text content and splits into chunks\n- Generates vector embeddings for semantic similarity\n- Stores in database for fast retrieval\n\nExamples:\n- Index documentation: [\"/home/user/docs/project-wiki\"]\n- Index codebase: [\"/home/user/projects/api-server\"]\n- Index multiple directories: [\"/home/user/docs\", \"/home/user/configs\"]\n\nIndexing can take several minutes for large directories. Most users will already have directories indexed and can directly use search tool. Use server_info to check current indexing status first.`,\n inputSchema: {\n type: 'object',\n properties: {\n directory_paths: {\n type: 'array',\n items: { type: 'string' },\n description: 'Array of absolute directory paths to index. Must be absolute paths since MCP server runs independently. Examples: [\"/home/user/projects\"] (Unix) or [\"C:\\\\\\\\Users\\\\\\\\user\\\\\\\\projects\"] (Windows)'\n }\n },\n required: ['directory_paths']\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false\n }\n },\n {\n name: 'search',\n description: `Search indexed files using natural language queries. Finds files containing content semantically similar to the query.\n\nWhen to use this tool:\n- Find documentation, guides, or explanations about specific topics\n- Locate code files implementing certain functionality or patterns\n- Discover configuration files, scripts, or settings related to a topic\n- Search for files covering specific concepts or technologies\n\nHow it works:\n- Converts query to vector embedding using semantic similarity\n- Searches all indexed file chunks for relevant content\n- Groups results by file and calculates average relevance scores\n- Returns files ranked by relevance score\n\nExamples:\n- \"database configuration and connection pooling setup\" - finds config files, documentation about DB setup\n- \"comprehensive error handling patterns and exception management\" - finds code files with exception handling\n- \"JWT authentication implementation and session management\" - finds auth-related code and docs\n- \"REST API documentation and endpoint specifications\" - finds API guides, endpoint definitions\n- \"Docker deployment scripts and CI/CD pipeline configuration\" - finds deployment automation\n\nReturns files with similarity scores and chunk information. Use get_content to retrieve full file content or get_chunk to retrieve specific chunk content by chunk ID.\n- Groups results by file to avoid duplicates from multiple matching sections\n\nResponse format:\n- Returns lightweight metadata including file paths, relevance scores, and chunk IDs\n- Use 'get_chunk' or 'get_content' tools to fetch actual content from search results\n- Chunks are sorted by relevance score within each file\n- Average similarity score calculated across all matching chunks per file\n\nExample queries:\n- \"error handling patterns and exception management strategies\" (finds try/catch, error classes, logging)\n- \"database migration scripts and schema versioning approaches\" (finds SQL, schema changes, migration files)\n- \"authentication middleware and JWT token validation logic\" (finds auth logic, JWT handling, middleware functions)`,\n inputSchema: {\n type: 'object',\n properties: {\n query: {\n type: 'string',\n description: 'Natural language search query describing what you are looking for. Can be concepts, functionality, or specific technical terms.'\n },\n limit: {\n type: 'number',\n description: 'Maximum number of files to return (default: 10). Each file may contain multiple matching chunks.',\n default: 10\n },\n workspace: {\n type: 'string',\n 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.'\n }\n },\n required: ['query']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'similar_files',\n description: `Find files with content similar to a reference file. Uses semantic similarity to find related documents, code files, or any text content.\n\nWhen to use this tool:\n- Find documentation similar to a specific guide or README\n- Locate related code files, configuration files, or scripts\n- Discover alternative implementations or approaches\n- Find files covering similar topics or concepts\n\nHow it works:\n- Analyzes the semantic content of the reference file\n- Compares against all indexed files using vector similarity\n- Returns files ranked by content similarity score\n\nExamples:\n- Given \"deployment-guide.md\" - finds other deployment docs, CI/CD guides, infrastructure setup\n- Given \"troubleshooting.md\" - finds other troubleshooting guides, FAQ files, error documentation\n- Given \"config.yaml\" - finds other configuration files, settings, environment setups\n- Given \"auth.py\" - finds other authentication modules, security code, middleware\n\nReturns file paths with similarity scores. Use get_content to read full files or get_chunk for specific sections.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the reference file. This file must have been previously indexed.'\n },\n limit: {\n type: 'number',\n description: 'Maximum number of similar files to return (default: 10). Results are sorted by similarity score.',\n default: 10\n },\n workspace: {\n type: 'string',\n 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.'\n }\n },\n required: ['file_path']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'get_content',\n description: `Retrieve the full content of a file or specific chunks. Reads files directly from the filesystem.\n\nWhen to use this tool:\n- Get complete file content after finding files through search\n- Read documentation, code files, or configuration files for analysis\n- Extract specific sections of large files using chunk ranges\n- Access any text-based file content\n\nHow it works:\n- Reads files directly from filesystem (not from search index)\n- Returns entire file by default\n- Can return specific chunk ranges for indexed files\n- Preserves original formatting and content\n\nExamples:\n- Get full file: file_path=\"/home/user/docs/api.md\"\n- Get specific chunks: file_path=\"/home/user/code/main.py\", chunks=\"2-5\"\n- Get single chunk: file_path=\"/home/user/config.json\", chunks=\"1\"\n\nReturns file content as text. Use this after search or similar_files to read actual content.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the file to retrieve. File must be readable and text-based.'\n },\n chunks: {\n type: 'string',\n description: 'Optional chunk range specification. Examples: \"3\" (single chunk), \"2-5\" (chunks 2 through 5), \"1-3\" (first three chunks). Only works for indexed files.'\n }\n },\n required: ['file_path']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'get_chunk',\n description: `Retrieve content of a specific chunk from an indexed file. Gets exact text segments identified during search.\n\nWhen to use this tool:\n- Get specific relevant sections after performing a search\n- Access only the most pertinent parts of large files\n- Retrieve content from high-scoring chunks identified in search results\n- Avoid reading entire files when only specific sections are needed\n\nHow it works:\n- Files are split into overlapping text chunks during indexing\n- Each chunk has a sequential ID (\"0\", \"1\", \"2\", etc.)\n- Search results include chunk IDs for relevant sections\n- Returns the exact content that was semantically matched\n\nExamples:\n- After search returns chunk \"3\" from \"api-docs.md\" with high score\n- Get chunk content: file_path=\"/docs/api-docs.md\", chunk_id=\"3\"\n- Returns the specific text segment that matched your query\n\nReturns chunk content as text. Use this with chunk IDs from search results to get precise content sections.`,\n inputSchema: {\n type: 'object',\n properties: {\n file_path: {\n type: 'string',\n description: 'Absolute or relative path to the indexed file containing the desired chunk.'\n },\n chunk_id: {\n type: 'string',\n description: 'ID of the specific chunk to retrieve. This is typically obtained from search results and is a sequential string like \"0\", \"1\", \"2\", etc.'\n }\n },\n required: ['file_path', 'chunk_id']\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n },\n {\n name: 'server_info',\n description: `Get information about server status and indexed content. Shows what directories and files are available for search.\n\nWhen to use this tool:\n- REQUIRED: Check available workspace names before using workspace parameter in search or similar_files tools\n- Check what content is already indexed before performing searches\n- Verify system is working properly\n- See indexing statistics and status\n- Understand scope of available searchable content\n\nHow it works:\n- Reports total indexed directories, files, and chunks\n- Shows database size and last indexing time\n- Lists all indexed directories with file counts\n- Lists all configured workspaces with their paths and file counts\n- Reports any errors or issues\n\nExamples:\n- Check workspaces before searching: \"What workspaces are available?\"\n- Check before searching: \"What content is indexed?\"\n- Verify after indexing: \"Did the indexing complete successfully?\"\n- Monitor system: \"How many files are searchable?\"\n\nReturns server version, indexing statistics, directory list, workspace information, and any errors. IMPORTANT: Always use this tool first to discover available workspace names when you need to search within specific workspaces.`,\n inputSchema: {\n type: 'object',\n properties: {},\n additionalProperties: false\n },\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n openWorldHint: false\n }\n }\n ];\n\n // Include delete_index tool unless DISABLE_DESTRUCTIVE is set\n if (process.env.DISABLE_DESTRUCTIVE !== 'true') {\n tools.push(DELETE_INDEX_TOOL);\n }\n\n return tools;\n}\n\nexport async function startMcpServer(config: Config): Promise<void> {\n initLogLevel();\n\n const server = new Server(\n {\n name: 'directory-indexer',\n version: VERSION\n },\n {\n capabilities: {\n tools: {},\n logging: {}\n }\n }\n );\n\n setMcpServer(server);\n\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: getMcpTools()\n };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n switch (name) {\n case 'index':\n return await handleIndexTool(args, config);\n \n case 'search':\n return await handleSearchTool(args);\n \n case 'similar_files':\n return await handleSimilarFilesTool(args);\n \n case 'get_content':\n return await handleGetContentTool(args, config);\n\n case 'get_chunk':\n return await handleGetChunkTool(args, config);\n \n case 'server_info':\n return await handleServerInfoTool(VERSION);\n\n case 'delete_index':\n if (process.env.DISABLE_DESTRUCTIVE === 'true') {\n throw new Error('delete_index is disabled via DISABLE_DESTRUCTIVE environment variable');\n }\n return await handleDeleteIndexTool(args, config);\n\n default:\n throw new Error(`Unknown tool: ${name}`);\n }\n } catch (error) {\n return formatErrorResponse(error);\n }\n });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n // Graceful shutdown: close all SQLite connections before exiting\n const cleanup = () => {\n log('info', 'Shutting down MCP server');\n closeAllStorage();\n process.exit(0);\n };\n process.on('SIGTERM', cleanup);\n process.on('SIGINT', cleanup);\n\n if (config.verbose) {\n console.error('MCP server started successfully');\n }\n\n log('info', 'MCP server started', { version: VERSION });\n}"],"names":["__dirname"],"mappings":"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"}
@@ -20,7 +20,8 @@ function validatePathWithinIndexedDirs(filePath, indexedDirs) {
20
20
  if (resolved === dir) {
21
21
  return;
22
22
  }
23
- if (resolved.startsWith(dir + sep)) {
23
+ const prefix = dir.endsWith(sep) ? dir : dir + sep;
24
+ if (resolved.startsWith(prefix)) {
24
25
  return;
25
26
  }
26
27
  }
@@ -1 +1 @@
1
- {"version":3,"file":"path-validation.js","sources":["../src/path-validation.ts"],"sourcesContent":["import { realpathSync } from 'fs';\nimport { resolve, sep } from 'path';\nimport { SQLiteStorage } from './storage.js';\n\n/**\n * Validates that a file path falls within one of the indexed directories.\n * Prevents path traversal attacks by resolving symlinks and checking prefixes.\n */\nexport function validatePathWithinIndexedDirs(filePath: string, indexedDirs: Set<string>): void {\n // Reject null bytes\n if (filePath.includes('\\x00')) {\n throw new Error('Access denied: path contains null bytes');\n }\n\n // Validate Windows UNC path format if it starts with \\\\\n if (filePath.startsWith('\\\\\\\\')) {\n const uncParts = filePath.split('\\\\').filter(Boolean);\n if (uncParts.length < 2) {\n throw new Error('Invalid UNC path format: expected \\\\\\\\server\\\\share\\\\... pattern');\n }\n }\n\n // Resolve the path, following symlinks if the file exists\n let resolved: string;\n try {\n resolved = realpathSync(resolve(filePath));\n } catch {\n // File may not exist yet (ENOENT), fall back to resolve only\n resolved = resolve(filePath);\n }\n\n for (const dir of indexedDirs) {\n // Exact match (the directory itself)\n if (resolved === dir) {\n return;\n }\n // Prefix match with separator to prevent /docs-evil matching /docs\n if (resolved.startsWith(dir + sep)) {\n return;\n }\n }\n\n throw new Error(\n `Access denied: ${filePath} is outside indexed directories. Only files within indexed directories can be accessed.`\n );\n}\n\n/**\n * Resolves all indexed directory paths from storage, following symlinks where possible.\n * Returns a Set of resolved absolute paths.\n */\nexport function resolveIndexedDirectories(storage: SQLiteStorage): Set<string> {\n const dirs = storage.getDirectories();\n const resolved = new Set<string>();\n\n for (const dir of dirs) {\n try {\n resolved.add(realpathSync(resolve(dir)));\n } catch {\n // Directory may have been removed; keep the raw resolved path\n resolved.add(resolve(dir));\n }\n }\n\n return resolved;\n}\n"],"names":[],"mappings":";;AAQO,SAAS,8BAA8B,UAAkB,aAAgC;AAE9F,MAAI,SAAS,SAAS,IAAM,GAAG;AAC7B,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAGA,MAAI,SAAS,WAAW,MAAM,GAAG;AAC/B,UAAM,WAAW,SAAS,MAAM,IAAI,EAAE,OAAO,OAAO;AACpD,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,IAAI,MAAM,kEAAkE;AAAA,IACpF;AAAA,EACF;AAGA,MAAI;AACJ,MAAI;AACF,eAAW,aAAa,QAAQ,QAAQ,CAAC;AAAA,EAC3C,QAAQ;AAEN,eAAW,QAAQ,QAAQ;AAAA,EAC7B;AAEA,aAAW,OAAO,aAAa;AAE7B,QAAI,aAAa,KAAK;AACpB;AAAA,IACF;AAEA,QAAI,SAAS,WAAW,MAAM,GAAG,GAAG;AAClC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,IAAI;AAAA,IACR,kBAAkB,QAAQ;AAAA,EAAA;AAE9B;AAMO,SAAS,0BAA0B,SAAqC;AAC7E,QAAM,OAAO,QAAQ,eAAA;AACrB,QAAM,+BAAe,IAAA;AAErB,aAAW,OAAO,MAAM;AACtB,QAAI;AACF,eAAS,IAAI,aAAa,QAAQ,GAAG,CAAC,CAAC;AAAA,IACzC,QAAQ;AAEN,eAAS,IAAI,QAAQ,GAAG,CAAC;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO;AACT;"}
1
+ {"version":3,"file":"path-validation.js","sources":["../src/path-validation.ts"],"sourcesContent":["import { realpathSync } from 'fs';\nimport { resolve, sep } from 'path';\nimport { SQLiteStorage } from './storage.js';\n\n/**\n * Validates that a file path falls within one of the indexed directories.\n * Prevents path traversal attacks by resolving symlinks and checking prefixes.\n */\nexport function validatePathWithinIndexedDirs(filePath: string, indexedDirs: Set<string>): void {\n // Reject null bytes\n if (filePath.includes('\\x00')) {\n throw new Error('Access denied: path contains null bytes');\n }\n\n // Validate Windows UNC path format if it starts with \\\\\n if (filePath.startsWith('\\\\\\\\')) {\n const uncParts = filePath.split('\\\\').filter(Boolean);\n if (uncParts.length < 2) {\n throw new Error('Invalid UNC path format: expected \\\\\\\\server\\\\share\\\\... pattern');\n }\n }\n\n // Resolve the path, following symlinks if the file exists\n let resolved: string;\n try {\n resolved = realpathSync(resolve(filePath));\n } catch {\n // File may not exist yet (ENOENT), fall back to resolve only\n resolved = resolve(filePath);\n }\n\n for (const dir of indexedDirs) {\n // Exact match (the directory itself)\n if (resolved === dir) {\n return;\n }\n // Prefix match with separator; roots already end with one, and the\n // separator prevents /docs-evil matching /docs\n const prefix = dir.endsWith(sep) ? dir : dir + sep;\n if (resolved.startsWith(prefix)) {\n return;\n }\n }\n\n throw new Error(\n `Access denied: ${filePath} is outside indexed directories. Only files within indexed directories can be accessed.`\n );\n}\n\n/**\n * Resolves all indexed directory paths from storage, following symlinks where possible.\n * Returns a Set of resolved absolute paths.\n */\nexport function resolveIndexedDirectories(storage: SQLiteStorage): Set<string> {\n const dirs = storage.getDirectories();\n const resolved = new Set<string>();\n\n for (const dir of dirs) {\n try {\n resolved.add(realpathSync(resolve(dir)));\n } catch {\n // Directory may have been removed; keep the raw resolved path\n resolved.add(resolve(dir));\n }\n }\n\n return resolved;\n}\n"],"names":[],"mappings":";;AAQO,SAAS,8BAA8B,UAAkB,aAAgC;AAE9F,MAAI,SAAS,SAAS,IAAM,GAAG;AAC7B,UAAM,IAAI,MAAM,yCAAyC;AAAA,EAC3D;AAGA,MAAI,SAAS,WAAW,MAAM,GAAG;AAC/B,UAAM,WAAW,SAAS,MAAM,IAAI,EAAE,OAAO,OAAO;AACpD,QAAI,SAAS,SAAS,GAAG;AACvB,YAAM,IAAI,MAAM,kEAAkE;AAAA,IACpF;AAAA,EACF;AAGA,MAAI;AACJ,MAAI;AACF,eAAW,aAAa,QAAQ,QAAQ,CAAC;AAAA,EAC3C,QAAQ;AAEN,eAAW,QAAQ,QAAQ;AAAA,EAC7B;AAEA,aAAW,OAAO,aAAa;AAE7B,QAAI,aAAa,KAAK;AACpB;AAAA,IACF;AAGA,UAAM,SAAS,IAAI,SAAS,GAAG,IAAI,MAAM,MAAM;AAC/C,QAAI,SAAS,WAAW,MAAM,GAAG;AAC/B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,IAAI;AAAA,IACR,kBAAkB,QAAQ;AAAA,EAAA;AAE9B;AAMO,SAAS,0BAA0B,SAAqC;AAC7E,QAAM,OAAO,QAAQ,eAAA;AACrB,QAAM,+BAAe,IAAA;AAErB,aAAW,OAAO,MAAM;AACtB,QAAI;AACF,eAAS,IAAI,aAAa,QAAQ,GAAG,CAAC,CAAC;AAAA,IACzC,QAAQ;AAEN,eAAS,IAAI,QAAQ,GAAG,CAAC;AAAA,IAC3B;AAAA,EACF;AAEA,SAAO;AACT;"}
@@ -1,3 +1,4 @@
1
+ import { QdrantClient } from "./storage.js";
1
2
  class PrerequisiteError extends Error {
2
3
  constructor(message, cause) {
3
4
  super(message);
@@ -6,12 +7,8 @@ class PrerequisiteError extends Error {
6
7
  }
7
8
  }
8
9
  async function checkQdrant(config) {
9
- try {
10
- const response = await fetch(`${config.storage.qdrantEndpoint}/healthz`);
11
- return response.ok;
12
- } catch {
13
- return false;
14
- }
10
+ const qdrant = new QdrantClient(config);
11
+ return qdrant.healthCheck();
15
12
  }
16
13
  async function checkOllama(config) {
17
14
  try {
@@ -35,7 +32,7 @@ async function checkOllamaModel(config) {
35
32
  async function checkOpenAI(config) {
36
33
  if (!process.env.OPENAI_API_KEY) return false;
37
34
  try {
38
- const response = await fetch("https://api.openai.com/v1/embeddings", {
35
+ const response = await fetch(`${config.embedding.endpoint}/embeddings`, {
39
36
  method: "POST",
40
37
  headers: {
41
38
  "Content-Type": "application/json",
@@ -57,7 +54,7 @@ async function checkAllPrerequisitesDetailed(config) {
57
54
  services.push({
58
55
  service: "qdrant",
59
56
  status: qdrantOk ? "available" : "unavailable",
60
- details: qdrantOk ? void 0 : `Cannot connect to Qdrant at ${config.storage.qdrantEndpoint}`
57
+ details: qdrantOk ? void 0 : "Cannot connect to Qdrant"
61
58
  });
62
59
  let embeddingOk = false;
63
60
  let embeddingDetails;
@@ -1 +1 @@
1
- {"version":3,"file":"prerequisites.js","sources":["../src/prerequisites.ts"],"sourcesContent":["import { Config } from './config.js';\n\nexport class PrerequisiteError extends Error {\n constructor(message: string, public override cause?: Error) {\n super(message);\n this.name = 'PrerequisiteError';\n }\n}\n\n/**\n * Check if Qdrant is accessible\n */\nexport async function checkQdrant(config: Config): Promise<boolean> {\n try {\n const response = await fetch(`${config.storage.qdrantEndpoint}/healthz`);\n return response.ok;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if Ollama is accessible\n */\nexport async function checkOllama(config: Config): Promise<boolean> {\n try {\n const response = await fetch(`${config.embedding.endpoint}/api/tags`);\n return response.ok;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if Ollama model is available\n */\nexport async function checkOllamaModel(config: Config): Promise<boolean> {\n try {\n const response = await fetch(`${config.embedding.endpoint}/api/tags`);\n if (!response.ok) return false;\n \n const data = await response.json();\n const models = data.models || [];\n return models.some((model: { name: string }) => model.name.includes(config.embedding.model));\n } catch {\n return false;\n }\n}\n\n/**\n * Check if OpenAI is accessible\n */\nexport async function checkOpenAI(config: Config): Promise<boolean> {\n if (!process.env.OPENAI_API_KEY) return false;\n \n try {\n const response = await fetch('https://api.openai.com/v1/embeddings', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`\n },\n body: JSON.stringify({\n model: config.embedding.model,\n input: 'test'\n })\n });\n return response.ok;\n } catch {\n return false;\n }\n}\n\nexport interface ServiceStatus {\n service: string;\n status: 'available' | 'unavailable';\n details?: string;\n}\n\nexport interface PrerequisiteValidationResult {\n allPassed: boolean;\n services: ServiceStatus[];\n}\n\n/**\n * Check all prerequisites and return structured status\n */\nexport async function checkAllPrerequisitesDetailed(config: Config): Promise<PrerequisiteValidationResult> {\n const services: ServiceStatus[] = [];\n \n // Check Qdrant\n const qdrantOk = await checkQdrant(config);\n services.push({\n service: 'qdrant',\n status: qdrantOk ? 'available' : 'unavailable',\n details: qdrantOk ? undefined : `Cannot connect to Qdrant at ${config.storage.qdrantEndpoint}`\n });\n \n // Check embedding service\n let embeddingOk = false;\n let embeddingDetails: string | undefined;\n \n if (config.embedding.provider === 'ollama') {\n const ollamaOk = await checkOllama(config);\n const modelOk = ollamaOk ? await checkOllamaModel(config) : false;\n embeddingOk = ollamaOk && modelOk;\n \n if (!ollamaOk) {\n embeddingDetails = `Cannot connect to Ollama at ${config.embedding.endpoint}`;\n } else if (!modelOk) {\n embeddingDetails = `Model \"${config.embedding.model}\" not available in Ollama`;\n }\n } else if (config.embedding.provider === 'openai') {\n embeddingOk = await checkOpenAI(config);\n if (!embeddingOk) {\n embeddingDetails = process.env.OPENAI_API_KEY ? \n 'OpenAI API request failed' : \n 'OPENAI_API_KEY environment variable not set';\n }\n } else if (config.embedding.provider === 'mock') {\n embeddingOk = true;\n } else {\n embeddingDetails = `Unknown embedding provider: ${config.embedding.provider}`;\n }\n \n services.push({\n service: config.embedding.provider,\n status: embeddingOk ? 'available' : 'unavailable',\n details: embeddingDetails\n });\n \n return {\n allPassed: services.every(s => s.status === 'available'),\n services\n };\n}\n\n/**\n * Generate comprehensive error message that lists all missing services\n */\nfunction createComprehensiveErrorMessage(result: PrerequisiteValidationResult): string {\n const unavailableServices = result.services.filter(s => s.status === 'unavailable');\n \n const serviceDescriptions = unavailableServices.map(service => {\n return `${service.service} (${service.details})`;\n });\n \n const message = `Required services are not available to use directory-indexer features: ${serviceDescriptions.join(', ')}.`;\n const setup = 'For setup instructions, see: https://github.com/peteretelej/directory-indexer#setup';\n \n return `${message}\\n\\n${setup}`;\n}\n\n/**\n * Validate all prerequisites for indexing (needs both Qdrant and embedding service)\n */\nexport async function validateIndexPrerequisites(config: Config): Promise<void> {\n const result = await checkAllPrerequisitesDetailed(config);\n \n if (!result.allPassed) {\n throw new PrerequisiteError(createComprehensiveErrorMessage(result));\n }\n}\n\n/**\n * Validate prerequisites for search (needs only Qdrant)\n */\nexport async function validateSearchPrerequisites(config: Config): Promise<void> {\n const result = await checkAllPrerequisitesDetailed(config);\n const qdrantService = result.services.find(s => s.service === 'qdrant');\n \n if (qdrantService?.status === 'unavailable') {\n // Create a result with only Qdrant for error message\n const qdrantOnlyResult: PrerequisiteValidationResult = {\n allPassed: false,\n services: [qdrantService]\n };\n throw new PrerequisiteError(createComprehensiveErrorMessage(qdrantOnlyResult));\n }\n}\n\n/**\n * Check embedding service based on provider\n */\nasync function checkEmbeddingService(config: Config): Promise<boolean> {\n switch (config.embedding.provider) {\n case 'ollama':\n return (await checkOllama(config)) && (await checkOllamaModel(config));\n case 'openai':\n return await checkOpenAI(config);\n case 'mock':\n return true;\n default:\n return false;\n }\n}\n\n/**\n * Get status of all services (for status command)\n */\nexport async function getServiceStatus(config: Config): Promise<{\n qdrant: boolean;\n embedding: boolean;\n embeddingProvider: string;\n}> {\n const [qdrantOk, embeddingOk] = await Promise.all([\n checkQdrant(config),\n checkEmbeddingService(config)\n ]);\n \n return {\n qdrant: qdrantOk,\n embedding: embeddingOk,\n embeddingProvider: config.embedding.provider\n };\n}"],"names":[],"mappings":"AAEO,MAAM,0BAA0B,MAAM;AAAA,EAC3C,YAAY,SAAiC,OAAe;AAC1D,UAAM,OAAO;AAD8B,SAAA,QAAA;AAE3C,SAAK,OAAO;AAAA,EACd;AACF;AAKA,eAAsB,YAAY,QAAkC;AAClE,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,GAAG,OAAO,QAAQ,cAAc,UAAU;AACvE,WAAO,SAAS;AAAA,EAClB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAsB,YAAY,QAAkC;AAClE,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,GAAG,OAAO,UAAU,QAAQ,WAAW;AACpE,WAAO,SAAS;AAAA,EAClB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAsB,iBAAiB,QAAkC;AACvE,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,GAAG,OAAO,UAAU,QAAQ,WAAW;AACpE,QAAI,CAAC,SAAS,GAAI,QAAO;AAEzB,UAAM,OAAO,MAAM,SAAS,KAAA;AAC5B,UAAM,SAAS,KAAK,UAAU,CAAA;AAC9B,WAAO,OAAO,KAAK,CAAC,UAA4B,MAAM,KAAK,SAAS,OAAO,UAAU,KAAK,CAAC;AAAA,EAC7F,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAsB,YAAY,QAAkC;AAClE,MAAI,CAAC,QAAQ,IAAI,eAAgB,QAAO;AAExC,MAAI;AACF,UAAM,WAAW,MAAM,MAAM,wCAAwC;AAAA,MACnE,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,iBAAiB,UAAU,QAAQ,IAAI,cAAc;AAAA,MAAA;AAAA,MAEvD,MAAM,KAAK,UAAU;AAAA,QACnB,OAAO,OAAO,UAAU;AAAA,QACxB,OAAO;AAAA,MAAA,CACR;AAAA,IAAA,CACF;AACD,WAAO,SAAS;AAAA,EAClB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAgBA,eAAsB,8BAA8B,QAAuD;AACzG,QAAM,WAA4B,CAAA;AAGlC,QAAM,WAAW,MAAM,YAAY,MAAM;AACzC,WAAS,KAAK;AAAA,IACZ,SAAS;AAAA,IACT,QAAQ,WAAW,cAAc;AAAA,IACjC,SAAS,WAAW,SAAY,+BAA+B,OAAO,QAAQ,cAAc;AAAA,EAAA,CAC7F;AAGD,MAAI,cAAc;AAClB,MAAI;AAEJ,MAAI,OAAO,UAAU,aAAa,UAAU;AAC1C,UAAM,WAAW,MAAM,YAAY,MAAM;AACzC,UAAM,UAAU,WAAW,MAAM,iBAAiB,MAAM,IAAI;AAC5D,kBAAc,YAAY;AAE1B,QAAI,CAAC,UAAU;AACb,yBAAmB,+BAA+B,OAAO,UAAU,QAAQ;AAAA,IAC7E,WAAW,CAAC,SAAS;AACnB,yBAAmB,UAAU,OAAO,UAAU,KAAK;AAAA,IACrD;AAAA,EACF,WAAW,OAAO,UAAU,aAAa,UAAU;AACjD,kBAAc,MAAM,YAAY,MAAM;AACtC,QAAI,CAAC,aAAa;AAChB,yBAAmB,QAAQ,IAAI,iBAC7B,8BACA;AAAA,IACJ;AAAA,EACF,WAAW,OAAO,UAAU,aAAa,QAAQ;AAC/C,kBAAc;AAAA,EAChB,OAAO;AACL,uBAAmB,+BAA+B,OAAO,UAAU,QAAQ;AAAA,EAC7E;AAEA,WAAS,KAAK;AAAA,IACZ,SAAS,OAAO,UAAU;AAAA,IAC1B,QAAQ,cAAc,cAAc;AAAA,IACpC,SAAS;AAAA,EAAA,CACV;AAED,SAAO;AAAA,IACL,WAAW,SAAS,MAAM,CAAA,MAAK,EAAE,WAAW,WAAW;AAAA,IACvD;AAAA,EAAA;AAEJ;AAKA,SAAS,gCAAgC,QAA8C;AACrF,QAAM,sBAAsB,OAAO,SAAS,OAAO,CAAA,MAAK,EAAE,WAAW,aAAa;AAElF,QAAM,sBAAsB,oBAAoB,IAAI,CAAA,YAAW;AAC7D,WAAO,GAAG,QAAQ,OAAO,KAAK,QAAQ,OAAO;AAAA,EAC/C,CAAC;AAED,QAAM,UAAU,0EAA0E,oBAAoB,KAAK,IAAI,CAAC;AACxH,QAAM,QAAQ;AAEd,SAAO,GAAG,OAAO;AAAA;AAAA,EAAO,KAAK;AAC/B;AAKA,eAAsB,2BAA2B,QAA+B;AAC9E,QAAM,SAAS,MAAM,8BAA8B,MAAM;AAEzD,MAAI,CAAC,OAAO,WAAW;AACrB,UAAM,IAAI,kBAAkB,gCAAgC,MAAM,CAAC;AAAA,EACrE;AACF;AAKA,eAAsB,4BAA4B,QAA+B;AAC/E,QAAM,SAAS,MAAM,8BAA8B,MAAM;AACzD,QAAM,gBAAgB,OAAO,SAAS,KAAK,CAAA,MAAK,EAAE,YAAY,QAAQ;AAEtE,MAAI,eAAe,WAAW,eAAe;AAE3C,UAAM,mBAAiD;AAAA,MAErD,UAAU,CAAC,aAAa;AAAA,IAAA;AAE1B,UAAM,IAAI,kBAAkB,gCAAgC,gBAAgB,CAAC;AAAA,EAC/E;AACF;AAKA,eAAe,sBAAsB,QAAkC;AACrE,UAAQ,OAAO,UAAU,UAAA;AAAA,IACvB,KAAK;AACH,aAAQ,MAAM,YAAY,MAAM,KAAO,MAAM,iBAAiB,MAAM;AAAA,IACtE,KAAK;AACH,aAAO,MAAM,YAAY,MAAM;AAAA,IACjC,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EAAA;AAEb;AAKA,eAAsB,iBAAiB,QAIpC;AACD,QAAM,CAAC,UAAU,WAAW,IAAI,MAAM,QAAQ,IAAI;AAAA,IAChD,YAAY,MAAM;AAAA,IAClB,sBAAsB,MAAM;AAAA,EAAA,CAC7B;AAED,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,mBAAmB,OAAO,UAAU;AAAA,EAAA;AAExC;"}
1
+ {"version":3,"file":"prerequisites.js","sources":["../src/prerequisites.ts"],"sourcesContent":["import { Config } from './config.js';\nimport { QdrantClient } from './storage.js';\n\nexport class PrerequisiteError extends Error {\n constructor(message: string, public override cause?: Error) {\n super(message);\n this.name = 'PrerequisiteError';\n }\n}\n\n/**\n * Check if Qdrant is accessible\n */\nexport async function checkQdrant(config: Config): Promise<boolean> {\n const qdrant = new QdrantClient(config);\n return qdrant.healthCheck();\n}\n\n/**\n * Check if Ollama is accessible\n */\nexport async function checkOllama(config: Config): Promise<boolean> {\n try {\n const response = await fetch(`${config.embedding.endpoint}/api/tags`);\n return response.ok;\n } catch {\n return false;\n }\n}\n\n/**\n * Check if Ollama model is available\n */\nexport async function checkOllamaModel(config: Config): Promise<boolean> {\n try {\n const response = await fetch(`${config.embedding.endpoint}/api/tags`);\n if (!response.ok) return false;\n \n const data = await response.json();\n const models = data.models || [];\n return models.some((model: { name: string }) => model.name.includes(config.embedding.model));\n } catch {\n return false;\n }\n}\n\n/**\n * Check if OpenAI is accessible\n */\nexport async function checkOpenAI(config: Config): Promise<boolean> {\n if (!process.env.OPENAI_API_KEY) return false;\n \n try {\n const response = await fetch(`${config.embedding.endpoint}/embeddings`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`\n },\n body: JSON.stringify({\n model: config.embedding.model,\n input: 'test'\n })\n });\n return response.ok;\n } catch {\n return false;\n }\n}\n\nexport interface ServiceStatus {\n service: string;\n status: 'available' | 'unavailable';\n details?: string;\n}\n\nexport interface PrerequisiteValidationResult {\n allPassed: boolean;\n services: ServiceStatus[];\n}\n\n/**\n * Check all prerequisites and return structured status\n */\nexport async function checkAllPrerequisitesDetailed(config: Config): Promise<PrerequisiteValidationResult> {\n const services: ServiceStatus[] = [];\n \n // Check Qdrant\n const qdrantOk = await checkQdrant(config);\n services.push({\n service: 'qdrant',\n status: qdrantOk ? 'available' : 'unavailable',\n details: qdrantOk ? undefined : 'Cannot connect to Qdrant'\n });\n \n // Check embedding service\n let embeddingOk = false;\n let embeddingDetails: string | undefined;\n \n if (config.embedding.provider === 'ollama') {\n const ollamaOk = await checkOllama(config);\n const modelOk = ollamaOk ? await checkOllamaModel(config) : false;\n embeddingOk = ollamaOk && modelOk;\n \n if (!ollamaOk) {\n embeddingDetails = `Cannot connect to Ollama at ${config.embedding.endpoint}`;\n } else if (!modelOk) {\n embeddingDetails = `Model \"${config.embedding.model}\" not available in Ollama`;\n }\n } else if (config.embedding.provider === 'openai') {\n embeddingOk = await checkOpenAI(config);\n if (!embeddingOk) {\n embeddingDetails = process.env.OPENAI_API_KEY ? \n 'OpenAI API request failed' : \n 'OPENAI_API_KEY environment variable not set';\n }\n } else if (config.embedding.provider === 'mock') {\n embeddingOk = true;\n } else {\n embeddingDetails = `Unknown embedding provider: ${config.embedding.provider}`;\n }\n \n services.push({\n service: config.embedding.provider,\n status: embeddingOk ? 'available' : 'unavailable',\n details: embeddingDetails\n });\n \n return {\n allPassed: services.every(s => s.status === 'available'),\n services\n };\n}\n\n/**\n * Generate comprehensive error message that lists all missing services\n */\nfunction createComprehensiveErrorMessage(result: PrerequisiteValidationResult): string {\n const unavailableServices = result.services.filter(s => s.status === 'unavailable');\n \n const serviceDescriptions = unavailableServices.map(service => {\n return `${service.service} (${service.details})`;\n });\n \n const message = `Required services are not available to use directory-indexer features: ${serviceDescriptions.join(', ')}.`;\n const setup = 'For setup instructions, see: https://github.com/peteretelej/directory-indexer#setup';\n \n return `${message}\\n\\n${setup}`;\n}\n\n/**\n * Validate all prerequisites for indexing (needs both Qdrant and embedding service)\n */\nexport async function validateIndexPrerequisites(config: Config): Promise<void> {\n const result = await checkAllPrerequisitesDetailed(config);\n \n if (!result.allPassed) {\n throw new PrerequisiteError(createComprehensiveErrorMessage(result));\n }\n}\n\n/**\n * Validate prerequisites for search (needs only Qdrant)\n */\nexport async function validateSearchPrerequisites(config: Config): Promise<void> {\n const result = await checkAllPrerequisitesDetailed(config);\n const qdrantService = result.services.find(s => s.service === 'qdrant');\n \n if (qdrantService?.status === 'unavailable') {\n // Create a result with only Qdrant for error message\n const qdrantOnlyResult: PrerequisiteValidationResult = {\n allPassed: false,\n services: [qdrantService]\n };\n throw new PrerequisiteError(createComprehensiveErrorMessage(qdrantOnlyResult));\n }\n}\n\n/**\n * Check embedding service based on provider\n */\nasync function checkEmbeddingService(config: Config): Promise<boolean> {\n switch (config.embedding.provider) {\n case 'ollama':\n return (await checkOllama(config)) && (await checkOllamaModel(config));\n case 'openai':\n return await checkOpenAI(config);\n case 'mock':\n return true;\n default:\n return false;\n }\n}\n\n/**\n * Get status of all services (for status command)\n */\nexport async function getServiceStatus(config: Config): Promise<{\n qdrant: boolean;\n embedding: boolean;\n embeddingProvider: string;\n}> {\n const [qdrantOk, embeddingOk] = await Promise.all([\n checkQdrant(config),\n checkEmbeddingService(config)\n ]);\n \n return {\n qdrant: qdrantOk,\n embedding: embeddingOk,\n embeddingProvider: config.embedding.provider\n };\n}"],"names":[],"mappings":"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"}
package/dist/reset.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readlineSync } from "./utils.js";
2
- import { getResetPreview, clearDatabase, StorageError, clearVectorCollection } from "./storage.js";
2
+ import { getResetPreview, clearVectorCollection, StorageError, clearDatabase } from "./storage.js";
3
3
  async function resetEnvironment(config, options = {}) {
4
4
  const stats = await getResetPreview(config);
5
5
  if (!options.force) {
@@ -20,7 +20,7 @@ async function showConfirmation(stats, config) {
20
20
  } else {
21
21
  console.log(` • Qdrant collection: ${config.storage.qdrantCollection} (not found)`);
22
22
  }
23
- if (config.storage.qdrantEndpoint !== "http://localhost:6333") {
23
+ if (config.storage.qdrantEndpoint !== "http://127.0.0.1:6333") {
24
24
  console.log(` • Qdrant endpoint: ${config.storage.qdrantEndpoint}`);
25
25
  }
26
26
  console.log("\nYour original files will not be touched.");
@@ -30,53 +30,43 @@ async function showConfirmation(stats, config) {
30
30
  }
31
31
  }
32
32
  async function performReset(config, options) {
33
- let sqliteDeleted = false;
34
- let qdrantDeleted = false;
35
- const warnings = [];
36
33
  if (options.verbose) {
37
34
  console.log("\nResetting directory-indexer data...");
38
35
  }
39
36
  try {
40
37
  if (options.verbose) {
41
- console.log(` ✓ Deleting SQLite database: ${config.storage.sqlitePath}`);
38
+ console.log(` ✓ Deleting Qdrant collection: ${config.storage.qdrantCollection}`);
42
39
  }
43
- sqliteDeleted = await clearDatabase(config);
44
- if (options.verbose && sqliteDeleted) {
45
- console.log(` ✓ SQLite database cleared`);
40
+ const qdrantDeleted = await clearVectorCollection(config);
41
+ if (options.verbose && qdrantDeleted) {
42
+ console.log(` ✓ Qdrant collection cleared`);
46
43
  }
47
44
  } catch (error) {
48
- const message = error instanceof StorageError ? error.message : `Failed to clear database: ${error}`;
49
- warnings.push(message);
50
45
  if (options.verbose) {
46
+ const message = error instanceof StorageError ? error.message : `Failed to clear collection: ${error}`;
51
47
  console.log(` ⚠ ${message}`);
48
+ console.log("\nSQLite database left untouched. Fix the issue and re-run `directory-indexer reset`.");
52
49
  }
50
+ throw error;
53
51
  }
54
52
  try {
55
53
  if (options.verbose) {
56
- console.log(` ✓ Deleting Qdrant collection: ${config.storage.qdrantCollection}`);
54
+ console.log(` ✓ Deleting SQLite database: ${config.storage.sqlitePath}`);
57
55
  }
58
- qdrantDeleted = await clearVectorCollection(config);
59
- if (options.verbose && qdrantDeleted) {
60
- console.log(` ✓ Qdrant collection cleared`);
56
+ const sqliteDeleted = await clearDatabase(config);
57
+ if (options.verbose && sqliteDeleted) {
58
+ console.log(` ✓ SQLite database cleared`);
61
59
  }
62
60
  } catch (error) {
63
- const message = error instanceof StorageError ? error.message : `Failed to clear collection: ${error}`;
64
- warnings.push(message);
65
- if (options.verbose) {
66
- console.log(` ⚠ ${message}`);
67
- }
68
- }
69
- if (warnings.length > 0) {
70
- console.log("\nReset completed with warnings:");
71
- warnings.forEach((warning) => console.log(` ⚠ ${warning}`));
72
- if (sqliteDeleted || qdrantDeleted) {
73
- console.log("\nPartial reset successful. Directory-indexer is ready for fresh indexing.");
74
- } else {
75
- console.log("\nReset had issues but you can try again or check your configuration.");
76
- }
77
- } else {
78
- console.log("\nReset complete. Directory-indexer is ready for fresh indexing.");
61
+ const message = error instanceof StorageError ? error.message : `Failed to clear database: ${error}`;
62
+ console.log("\nReset incomplete:");
63
+ console.log(` ✓ Vector data deleted from Qdrant collection: ${config.storage.qdrantCollection}`);
64
+ console.log(` ⚠ SQLite cleanup failed: ${message}`);
65
+ console.log("\nVector data is gone but SQLite metadata remains.");
66
+ console.log("Fix the issue, then re-run `directory-indexer reset` to finish the reset.");
67
+ throw error;
79
68
  }
69
+ console.log("\nReset complete. Directory-indexer is ready for fresh indexing.");
80
70
  }
81
71
  export {
82
72
  resetEnvironment
package/dist/reset.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"reset.js","sources":["../src/reset.ts"],"sourcesContent":["import { readlineSync } from './utils.js';\nimport { getResetPreview, clearDatabase, clearVectorCollection, StorageError, type ResetStats } from './storage.js';\nimport type { Config } from './config.js';\n\nexport interface ResetOptions {\n force?: boolean;\n verbose?: boolean;\n}\n\nexport async function resetEnvironment(config: Config, options: ResetOptions = {}): Promise<void> {\n const stats = await getResetPreview(config);\n \n if (!options.force) {\n await showConfirmation(stats, config);\n }\n \n await performReset(config, options);\n}\n\n\nasync function showConfirmation(stats: ResetStats, config: Config): Promise<void> {\n console.log('\\nThe following directory-indexer data will be reset:');\n \n if (stats.sqliteExists) {\n console.log(` • SQLite database: ${config.storage.sqlitePath} (${stats.sqliteSize})`);\n } else {\n console.log(` • SQLite database: ${config.storage.sqlitePath} (not found)`);\n }\n \n if (stats.qdrantCollectionExists) {\n const vectorText = stats.qdrantVectorCount === 1 ? 'vector' : 'vectors';\n console.log(` • Qdrant collection: ${config.storage.qdrantCollection} (${stats.qdrantVectorCount} ${vectorText})`);\n } else {\n console.log(` • Qdrant collection: ${config.storage.qdrantCollection} (not found)`);\n }\n \n if (config.storage.qdrantEndpoint !== 'http://localhost:6333') {\n console.log(` • Qdrant endpoint: ${config.storage.qdrantEndpoint}`);\n }\n \n console.log('\\nYour original files will not be touched.');\n \n const answer = await readlineSync('\\nContinue? (y/N): ');\n if (!answer || !['y', 'yes'].includes(answer.toLowerCase().trim())) {\n throw new Error('Reset cancelled by user');\n }\n}\n\nasync function performReset(config: Config, options: ResetOptions): Promise<void> {\n let sqliteDeleted = false;\n let qdrantDeleted = false;\n const warnings: string[] = [];\n\n if (options.verbose) {\n console.log('\\nResetting directory-indexer data...');\n }\n\n // Reset SQLite database\n try {\n if (options.verbose) {\n console.log(` ✓ Deleting SQLite database: ${config.storage.sqlitePath}`);\n }\n sqliteDeleted = await clearDatabase(config);\n if (options.verbose && sqliteDeleted) {\n console.log(` ✓ SQLite database cleared`);\n }\n } catch (error) {\n const message = error instanceof StorageError ? error.message : `Failed to clear database: ${error}`;\n warnings.push(message);\n if (options.verbose) {\n console.log(` ⚠ ${message}`);\n }\n }\n\n // Reset Qdrant collection\n try {\n if (options.verbose) {\n console.log(` ✓ Deleting Qdrant collection: ${config.storage.qdrantCollection}`);\n }\n qdrantDeleted = await clearVectorCollection(config);\n if (options.verbose && qdrantDeleted) {\n console.log(` ✓ Qdrant collection cleared`);\n }\n } catch (error) {\n const message = error instanceof StorageError ? error.message : `Failed to clear collection: ${error}`;\n warnings.push(message);\n if (options.verbose) {\n console.log(` ⚠ ${message}`);\n }\n }\n\n // Show results\n if (warnings.length > 0) {\n console.log('\\nReset completed with warnings:');\n warnings.forEach(warning => console.log(` ⚠ ${warning}`));\n \n if (sqliteDeleted || qdrantDeleted) {\n console.log('\\nPartial reset successful. Directory-indexer is ready for fresh indexing.');\n } else {\n console.log('\\nReset had issues but you can try again or check your configuration.');\n }\n } else {\n console.log('\\nReset complete. Directory-indexer is ready for fresh indexing.');\n }\n}"],"names":[],"mappings":";;AASA,eAAsB,iBAAiB,QAAgB,UAAwB,IAAmB;AAChG,QAAM,QAAQ,MAAM,gBAAgB,MAAM;AAE1C,MAAI,CAAC,QAAQ,OAAO;AAClB,UAAM,iBAAiB,OAAO,MAAM;AAAA,EACtC;AAEA,QAAM,aAAa,QAAQ,OAAO;AACpC;AAGA,eAAe,iBAAiB,OAAmB,QAA+B;AAChF,UAAQ,IAAI,uDAAuD;AAEnE,MAAI,MAAM,cAAc;AACtB,YAAQ,IAAI,wBAAwB,OAAO,QAAQ,UAAU,KAAK,MAAM,UAAU,GAAG;AAAA,EACvF,OAAO;AACL,YAAQ,IAAI,wBAAwB,OAAO,QAAQ,UAAU,cAAc;AAAA,EAC7E;AAEA,MAAI,MAAM,wBAAwB;AAChC,UAAM,aAAa,MAAM,sBAAsB,IAAI,WAAW;AAC9D,YAAQ,IAAI,0BAA0B,OAAO,QAAQ,gBAAgB,KAAK,MAAM,iBAAiB,IAAI,UAAU,GAAG;AAAA,EACpH,OAAO;AACL,YAAQ,IAAI,0BAA0B,OAAO,QAAQ,gBAAgB,cAAc;AAAA,EACrF;AAEA,MAAI,OAAO,QAAQ,mBAAmB,yBAAyB;AAC7D,YAAQ,IAAI,wBAAwB,OAAO,QAAQ,cAAc,EAAE;AAAA,EACrE;AAEA,UAAQ,IAAI,4CAA4C;AAExD,QAAM,SAAS,MAAM,aAAa,qBAAqB;AACvD,MAAI,CAAC,UAAU,CAAC,CAAC,KAAK,KAAK,EAAE,SAAS,OAAO,cAAc,KAAA,CAAM,GAAG;AAClE,UAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C;AACF;AAEA,eAAe,aAAa,QAAgB,SAAsC;AAChF,MAAI,gBAAgB;AACpB,MAAI,gBAAgB;AACpB,QAAM,WAAqB,CAAA;AAE3B,MAAI,QAAQ,SAAS;AACnB,YAAQ,IAAI,uCAAuC;AAAA,EACrD;AAGA,MAAI;AACF,QAAI,QAAQ,SAAS;AACnB,cAAQ,IAAI,iCAAiC,OAAO,QAAQ,UAAU,EAAE;AAAA,IAC1E;AACA,oBAAgB,MAAM,cAAc,MAAM;AAC1C,QAAI,QAAQ,WAAW,eAAe;AACpC,cAAQ,IAAI,6BAA6B;AAAA,IAC3C;AAAA,EACF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,eAAe,MAAM,UAAU,6BAA6B,KAAK;AAClG,aAAS,KAAK,OAAO;AACrB,QAAI,QAAQ,SAAS;AACnB,cAAQ,IAAI,OAAO,OAAO,EAAE;AAAA,IAC9B;AAAA,EACF;AAGA,MAAI;AACF,QAAI,QAAQ,SAAS;AACnB,cAAQ,IAAI,mCAAmC,OAAO,QAAQ,gBAAgB,EAAE;AAAA,IAClF;AACA,oBAAgB,MAAM,sBAAsB,MAAM;AAClD,QAAI,QAAQ,WAAW,eAAe;AACpC,cAAQ,IAAI,+BAA+B;AAAA,IAC7C;AAAA,EACF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,eAAe,MAAM,UAAU,+BAA+B,KAAK;AACpG,aAAS,KAAK,OAAO;AACrB,QAAI,QAAQ,SAAS;AACnB,cAAQ,IAAI,OAAO,OAAO,EAAE;AAAA,IAC9B;AAAA,EACF;AAGA,MAAI,SAAS,SAAS,GAAG;AACvB,YAAQ,IAAI,kCAAkC;AAC9C,aAAS,QAAQ,CAAA,YAAW,QAAQ,IAAI,OAAO,OAAO,EAAE,CAAC;AAEzD,QAAI,iBAAiB,eAAe;AAClC,cAAQ,IAAI,4EAA4E;AAAA,IAC1F,OAAO;AACL,cAAQ,IAAI,uEAAuE;AAAA,IACrF;AAAA,EACF,OAAO;AACL,YAAQ,IAAI,kEAAkE;AAAA,EAChF;AACF;"}
1
+ {"version":3,"file":"reset.js","sources":["../src/reset.ts"],"sourcesContent":["import { readlineSync } from './utils.js';\nimport { getResetPreview, clearDatabase, clearVectorCollection, StorageError, type ResetStats } from './storage.js';\nimport type { Config } from './config.js';\n\nexport interface ResetOptions {\n force?: boolean;\n verbose?: boolean;\n}\n\nexport async function resetEnvironment(config: Config, options: ResetOptions = {}): Promise<void> {\n const stats = await getResetPreview(config);\n \n if (!options.force) {\n await showConfirmation(stats, config);\n }\n \n await performReset(config, options);\n}\n\n\nasync function showConfirmation(stats: ResetStats, config: Config): Promise<void> {\n console.log('\\nThe following directory-indexer data will be reset:');\n \n if (stats.sqliteExists) {\n console.log(` • SQLite database: ${config.storage.sqlitePath} (${stats.sqliteSize})`);\n } else {\n console.log(` • SQLite database: ${config.storage.sqlitePath} (not found)`);\n }\n \n if (stats.qdrantCollectionExists) {\n const vectorText = stats.qdrantVectorCount === 1 ? 'vector' : 'vectors';\n console.log(` • Qdrant collection: ${config.storage.qdrantCollection} (${stats.qdrantVectorCount} ${vectorText})`);\n } else {\n console.log(` • Qdrant collection: ${config.storage.qdrantCollection} (not found)`);\n }\n \n if (config.storage.qdrantEndpoint !== 'http://127.0.0.1:6333') {\n console.log(` • Qdrant endpoint: ${config.storage.qdrantEndpoint}`);\n }\n \n console.log('\\nYour original files will not be touched.');\n \n const answer = await readlineSync('\\nContinue? (y/N): ');\n if (!answer || !['y', 'yes'].includes(answer.toLowerCase().trim())) {\n throw new Error('Reset cancelled by user');\n }\n}\n\nasync function performReset(config: Config, options: ResetOptions): Promise<void> {\n if (options.verbose) {\n console.log('\\nResetting directory-indexer data...');\n }\n\n // Reset Qdrant collection first: if this fails, stop before touching SQLite\n // so metadata and vectors stay consistent and re-running reset can retry both.\n try {\n if (options.verbose) {\n console.log(` ✓ Deleting Qdrant collection: ${config.storage.qdrantCollection}`);\n }\n const qdrantDeleted = await clearVectorCollection(config);\n if (options.verbose && qdrantDeleted) {\n console.log(` ✓ Qdrant collection cleared`);\n }\n } catch (error) {\n if (options.verbose) {\n const message = error instanceof StorageError ? error.message : `Failed to clear collection: ${error}`;\n console.log(` ⚠ ${message}`);\n console.log('\\nSQLite database left untouched. Fix the issue and re-run `directory-indexer reset`.');\n }\n throw error;\n }\n\n // Reset SQLite database\n try {\n if (options.verbose) {\n console.log(` ✓ Deleting SQLite database: ${config.storage.sqlitePath}`);\n }\n const sqliteDeleted = await clearDatabase(config);\n if (options.verbose && sqliteDeleted) {\n console.log(` ✓ SQLite database cleared`);\n }\n } catch (error) {\n const message = error instanceof StorageError ? error.message : `Failed to clear database: ${error}`;\n console.log('\\nReset incomplete:');\n console.log(` ✓ Vector data deleted from Qdrant collection: ${config.storage.qdrantCollection}`);\n console.log(` ⚠ SQLite cleanup failed: ${message}`);\n console.log('\\nVector data is gone but SQLite metadata remains.');\n console.log('Fix the issue, then re-run `directory-indexer reset` to finish the reset.');\n throw error;\n }\n\n console.log('\\nReset complete. Directory-indexer is ready for fresh indexing.');\n}"],"names":[],"mappings":";;AASA,eAAsB,iBAAiB,QAAgB,UAAwB,IAAmB;AAChG,QAAM,QAAQ,MAAM,gBAAgB,MAAM;AAE1C,MAAI,CAAC,QAAQ,OAAO;AAClB,UAAM,iBAAiB,OAAO,MAAM;AAAA,EACtC;AAEA,QAAM,aAAa,QAAQ,OAAO;AACpC;AAGA,eAAe,iBAAiB,OAAmB,QAA+B;AAChF,UAAQ,IAAI,uDAAuD;AAEnE,MAAI,MAAM,cAAc;AACtB,YAAQ,IAAI,wBAAwB,OAAO,QAAQ,UAAU,KAAK,MAAM,UAAU,GAAG;AAAA,EACvF,OAAO;AACL,YAAQ,IAAI,wBAAwB,OAAO,QAAQ,UAAU,cAAc;AAAA,EAC7E;AAEA,MAAI,MAAM,wBAAwB;AAChC,UAAM,aAAa,MAAM,sBAAsB,IAAI,WAAW;AAC9D,YAAQ,IAAI,0BAA0B,OAAO,QAAQ,gBAAgB,KAAK,MAAM,iBAAiB,IAAI,UAAU,GAAG;AAAA,EACpH,OAAO;AACL,YAAQ,IAAI,0BAA0B,OAAO,QAAQ,gBAAgB,cAAc;AAAA,EACrF;AAEA,MAAI,OAAO,QAAQ,mBAAmB,yBAAyB;AAC7D,YAAQ,IAAI,wBAAwB,OAAO,QAAQ,cAAc,EAAE;AAAA,EACrE;AAEA,UAAQ,IAAI,4CAA4C;AAExD,QAAM,SAAS,MAAM,aAAa,qBAAqB;AACvD,MAAI,CAAC,UAAU,CAAC,CAAC,KAAK,KAAK,EAAE,SAAS,OAAO,cAAc,KAAA,CAAM,GAAG;AAClE,UAAM,IAAI,MAAM,yBAAyB;AAAA,EAC3C;AACF;AAEA,eAAe,aAAa,QAAgB,SAAsC;AAChF,MAAI,QAAQ,SAAS;AACnB,YAAQ,IAAI,uCAAuC;AAAA,EACrD;AAIA,MAAI;AACF,QAAI,QAAQ,SAAS;AACnB,cAAQ,IAAI,mCAAmC,OAAO,QAAQ,gBAAgB,EAAE;AAAA,IAClF;AACA,UAAM,gBAAgB,MAAM,sBAAsB,MAAM;AACxD,QAAI,QAAQ,WAAW,eAAe;AACpC,cAAQ,IAAI,+BAA+B;AAAA,IAC7C;AAAA,EACF,SAAS,OAAO;AACd,QAAI,QAAQ,SAAS;AACnB,YAAM,UAAU,iBAAiB,eAAe,MAAM,UAAU,+BAA+B,KAAK;AACpG,cAAQ,IAAI,OAAO,OAAO,EAAE;AAC5B,cAAQ,IAAI,uFAAuF;AAAA,IACrG;AACA,UAAM;AAAA,EACR;AAGA,MAAI;AACF,QAAI,QAAQ,SAAS;AACnB,cAAQ,IAAI,iCAAiC,OAAO,QAAQ,UAAU,EAAE;AAAA,IAC1E;AACA,UAAM,gBAAgB,MAAM,cAAc,MAAM;AAChD,QAAI,QAAQ,WAAW,eAAe;AACpC,cAAQ,IAAI,6BAA6B;AAAA,IAC3C;AAAA,EACF,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,eAAe,MAAM,UAAU,6BAA6B,KAAK;AAClG,YAAQ,IAAI,qBAAqB;AACjC,YAAQ,IAAI,mDAAmD,OAAO,QAAQ,gBAAgB,EAAE;AAChG,YAAQ,IAAI,8BAA8B,OAAO,EAAE;AACnD,YAAQ,IAAI,oDAAoD;AAChE,YAAQ,IAAI,2EAA2E;AACvF,UAAM;AAAA,EACR;AAEA,UAAQ,IAAI,kEAAkE;AAChF;"}
package/dist/search.js CHANGED
@@ -9,6 +9,7 @@ class SearchError extends Error {
9
9
  this.name = "SearchError";
10
10
  }
11
11
  }
12
+ const MAX_EMBED_INPUT_CHARS = 24e3;
12
13
  function buildWorkspaceFilter(workspacePaths) {
13
14
  return {
14
15
  must: [
@@ -28,7 +29,8 @@ async function searchContent(query, options = {}) {
28
29
  const queryEmbedding = await generateEmbedding(query, config);
29
30
  const workspacePaths = workspace ? getWorkspacePaths(config, workspace) : [];
30
31
  const filter = workspace && workspacePaths.length > 0 ? buildWorkspaceFilter(workspacePaths) : void 0;
31
- const points = await qdrant.searchPoints(queryEmbedding, limit, filter);
32
+ const fetchLimit = threshold > 0 ? Math.max(limit * 5, 100) : limit;
33
+ const points = await qdrant.searchPoints(queryEmbedding, fetchLimit, filter);
32
34
  const fileGroups = /* @__PURE__ */ new Map();
33
35
  for (const point of points) {
34
36
  const score = point.score ?? 0;
@@ -81,7 +83,7 @@ async function findSimilarFiles(filePath, limit = 5, workspace) {
81
83
  embedding = await generateEmbedding(fileRecord.chunks[0].content, config);
82
84
  } else {
83
85
  const content = await promises.readFile(filePath, "utf-8");
84
- embedding = await generateEmbedding(content, config);
86
+ embedding = await generateEmbedding(content.slice(0, MAX_EMBED_INPUT_CHARS), config);
85
87
  }
86
88
  const points = await qdrant.searchPoints(embedding, limit + 1, filter);
87
89
  const filteredPoints = points.filter((point) => point.payload.filePath !== filePath).slice(0, limit);
@@ -121,6 +123,9 @@ async function getChunkContent(filePath, chunkId) {
121
123
  }
122
124
  }
123
125
  async function getFileContent(filePath, chunks) {
126
+ if (chunks) {
127
+ parseChunkRange(chunks);
128
+ }
124
129
  try {
125
130
  if (!await fileExists(filePath)) {
126
131
  throw new Error(`File not found: ${filePath}`);
@@ -147,9 +152,15 @@ async function getFileContent(filePath, chunks) {
147
152
  function parseChunkRange(chunks) {
148
153
  if (chunks.includes("-")) {
149
154
  const [start, end] = chunks.split("-").map((num2) => parseInt(num2.trim()));
150
- return { start: start || 0, end: end || start || 0 };
155
+ if (Number.isNaN(start) || Number.isNaN(end) || start > end) {
156
+ throw new Error(`Invalid chunk range: '${chunks}'`);
157
+ }
158
+ return { start, end };
151
159
  }
152
160
  const num = parseInt(chunks);
161
+ if (Number.isNaN(num)) {
162
+ throw new Error(`Invalid chunk range: '${chunks}'`);
163
+ }
153
164
  return { start: num, end: num };
154
165
  }
155
166
  export {
@@ -1 +1 @@
1
- {"version":3,"file":"search.js","sources":["../src/search.ts"],"sourcesContent":["import { promises as fs } from 'fs';\nimport { generateEmbedding } from './embedding.js';\nimport { initializeStorage } from './storage.js';\nimport { fileExists } from './utils.js';\n\nexport interface SearchOptions {\n limit?: number;\n threshold?: number;\n workspace?: string;\n}\n\nexport interface ChunkMatch {\n chunkId: string;\n score: number;\n}\n\nexport interface SearchResult {\n filePath: string;\n score: number;\n fileSizeBytes: number;\n matchingChunks: number;\n chunks: ChunkMatch[];\n}\n\nexport interface SimilarFile {\n filePath: string;\n score: number;\n fileSizeBytes: number;\n}\n\nexport class SearchError extends Error {\n constructor(message: string, public override cause?: Error) {\n super(message);\n this.name = 'SearchError';\n }\n}\n\n\nfunction buildWorkspaceFilter(workspacePaths: string[]): Record<string, unknown> {\n return {\n must: [\n {\n key: \"parentDirectories\",\n match: { any: workspacePaths }\n }\n ]\n };\n}\n\nexport async function searchContent(query: string, options: SearchOptions = {}): Promise<SearchResult[]> {\n const { limit = 10, threshold = 0.0, workspace } = options;\n \n try {\n const config = (await import('./config.js')).loadConfig();\n const { getWorkspacePaths } = await import('./config.js');\n const { sqlite, qdrant } = await initializeStorage(config);\n \n const queryEmbedding = await generateEmbedding(query, config);\n \n // Get workspace paths if workspace is specified\n const workspacePaths = workspace ? getWorkspacePaths(config, workspace) : [];\n \n // Use Qdrant filtering for workspace searches\n const filter = workspace && workspacePaths.length > 0 ? buildWorkspaceFilter(workspacePaths) : undefined;\n const points = await qdrant.searchPoints(queryEmbedding, limit, filter);\n \n // Group points by file path\n const fileGroups = new Map<string, Array<{ score: number; chunkId: string }>>();\n \n for (const point of points) {\n const score = point.score ?? 0;\n if (score < threshold) continue;\n \n const filePath = point.payload.filePath;\n \n if (!fileGroups.has(filePath)) {\n fileGroups.set(filePath, []);\n }\n \n fileGroups.get(filePath)!.push({\n score,\n chunkId: point.payload.chunkId\n });\n }\n \n // Calculate average score per file and sort\n const results: SearchResult[] = [];\n for (const [filePath, chunks] of fileGroups.entries()) {\n const avgScore = chunks.reduce((sum, chunk) => sum + chunk.score, 0) / chunks.length;\n \n // Sort chunks by score (best first) and create chunk matches\n const sortedChunks = chunks.sort((a, b) => b.score - a.score);\n const chunkMatches: ChunkMatch[] = sortedChunks.map(chunk => ({\n chunkId: chunk.chunkId,\n score: chunk.score\n }));\n \n // Get file size from SQLite database\n const fileRecord = await sqlite.getFile(filePath);\n const fileSizeBytes = fileRecord?.size ?? 0;\n \n results.push({\n filePath,\n score: avgScore,\n fileSizeBytes,\n matchingChunks: chunks.length,\n chunks: chunkMatches\n });\n }\n \n // Sort by average score and return top results\n return results\n .sort((a, b) => b.score - a.score)\n .slice(0, limit);\n } catch (error) {\n throw new SearchError(`Failed to search content`, error as Error);\n }\n}\n\nexport async function findSimilarFiles(filePath: string, limit: number = 5, workspace?: string): Promise<SimilarFile[]> {\n try {\n if (!await fileExists(filePath)) {\n throw new Error(`File not found: ${filePath}`);\n }\n \n const config = (await import('./config.js')).loadConfig();\n const { getWorkspacePaths } = await import('./config.js');\n const { sqlite, qdrant } = await initializeStorage(config);\n \n // Get workspace paths if workspace is specified\n const workspacePaths = workspace ? getWorkspacePaths(config, workspace) : [];\n const filter = workspace && workspacePaths.length > 0 ? buildWorkspaceFilter(workspacePaths) : undefined;\n \n // Get embedding from file record or read file directly\n let embedding: number[];\n const fileRecord = await sqlite.getFile(filePath);\n if (fileRecord && fileRecord.chunks.length > 0) {\n embedding = await generateEmbedding(fileRecord.chunks[0].content, config);\n } else {\n const content = await fs.readFile(filePath, 'utf-8');\n embedding = await generateEmbedding(content, config);\n }\n \n const points = await qdrant.searchPoints(embedding, limit + 1, filter);\n \n const filteredPoints = points\n .filter(point => point.payload.filePath !== filePath)\n .slice(0, limit);\n \n const results: SimilarFile[] = [];\n for (const point of filteredPoints) {\n const pointFileRecord = await sqlite.getFile(point.payload.filePath);\n const fileSizeBytes = pointFileRecord?.size ?? 0;\n \n results.push({\n filePath: point.payload.filePath,\n score: point.score ?? 0,\n fileSizeBytes\n });\n }\n \n return results;\n } catch (error) {\n throw new SearchError(`Failed to find similar files`, error as Error);\n }\n}\n\nexport async function getChunkContent(filePath: string, chunkId: string): Promise<string> {\n try {\n if (!await fileExists(filePath)) {\n throw new Error(`File not found: ${filePath}`);\n }\n \n const config = (await import('./config.js')).loadConfig();\n const { sqlite } = await initializeStorage(config);\n \n const fileRecord = await sqlite.getFile(filePath);\n if (!fileRecord || fileRecord.chunks.length === 0) {\n throw new Error(`File not indexed: ${filePath}`);\n }\n \n const chunk = fileRecord.chunks.find(c => c.id === chunkId);\n if (!chunk) {\n throw new Error(`Chunk ${chunkId} not found in file: ${filePath}`);\n }\n \n return chunk.content;\n } catch (error) {\n throw new SearchError(`Failed to get chunk content`, error as Error);\n }\n}\n\nexport async function getFileContent(filePath: string, chunks?: string): Promise<string> {\n try {\n if (!await fileExists(filePath)) {\n throw new Error(`File not found: ${filePath}`);\n }\n \n const config = (await import('./config.js')).loadConfig();\n const { sqlite } = await initializeStorage(config);\n \n const fileRecord = await sqlite.getFile(filePath);\n \n if (!chunks) {\n return await fs.readFile(filePath, 'utf-8');\n }\n \n if (!fileRecord || fileRecord.chunks.length === 0) {\n return await fs.readFile(filePath, 'utf-8');\n }\n \n const chunkRange = parseChunkRange(chunks);\n const selectedChunks = fileRecord.chunks.filter(chunk => {\n const chunkNum = parseInt(chunk.id);\n return chunkNum >= chunkRange.start && chunkNum <= chunkRange.end;\n });\n \n return selectedChunks.map(chunk => chunk.content).join('');\n } catch (error) {\n throw new SearchError(`Failed to get file content`, error as Error);\n }\n}\n\nfunction parseChunkRange(chunks: string): { start: number; end: number } {\n if (chunks.includes('-')) {\n const [start, end] = chunks.split('-').map(num => parseInt(num.trim()));\n return { start: start || 0, end: end || start || 0 };\n }\n \n const num = parseInt(chunks);\n return { start: num, end: num };\n}"],"names":["fs","num"],"mappings":"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"}
1
+ {"version":3,"file":"search.js","sources":["../src/search.ts"],"sourcesContent":["import { promises as fs } from 'fs';\nimport { generateEmbedding } from './embedding.js';\nimport { initializeStorage } from './storage.js';\nimport { fileExists } from './utils.js';\n\nexport interface SearchOptions {\n limit?: number;\n threshold?: number;\n workspace?: string;\n}\n\nexport interface ChunkMatch {\n chunkId: string;\n score: number;\n}\n\nexport interface SearchResult {\n filePath: string;\n score: number;\n fileSizeBytes: number;\n matchingChunks: number;\n chunks: ChunkMatch[];\n}\n\nexport interface SimilarFile {\n filePath: string;\n score: number;\n fileSizeBytes: number;\n}\n\nexport class SearchError extends Error {\n constructor(message: string, public override cause?: Error) {\n super(message);\n this.name = 'SearchError';\n }\n}\n\n// Caps ad-hoc embedding input below the ~8k-token context of common\n// embedding models; oversized files would otherwise fail at the provider\nconst MAX_EMBED_INPUT_CHARS = 24000;\n\n\nfunction buildWorkspaceFilter(workspacePaths: string[]): Record<string, unknown> {\n return {\n must: [\n {\n key: \"parentDirectories\",\n match: { any: workspacePaths }\n }\n ]\n };\n}\n\nexport async function searchContent(query: string, options: SearchOptions = {}): Promise<SearchResult[]> {\n const { limit = 10, threshold = 0.0, workspace } = options;\n \n try {\n const config = (await import('./config.js')).loadConfig();\n const { getWorkspacePaths } = await import('./config.js');\n const { sqlite, qdrant } = await initializeStorage(config);\n \n const queryEmbedding = await generateEmbedding(query, config);\n \n // Get workspace paths if workspace is specified\n const workspacePaths = workspace ? getWorkspacePaths(config, workspace) : [];\n \n // Use Qdrant filtering for workspace searches\n const filter = workspace && workspacePaths.length > 0 ? buildWorkspaceFilter(workspacePaths) : undefined;\n const fetchLimit = threshold > 0 ? Math.max(limit * 5, 100) : limit;\n const points = await qdrant.searchPoints(queryEmbedding, fetchLimit, filter);\n \n // Group points by file path\n const fileGroups = new Map<string, Array<{ score: number; chunkId: string }>>();\n \n for (const point of points) {\n const score = point.score ?? 0;\n if (score < threshold) continue;\n \n const filePath = point.payload.filePath;\n \n if (!fileGroups.has(filePath)) {\n fileGroups.set(filePath, []);\n }\n \n fileGroups.get(filePath)!.push({\n score,\n chunkId: point.payload.chunkId\n });\n }\n \n // Calculate average score per file and sort\n const results: SearchResult[] = [];\n for (const [filePath, chunks] of fileGroups.entries()) {\n const avgScore = chunks.reduce((sum, chunk) => sum + chunk.score, 0) / chunks.length;\n \n // Sort chunks by score (best first) and create chunk matches\n const sortedChunks = chunks.sort((a, b) => b.score - a.score);\n const chunkMatches: ChunkMatch[] = sortedChunks.map(chunk => ({\n chunkId: chunk.chunkId,\n score: chunk.score\n }));\n \n // Get file size from SQLite database\n const fileRecord = await sqlite.getFile(filePath);\n const fileSizeBytes = fileRecord?.size ?? 0;\n \n results.push({\n filePath,\n score: avgScore,\n fileSizeBytes,\n matchingChunks: chunks.length,\n chunks: chunkMatches\n });\n }\n \n // Sort by average score and return top results\n return results\n .sort((a, b) => b.score - a.score)\n .slice(0, limit);\n } catch (error) {\n throw new SearchError(`Failed to search content`, error as Error);\n }\n}\n\nexport async function findSimilarFiles(filePath: string, limit: number = 5, workspace?: string): Promise<SimilarFile[]> {\n try {\n if (!await fileExists(filePath)) {\n throw new Error(`File not found: ${filePath}`);\n }\n \n const config = (await import('./config.js')).loadConfig();\n const { getWorkspacePaths } = await import('./config.js');\n const { sqlite, qdrant } = await initializeStorage(config);\n \n // Get workspace paths if workspace is specified\n const workspacePaths = workspace ? getWorkspacePaths(config, workspace) : [];\n const filter = workspace && workspacePaths.length > 0 ? buildWorkspaceFilter(workspacePaths) : undefined;\n \n // Get embedding from file record or read file directly\n let embedding: number[];\n const fileRecord = await sqlite.getFile(filePath);\n if (fileRecord && fileRecord.chunks.length > 0) {\n embedding = await generateEmbedding(fileRecord.chunks[0].content, config);\n } else {\n const content = await fs.readFile(filePath, 'utf-8');\n embedding = await generateEmbedding(content.slice(0, MAX_EMBED_INPUT_CHARS), config);\n }\n \n const points = await qdrant.searchPoints(embedding, limit + 1, filter);\n \n const filteredPoints = points\n .filter(point => point.payload.filePath !== filePath)\n .slice(0, limit);\n \n const results: SimilarFile[] = [];\n for (const point of filteredPoints) {\n const pointFileRecord = await sqlite.getFile(point.payload.filePath);\n const fileSizeBytes = pointFileRecord?.size ?? 0;\n \n results.push({\n filePath: point.payload.filePath,\n score: point.score ?? 0,\n fileSizeBytes\n });\n }\n \n return results;\n } catch (error) {\n throw new SearchError(`Failed to find similar files`, error as Error);\n }\n}\n\nexport async function getChunkContent(filePath: string, chunkId: string): Promise<string> {\n try {\n if (!await fileExists(filePath)) {\n throw new Error(`File not found: ${filePath}`);\n }\n \n const config = (await import('./config.js')).loadConfig();\n const { sqlite } = await initializeStorage(config);\n \n const fileRecord = await sqlite.getFile(filePath);\n if (!fileRecord || fileRecord.chunks.length === 0) {\n throw new Error(`File not indexed: ${filePath}`);\n }\n \n const chunk = fileRecord.chunks.find(c => c.id === chunkId);\n if (!chunk) {\n throw new Error(`Chunk ${chunkId} not found in file: ${filePath}`);\n }\n \n return chunk.content;\n } catch (error) {\n throw new SearchError(`Failed to get chunk content`, error as Error);\n }\n}\n\nexport async function getFileContent(filePath: string, chunks?: string): Promise<string> {\n if (chunks) {\n parseChunkRange(chunks);\n }\n\n try {\n if (!await fileExists(filePath)) {\n throw new Error(`File not found: ${filePath}`);\n }\n \n const config = (await import('./config.js')).loadConfig();\n const { sqlite } = await initializeStorage(config);\n \n const fileRecord = await sqlite.getFile(filePath);\n \n if (!chunks) {\n return await fs.readFile(filePath, 'utf-8');\n }\n \n if (!fileRecord || fileRecord.chunks.length === 0) {\n return await fs.readFile(filePath, 'utf-8');\n }\n \n const chunkRange = parseChunkRange(chunks);\n const selectedChunks = fileRecord.chunks.filter(chunk => {\n const chunkNum = parseInt(chunk.id);\n return chunkNum >= chunkRange.start && chunkNum <= chunkRange.end;\n });\n \n return selectedChunks.map(chunk => chunk.content).join('');\n } catch (error) {\n throw new SearchError(`Failed to get file content`, error as Error);\n }\n}\n\nfunction parseChunkRange(chunks: string): { start: number; end: number } {\n if (chunks.includes('-')) {\n const [start, end] = chunks.split('-').map(num => parseInt(num.trim()));\n if (Number.isNaN(start) || Number.isNaN(end) || start > end) {\n throw new Error(`Invalid chunk range: '${chunks}'`);\n }\n return { start, end };\n }\n\n const num = parseInt(chunks);\n if (Number.isNaN(num)) {\n throw new Error(`Invalid chunk range: '${chunks}'`);\n }\n return { start: num, end: num };\n}"],"names":["fs","num"],"mappings":"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"}