directory-indexer 0.2.2 → 1.0.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/README.md +1 -1
- package/dist/cli-handlers.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.js +4 -4
- package/dist/config.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 +48 -10
- package/dist/storage.js.map +1 -1
- package/dist/utils.js +2 -0
- package/dist/utils.js.map +1 -1
- package/package.json +18 -20
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Self-hosted semantic search for local files. Give your AI assistant access to se
|
|
|
14
14
|
**Prerequisites:**
|
|
15
15
|
|
|
16
16
|
- **[Docker](https://docs.docker.com/get-docker/)** - For running Qdrant and Ollama _(skip if you already have them running natively)_
|
|
17
|
-
- **[Node.js 18+](https://nodejs.org/en/download/)** - Required for running directory-indexer
|
|
17
|
+
- **[Node.js 18+](https://nodejs.org/en/download/)** - Required for running directory-indexer (the v1.x line targets Node 18 LTS compatibility)
|
|
18
18
|
|
|
19
19
|
_Note: For native Qdrant and Ollama installation without Docker, see [Setup section](#setup)._
|
|
20
20
|
|
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/config.js
CHANGED
|
@@ -12,14 +12,14 @@ const WorkspaceSchema = z.object({
|
|
|
12
12
|
const ConfigSchema = z.object({
|
|
13
13
|
storage: z.object({
|
|
14
14
|
sqlitePath: z.string(),
|
|
15
|
-
qdrantEndpoint: z.
|
|
15
|
+
qdrantEndpoint: z.url(),
|
|
16
16
|
qdrantCollection: z.string(),
|
|
17
17
|
qdrantApiKey: z.string().optional()
|
|
18
18
|
}),
|
|
19
19
|
embedding: z.object({
|
|
20
20
|
provider: z.enum(["ollama", "openai", "mock"]),
|
|
21
21
|
model: z.string(),
|
|
22
|
-
endpoint: z.
|
|
22
|
+
endpoint: z.url()
|
|
23
23
|
}),
|
|
24
24
|
indexing: z.object({
|
|
25
25
|
chunkSize: z.number().positive(),
|
|
@@ -30,7 +30,7 @@ const ConfigSchema = z.object({
|
|
|
30
30
|
}),
|
|
31
31
|
dataDir: z.string(),
|
|
32
32
|
verbose: z.boolean(),
|
|
33
|
-
workspaces: z.record(WorkspaceSchema)
|
|
33
|
+
workspaces: z.record(z.string(), WorkspaceSchema)
|
|
34
34
|
});
|
|
35
35
|
class ConfigError extends Error {
|
|
36
36
|
constructor(message, cause) {
|
|
@@ -109,7 +109,7 @@ function loadConfig(options = {}) {
|
|
|
109
109
|
return ConfigSchema.parse(config);
|
|
110
110
|
} catch (error) {
|
|
111
111
|
if (error instanceof z.ZodError) {
|
|
112
|
-
const messages = error.
|
|
112
|
+
const messages = error.issues.map((e) => `${e.path.join(".")}: ${e.message}`);
|
|
113
113
|
throw new ConfigError(`Configuration validation failed: ${messages.join(", ")}`, error);
|
|
114
114
|
}
|
|
115
115
|
throw new ConfigError("Failed to load configuration", error);
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sources":["../src/config.ts"],"sourcesContent":["import { homedir } from 'os';\nimport { join } from 'path';\nimport { existsSync, statSync } from 'fs';\nimport { z } from 'zod';\nimport { normalizePath } from './utils';\n\nconst WorkspaceSchema = z.object({\n paths: z.array(z.string()),\n isValid: z.boolean(),\n filesCount: z.number().optional(),\n chunksCount: z.number().optional(),\n});\n\nconst ConfigSchema = z.object({\n storage: z.object({\n sqlitePath: z.string(),\n qdrantEndpoint: z.
|
|
1
|
+
{"version":3,"file":"config.js","sources":["../src/config.ts"],"sourcesContent":["import { homedir } from 'os';\nimport { join } from 'path';\nimport { existsSync, statSync } from 'fs';\nimport { z } from 'zod';\nimport { normalizePath } from './utils';\n\nconst WorkspaceSchema = z.object({\n paths: z.array(z.string()),\n isValid: z.boolean(),\n filesCount: z.number().optional(),\n chunksCount: z.number().optional(),\n});\n\nconst ConfigSchema = z.object({\n storage: z.object({\n sqlitePath: z.string(),\n qdrantEndpoint: z.url(),\n qdrantCollection: z.string(),\n qdrantApiKey: z.string().optional(),\n }),\n embedding: z.object({\n provider: z.enum(['ollama', 'openai', 'mock']),\n model: z.string(),\n endpoint: z.url(),\n }),\n indexing: z.object({\n chunkSize: z.number().positive(),\n chunkOverlap: z.number().nonnegative(),\n maxFileSize: z.number().positive(),\n ignorePatterns: z.array(z.string()),\n respectGitignore: z.boolean(),\n }),\n dataDir: z.string(),\n verbose: z.boolean(),\n workspaces: z.record(z.string(), WorkspaceSchema),\n});\n\nexport type Config = z.infer<typeof ConfigSchema>;\nexport type WorkspaceConfig = z.infer<typeof WorkspaceSchema>;\n\nexport class ConfigError extends Error {\n constructor(message: string, public override cause?: Error) {\n super(message);\n this.name = 'ConfigError';\n }\n}\n\nfunction parseWorkspaces(env: Record<string, string | undefined>): Record<string, WorkspaceConfig> {\n const workspaces: Record<string, WorkspaceConfig> = {};\n \n for (const [key, value] of Object.entries(env)) {\n if (key.startsWith('WORKSPACE_') && value) {\n const name = key.replace('WORKSPACE_', '').toLowerCase();\n \n // Parse paths from comma-separated string or JSON array\n let paths: string[];\n try {\n // Try parsing as JSON array first\n paths = JSON.parse(value);\n if (!Array.isArray(paths)) {\n throw new Error('Not an array');\n }\n } catch {\n // Fall back to comma-separated string\n paths = value.split(',').map(p => p.trim()).filter(p => p.length > 0);\n }\n \n // Normalize paths for consistent comparison\n const normalizedPaths = paths.map(normalizePath);\n \n // Validate that paths exist and are directories\n const isValid = normalizedPaths.every(path => {\n try {\n return existsSync(path) && statSync(path).isDirectory();\n } catch {\n return false;\n }\n });\n \n workspaces[name] = {\n paths: normalizedPaths,\n isValid,\n };\n }\n }\n \n return workspaces;\n}\n\nexport function getWorkspacePaths(config: Config, workspace: string): string[] {\n const workspaceConfig = config.workspaces[workspace];\n return workspaceConfig?.paths || [];\n}\n\nexport function getAvailableWorkspaces(config: Config): string[] {\n return Object.keys(config.workspaces);\n}\n\nexport function loadConfig(options: { verbose?: boolean } = {}): Config {\n const dataDir = process.env.DIRECTORY_INDEXER_DATA_DIR || join(homedir(), '.directory-indexer');\n \n // Use separate database file and collection for tests to avoid contaminating main data\n const isTest = process.env.NODE_ENV === 'test' || process.env.VITEST === 'true';\n const dbFileName = isTest ? 'test-data.db' : 'data.db';\n const defaultCollection = isTest ? 'directory-indexer-test' : 'directory-indexer';\n \n // Parse workspace configurations from environment variables\n const workspaces = parseWorkspaces(process.env);\n \n const config = {\n storage: {\n sqlitePath: join(dataDir, dbFileName),\n qdrantEndpoint: process.env.QDRANT_ENDPOINT || 'http://127.0.0.1:6333',\n qdrantCollection: process.env.DIRECTORY_INDEXER_QDRANT_COLLECTION || defaultCollection,\n qdrantApiKey: process.env.QDRANT_API_KEY,\n },\n embedding: {\n provider: (process.env.EMBEDDING_PROVIDER as Config['embedding']['provider']) || 'ollama',\n model: process.env.EMBEDDING_MODEL || 'nomic-embed-text',\n endpoint: process.env.OLLAMA_ENDPOINT || 'http://127.0.0.1:11434',\n },\n indexing: {\n chunkSize: parseInt(process.env.CHUNK_SIZE || '512'),\n chunkOverlap: parseInt(process.env.CHUNK_OVERLAP || '50'),\n maxFileSize: parseInt(process.env.MAX_FILE_SIZE || '10485760'),\n ignorePatterns: ['.git', 'node_modules', 'target', '.DS_Store'],\n respectGitignore: process.env.RESPECT_GITIGNORE !== 'false',\n },\n dataDir,\n verbose: options.verbose ?? (process.env.VERBOSE === 'true'),\n workspaces,\n };\n\n try {\n return ConfigSchema.parse(config);\n } catch (error) {\n if (error instanceof z.ZodError) {\n const messages = error.issues.map(e => `${e.path.join('.')}: ${e.message}`);\n throw new ConfigError(`Configuration validation failed: ${messages.join(', ')}`, error);\n }\n throw new ConfigError('Failed to load configuration', error as Error);\n }\n}"],"names":[],"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
|