edhindex 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/.edhindexignore +66 -0
- package/BUILD_BRIEF.md +228 -0
- package/package.json +38 -0
- package/src/cli/commands/config_cmd.ts +58 -0
- package/src/cli/commands/doctor_cmd.ts +31 -0
- package/src/cli/commands/index_cmd.ts +121 -0
- package/src/cli/commands/init.ts +25 -0
- package/src/cli/commands/models.ts +19 -0
- package/src/cli/commands/search.ts +44 -0
- package/src/cli/commands/start.ts +253 -0
- package/src/cli/commands/status.ts +54 -0
- package/src/cli/index.ts +99 -0
- package/src/config/index.ts +75 -0
- package/src/doctor/index.ts +131 -0
- package/src/embeddings/provider.ts +9 -0
- package/src/embeddings/transformers.ts +130 -0
- package/src/indexer/chunker.ts +198 -0
- package/src/indexer/file-utils.ts +139 -0
- package/src/indexer/ignore.ts +100 -0
- package/src/indexer/incremental.ts +80 -0
- package/src/indexer/indexer.ts +181 -0
- package/src/indexer/parser.ts +267 -0
- package/src/logging.ts +44 -0
- package/src/mcp/server.ts +126 -0
- package/src/progress.ts +28 -0
- package/src/retrieval/bm25.ts +22 -0
- package/src/retrieval/reranker.ts +64 -0
- package/src/retrieval/search.ts +144 -0
- package/src/security.ts +46 -0
- package/src/storage/fts.ts +130 -0
- package/src/storage/metadata.ts +162 -0
- package/src/vector/lancedb.ts +95 -0
- package/src/vector/store.ts +8 -0
- package/src/version.ts +55 -0
- package/src/watcher/index.ts +77 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { loadConfig, MODEL_CONFIGS } from '../../config/index.js';
|
|
4
|
+
import { logger, setLogLevel, LogLevel, getLogLevel } from '../../logging.js';
|
|
5
|
+
import { emitProgress, onProgress } from '../../progress.js';
|
|
6
|
+
import { loadVersion, saveVersion, getExpectedVersion, needsRebuild } from '../../version.js';
|
|
7
|
+
import { MetadataStore } from '../../storage/metadata.js';
|
|
8
|
+
import { FTSStore } from '../../storage/fts.js';
|
|
9
|
+
import { TransformersEmbeddingProvider, TransformersReranker } from '../../embeddings/transformers.js';
|
|
10
|
+
import { LanceDBStore } from '../../vector/lancedb.js';
|
|
11
|
+
import { Indexer } from '../../indexer/indexer.js';
|
|
12
|
+
import { SearchEngine } from '../../retrieval/search.js';
|
|
13
|
+
import { createMCPServer } from '../../mcp/server.js';
|
|
14
|
+
import { FileWatcher } from '../../watcher/index.js';
|
|
15
|
+
import cliProgress from 'cli-progress';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
|
|
18
|
+
export async function startCommand(rootPath: string) {
|
|
19
|
+
const indexDir = join(rootPath, '.edhindex');
|
|
20
|
+
if (!existsSync(indexDir)) {
|
|
21
|
+
mkdirSync(indexDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const config = loadConfig(indexDir);
|
|
25
|
+
const modelCfg = MODEL_CONFIGS[config.model];
|
|
26
|
+
|
|
27
|
+
const logLevelMap: Record<string, LogLevel> = {
|
|
28
|
+
silent: LogLevel.Silent,
|
|
29
|
+
info: LogLevel.Info,
|
|
30
|
+
verbose: LogLevel.Verbose,
|
|
31
|
+
debug: LogLevel.Debug,
|
|
32
|
+
};
|
|
33
|
+
setLogLevel(logLevelMap[config.logLevel] || LogLevel.Info);
|
|
34
|
+
|
|
35
|
+
console.log(chalk.bold('EDHIndex v1.0.0'));
|
|
36
|
+
console.log(`Workspace: ${rootPath}`);
|
|
37
|
+
console.log(`Model: ${modelCfg.displayName} (${modelCfg.dimensions}d)`);
|
|
38
|
+
console.log(`Languages: ${config.languages.join(', ')}`);
|
|
39
|
+
console.log('');
|
|
40
|
+
|
|
41
|
+
// Check index version
|
|
42
|
+
const version = loadVersion(indexDir);
|
|
43
|
+
if (needsRebuild(version, config.model)) {
|
|
44
|
+
if (version) {
|
|
45
|
+
console.log(chalk.yellow('Index version mismatch — full rebuild required.'));
|
|
46
|
+
console.log(chalk.dim(` Expected model: ${config.model}, existing: ${version.embeddingModel}`));
|
|
47
|
+
} else {
|
|
48
|
+
console.log(chalk.dim('No existing index found — building from scratch.'));
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
console.log(chalk.dim('Existing index is compatible — incremental update.'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Setup progress bar
|
|
55
|
+
let progressBar: cliProgress.SingleBar | null = null;
|
|
56
|
+
const unsub = onProgress((event) => {
|
|
57
|
+
if (progressBar) {
|
|
58
|
+
if (event.phase === 'scanning' && event.total) {
|
|
59
|
+
progressBar.setTotal(event.total);
|
|
60
|
+
progressBar.update(event.current || 0);
|
|
61
|
+
} else if (event.phase === 'embedding' && event.percent !== undefined) {
|
|
62
|
+
progressBar.setTotal(100);
|
|
63
|
+
progressBar.update(event.percent);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Initialize stores
|
|
69
|
+
console.log(chalk.dim('Initializing storage...'));
|
|
70
|
+
const metadataStore = new MetadataStore(indexDir);
|
|
71
|
+
const ftsStore = new FTSStore(indexDir);
|
|
72
|
+
|
|
73
|
+
// Initialize vector store
|
|
74
|
+
const vectorStore = new LanceDBStore(indexDir, modelCfg.dimensions);
|
|
75
|
+
await vectorStore.init();
|
|
76
|
+
|
|
77
|
+
// Initialize embedding provider
|
|
78
|
+
const embedder = new TransformersEmbeddingProvider(config.model);
|
|
79
|
+
console.log(chalk.dim(`Loading embedding model (${modelCfg.displayName})...`));
|
|
80
|
+
await embedder.load();
|
|
81
|
+
|
|
82
|
+
// Initialize reranker if enabled
|
|
83
|
+
let reranker: TransformersReranker | null = null;
|
|
84
|
+
if (config.rerank) {
|
|
85
|
+
console.log(chalk.dim('Loading reranker model...'));
|
|
86
|
+
reranker = new TransformersReranker();
|
|
87
|
+
await reranker.load();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Initialize indexer
|
|
91
|
+
const indexer = new Indexer({
|
|
92
|
+
config,
|
|
93
|
+
rootPath,
|
|
94
|
+
indexDir,
|
|
95
|
+
onIndex: async (result) => {
|
|
96
|
+
for (const chunk of result.chunks) {
|
|
97
|
+
metadataStore.upsertChunk({
|
|
98
|
+
id: chunk.id,
|
|
99
|
+
file: chunk.file,
|
|
100
|
+
language: chunk.language,
|
|
101
|
+
symbol: chunk.symbol,
|
|
102
|
+
kind: chunk.kind,
|
|
103
|
+
parent: chunk.parent,
|
|
104
|
+
hash: chunk.hash,
|
|
105
|
+
git_commit: null,
|
|
106
|
+
start_line: chunk.startLine,
|
|
107
|
+
end_line: chunk.endLine,
|
|
108
|
+
imports: chunk.imports.join('\n'),
|
|
109
|
+
exports: chunk.exports.join('\n'),
|
|
110
|
+
embedding_model: embedder.name(),
|
|
111
|
+
last_indexed: new Date().toISOString(),
|
|
112
|
+
checksum: chunk.hash,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
ftsStore.insertChunk(chunk.id, chunk.content, chunk.file, chunk.symbol, chunk.language);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (result.chunks.length > 0) {
|
|
119
|
+
const texts = result.chunks.map(c => c.content);
|
|
120
|
+
const embeddings = await embedder.embed(texts);
|
|
121
|
+
const vectors = result.chunks.map((c, i) => ({
|
|
122
|
+
id: c.id,
|
|
123
|
+
values: embeddings[i],
|
|
124
|
+
metadata: {
|
|
125
|
+
file: c.file,
|
|
126
|
+
symbol: c.symbol,
|
|
127
|
+
kind: c.kind,
|
|
128
|
+
text: c.content,
|
|
129
|
+
language: c.language,
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
await vectorStore.insert(vectors);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
metadataStore.upsertFileHash(result.relativePath, result.hash);
|
|
136
|
+
},
|
|
137
|
+
onDelete: async (chunkIds) => {
|
|
138
|
+
for (const id of chunkIds) {
|
|
139
|
+
ftsStore.deleteChunk(id);
|
|
140
|
+
}
|
|
141
|
+
await vectorStore.delete(chunkIds);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await indexer.initialize();
|
|
146
|
+
|
|
147
|
+
// Determine if full or incremental
|
|
148
|
+
const existingFileHashes = metadataStore.getAllFileHashes();
|
|
149
|
+
const existingChunkHashes = metadataStore.getAllChunkHashes();
|
|
150
|
+
const doFullRebuild = needsRebuild(version, config.model) || existingFileHashes.size === 0;
|
|
151
|
+
|
|
152
|
+
// Index
|
|
153
|
+
console.log('');
|
|
154
|
+
progressBar = new cliProgress.SingleBar({
|
|
155
|
+
format: '{phase} | {bar} | {percentage}% | {value}/{total}',
|
|
156
|
+
barCompleteChar: '\u2588',
|
|
157
|
+
barIncompleteChar: '\u2591',
|
|
158
|
+
hideCursor: true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (doFullRebuild) {
|
|
162
|
+
progressBar.start(100, 0, { phase: 'Scanning' });
|
|
163
|
+
console.log(chalk.dim('Running full index...'));
|
|
164
|
+
const chunks = await indexer.runFullIndex();
|
|
165
|
+
progressBar.stop();
|
|
166
|
+
console.log(chalk.green(`Indexed ${chunks.length} chunks.`));
|
|
167
|
+
|
|
168
|
+
// Save version
|
|
169
|
+
const newVersion = getExpectedVersion(config.model);
|
|
170
|
+
saveVersion(indexDir, newVersion);
|
|
171
|
+
} else {
|
|
172
|
+
progressBar.start(100, 0, { phase: 'Scanning' });
|
|
173
|
+
console.log(chalk.dim('Running incremental index...'));
|
|
174
|
+
const result = await indexer.runIncremental(existingFileHashes, existingChunkHashes);
|
|
175
|
+
progressBar.stop();
|
|
176
|
+
console.log(chalk.green(`Indexed ${result.chunks.length} chunks, removed ${result.removed.length} files.`));
|
|
177
|
+
|
|
178
|
+
// Update version
|
|
179
|
+
const existingVersion = version!;
|
|
180
|
+
existingVersion.lastIndexed = new Date().toISOString();
|
|
181
|
+
saveVersion(indexDir, existingVersion);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
unsub();
|
|
185
|
+
|
|
186
|
+
// Build search engine
|
|
187
|
+
const searchEngine = new SearchEngine(ftsStore, metadataStore, vectorStore, embedder, reranker, config.rerank);
|
|
188
|
+
|
|
189
|
+
// Start file watcher if enabled
|
|
190
|
+
let watcher: FileWatcher | null = null;
|
|
191
|
+
if (config.watch) {
|
|
192
|
+
watcher = new FileWatcher(rootPath, async (filePath) => {
|
|
193
|
+
logger.info(`File changed: ${filePath}`);
|
|
194
|
+
const { scanFiles } = await import('../../indexer/file-utils.js');
|
|
195
|
+
const files = scanFiles(rootPath, config.languages);
|
|
196
|
+
const file = files.find(f => f.relativePath === filePath);
|
|
197
|
+
if (file) {
|
|
198
|
+
const oldChunks = metadataStore.getChunksForFile(filePath);
|
|
199
|
+
const oldIds = oldChunks.map(c => c.id);
|
|
200
|
+
if (oldIds.length > 0) {
|
|
201
|
+
for (const id of oldIds) ftsStore.deleteChunk(id);
|
|
202
|
+
await vectorStore.delete(oldIds);
|
|
203
|
+
}
|
|
204
|
+
metadataStore.deleteChunksForFile(filePath);
|
|
205
|
+
metadataStore.deleteFileHash(filePath);
|
|
206
|
+
|
|
207
|
+
const chunks = await indexer.processFile(file.path, file.relativePath, file.language);
|
|
208
|
+
for (const chunk of chunks) {
|
|
209
|
+
metadataStore.upsertChunk({
|
|
210
|
+
id: chunk.id, file: chunk.file, language: chunk.language, symbol: chunk.symbol,
|
|
211
|
+
kind: chunk.kind, parent: chunk.parent, hash: chunk.hash, git_commit: null,
|
|
212
|
+
start_line: chunk.startLine, end_line: chunk.endLine,
|
|
213
|
+
imports: chunk.imports.join('\n'), exports: chunk.exports.join('\n'),
|
|
214
|
+
embedding_model: embedder.name(), last_indexed: new Date().toISOString(), checksum: chunk.hash,
|
|
215
|
+
});
|
|
216
|
+
ftsStore.insertChunk(chunk.id, chunk.content, chunk.file, chunk.symbol, chunk.language);
|
|
217
|
+
}
|
|
218
|
+
if (chunks.length > 0) {
|
|
219
|
+
const embeddings = await embedder.embed(chunks.map(c => c.content));
|
|
220
|
+
await vectorStore.insert(chunks.map((c, i) => ({
|
|
221
|
+
id: c.id, values: embeddings[i],
|
|
222
|
+
metadata: { file: c.file, symbol: c.symbol, kind: c.kind, text: c.content, language: c.language },
|
|
223
|
+
})));
|
|
224
|
+
}
|
|
225
|
+
metadataStore.upsertFileHash(filePath, file.hash);
|
|
226
|
+
logger.info(`Re-indexed: ${filePath}`);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
await watcher.start();
|
|
230
|
+
console.log(chalk.green('File watcher started.'));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Start MCP server
|
|
234
|
+
console.log(chalk.green('Starting MCP server on stdio...'));
|
|
235
|
+
const mcpServer = createMCPServer(searchEngine, rootPath);
|
|
236
|
+
|
|
237
|
+
const transport = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
|
238
|
+
const stdioTransport = new transport.StdioServerTransport();
|
|
239
|
+
await mcpServer.connect(stdioTransport);
|
|
240
|
+
|
|
241
|
+
// Handle shutdown
|
|
242
|
+
const shutdown = async () => {
|
|
243
|
+
console.log(chalk.yellow('\nShutting down...'));
|
|
244
|
+
if (watcher) await watcher.stop();
|
|
245
|
+
vectorStore.close();
|
|
246
|
+
metadataStore.close();
|
|
247
|
+
ftsStore.close();
|
|
248
|
+
process.exit(0);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
process.on('SIGINT', shutdown);
|
|
252
|
+
process.on('SIGTERM', shutdown);
|
|
253
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { loadConfig, MODEL_CONFIGS } from '../../config/index.js';
|
|
4
|
+
import { loadVersion } from '../../version.js';
|
|
5
|
+
import { MetadataStore } from '../../storage/metadata.js';
|
|
6
|
+
import { FTSStore } from '../../storage/fts.js';
|
|
7
|
+
import { LanceDBStore } from '../../vector/lancedb.js';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
export async function statusCommand(rootPath: string) {
|
|
11
|
+
const indexDir = join(rootPath, '.edhindex');
|
|
12
|
+
|
|
13
|
+
if (!existsSync(indexDir)) {
|
|
14
|
+
console.log(chalk.yellow('No .edhindex directory found. Run "edhindex init" first.'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const config = loadConfig(indexDir);
|
|
19
|
+
const version = loadVersion(indexDir);
|
|
20
|
+
|
|
21
|
+
console.log(chalk.bold('EDHIndex Status'));
|
|
22
|
+
console.log('');
|
|
23
|
+
|
|
24
|
+
if (version) {
|
|
25
|
+
console.log(` Index: ${chalk.green('Present')}`);
|
|
26
|
+
console.log(` Schema: v${version.schema}`);
|
|
27
|
+
console.log(` Model tier: ${version.embeddingModel}`);
|
|
28
|
+
console.log(` Created: ${version.createdAt}`);
|
|
29
|
+
console.log(` Last indexed: ${version.lastIndexed}`);
|
|
30
|
+
} else {
|
|
31
|
+
console.log(` Index: ${chalk.yellow('No version file')}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(` Config: ${chalk.cyan(JSON.stringify(config))}`);
|
|
35
|
+
|
|
36
|
+
const metadataStore = new MetadataStore(indexDir);
|
|
37
|
+
const chunkCount = metadataStore.getChunkCount();
|
|
38
|
+
const fileCount = metadataStore.getFileCount();
|
|
39
|
+
metadataStore.close();
|
|
40
|
+
|
|
41
|
+
console.log(` Chunks: ${chunkCount}`);
|
|
42
|
+
console.log(` Files: ${fileCount}`);
|
|
43
|
+
console.log(` Languages: ${config.languages.join(', ')}`);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const vectorStore = new LanceDBStore(indexDir, MODEL_CONFIGS[config.model].dimensions);
|
|
47
|
+
await vectorStore.init();
|
|
48
|
+
const vecCount = await vectorStore.count();
|
|
49
|
+
console.log(` Vectors: ${vecCount}`);
|
|
50
|
+
await vectorStore.close();
|
|
51
|
+
} catch {
|
|
52
|
+
console.log(` Vectors: ${chalk.yellow('unavailable')}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { initCommand } from './commands/init.js';
|
|
5
|
+
import { startCommand } from './commands/start.js';
|
|
6
|
+
import { indexCommand } from './commands/index_cmd.js';
|
|
7
|
+
import { searchCommand } from './commands/search.js';
|
|
8
|
+
import { statusCommand } from './commands/status.js';
|
|
9
|
+
import { configCommand } from './commands/config_cmd.js';
|
|
10
|
+
import { modelsCommand } from './commands/models.js';
|
|
11
|
+
import { doctorCommand } from './commands/doctor_cmd.js';
|
|
12
|
+
import { setLogLevel, LogLevel } from '../logging.js';
|
|
13
|
+
|
|
14
|
+
const program = new Command();
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.name('edhindex')
|
|
18
|
+
.description('Local-first hybrid code search engine with MCP')
|
|
19
|
+
.version('1.0.0')
|
|
20
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
21
|
+
.option('--debug', 'Enable debug logging')
|
|
22
|
+
.hook('preAction', (thisCommand) => {
|
|
23
|
+
if (thisCommand.opts().debug) setLogLevel(LogLevel.Debug);
|
|
24
|
+
else if (thisCommand.opts().verbose) setLogLevel(LogLevel.Verbose);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('init')
|
|
29
|
+
.description('Initialize EDHIndex in the current directory')
|
|
30
|
+
.action(async () => {
|
|
31
|
+
const rootPath = process.cwd();
|
|
32
|
+
await initCommand(rootPath);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command('start')
|
|
37
|
+
.description('Build/update index and start the MCP server')
|
|
38
|
+
.action(async () => {
|
|
39
|
+
const rootPath = process.cwd();
|
|
40
|
+
await startCommand(rootPath);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.command('index')
|
|
45
|
+
.description('Build or update the search index')
|
|
46
|
+
.action(async () => {
|
|
47
|
+
const rootPath = process.cwd();
|
|
48
|
+
await indexCommand(rootPath);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command('search')
|
|
53
|
+
.description('Search the indexed codebase from the CLI')
|
|
54
|
+
.argument('<query>', 'Search query')
|
|
55
|
+
.option('-n, --max-results <number>', 'Max results', '10')
|
|
56
|
+
.action(async (query: string, opts: { maxResults: string }) => {
|
|
57
|
+
const rootPath = process.cwd();
|
|
58
|
+
await searchCommand(rootPath, query, parseInt(opts.maxResults, 10) || 10);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command('status')
|
|
63
|
+
.description('Show index status')
|
|
64
|
+
.action(async () => {
|
|
65
|
+
const rootPath = process.cwd();
|
|
66
|
+
await statusCommand(rootPath);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command('config')
|
|
71
|
+
.description('View or update configuration')
|
|
72
|
+
.argument('[key]', 'Config key')
|
|
73
|
+
.argument('[value]', 'Config value')
|
|
74
|
+
.action((key?: string, value?: string) => {
|
|
75
|
+
const rootPath = process.cwd();
|
|
76
|
+
const changes: Record<string, string> = {};
|
|
77
|
+
if (key && value !== undefined) {
|
|
78
|
+
changes[key] = value;
|
|
79
|
+
}
|
|
80
|
+
configCommand(rootPath, changes);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
program
|
|
84
|
+
.command('models')
|
|
85
|
+
.description('List available embedding models')
|
|
86
|
+
.action(() => {
|
|
87
|
+
modelsCommand();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command('doctor')
|
|
92
|
+
.description('Run diagnostics')
|
|
93
|
+
.action(async () => {
|
|
94
|
+
const rootPath = process.cwd();
|
|
95
|
+
const indexDir = join(rootPath, '.edhindex');
|
|
96
|
+
await doctorCommand(rootPath, indexDir);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { logger } from '../logging.js';
|
|
4
|
+
|
|
5
|
+
export type ModelTier = 'light' | 'balanced' | 'max';
|
|
6
|
+
export type LogLevelConfig = 'silent' | 'info' | 'verbose' | 'debug';
|
|
7
|
+
|
|
8
|
+
export interface Config {
|
|
9
|
+
model: ModelTier;
|
|
10
|
+
languages: string[];
|
|
11
|
+
watch: boolean;
|
|
12
|
+
rerank: boolean;
|
|
13
|
+
maxResults: number;
|
|
14
|
+
logLevel: LogLevelConfig;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_CONFIG: Config = {
|
|
18
|
+
model: 'balanced',
|
|
19
|
+
languages: ['ts', 'js', 'go', 'py'],
|
|
20
|
+
watch: true,
|
|
21
|
+
rerank: true,
|
|
22
|
+
maxResults: 10,
|
|
23
|
+
logLevel: 'info',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function defaultConfig(): Config {
|
|
27
|
+
return { ...DEFAULT_CONFIG };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function loadConfig(indexDir: string): Config {
|
|
31
|
+
const path = join(indexDir, 'config.json');
|
|
32
|
+
if (!existsSync(path)) {
|
|
33
|
+
const cfg = defaultConfig();
|
|
34
|
+
saveConfig(indexDir, cfg);
|
|
35
|
+
return cfg;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const raw = readFileSync(path, 'utf-8');
|
|
39
|
+
const parsed = JSON.parse(raw) as Partial<Config>;
|
|
40
|
+
return { ...defaultConfig(), ...parsed };
|
|
41
|
+
} catch (e) {
|
|
42
|
+
logger.error('Failed to load config, using defaults:', e);
|
|
43
|
+
return defaultConfig();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function saveConfig(indexDir: string, config: Config) {
|
|
48
|
+
if (!existsSync(indexDir)) {
|
|
49
|
+
mkdirSync(indexDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(join(indexDir, 'config.json'), JSON.stringify(config, null, 2), 'utf-8');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const MODEL_CONFIGS: Record<ModelTier, { modelId: string; dimensions: number; displayName: string; size: string }> = {
|
|
55
|
+
light: {
|
|
56
|
+
modelId: 'Xenova/all-MiniLM-L6-v2',
|
|
57
|
+
dimensions: 384,
|
|
58
|
+
displayName: 'all-MiniLM-L6-v2',
|
|
59
|
+
size: '~46MB',
|
|
60
|
+
},
|
|
61
|
+
balanced: {
|
|
62
|
+
modelId: 'Xenova/bge-base-en-v1.5',
|
|
63
|
+
dimensions: 768,
|
|
64
|
+
displayName: 'bge-base-en-v1.5',
|
|
65
|
+
size: '~109MB',
|
|
66
|
+
},
|
|
67
|
+
max: {
|
|
68
|
+
modelId: 'Xenova/bge-m3',
|
|
69
|
+
dimensions: 1024,
|
|
70
|
+
displayName: 'BGE-M3',
|
|
71
|
+
size: '~1.1GB',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const RERANKER_MODEL = 'Xenova/ms-marco-MiniLM-L-6-v2';
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { existsSync, statSync, accessSync, constants, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { logger } from '../logging.js';
|
|
4
|
+
import { MetadataStore } from '../storage/metadata.js';
|
|
5
|
+
import { FTSStore } from '../storage/fts.js';
|
|
6
|
+
import { EmbeddingProvider } from '../embeddings/provider.js';
|
|
7
|
+
import { VectorStore } from '../vector/store.js';
|
|
8
|
+
import { loadVersion } from '../version.js';
|
|
9
|
+
import { loadConfig, MODEL_CONFIGS, ModelTier } from '../config/index.js';
|
|
10
|
+
|
|
11
|
+
export interface DoctorResult {
|
|
12
|
+
category: string;
|
|
13
|
+
status: 'ok' | 'warn' | 'error';
|
|
14
|
+
message: string;
|
|
15
|
+
detail?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function runDoctor(rootPath: string, indexDir: string): Promise<DoctorResult[]> {
|
|
19
|
+
const results: DoctorResult[] = [];
|
|
20
|
+
|
|
21
|
+
// 1. Workspace check
|
|
22
|
+
results.push(checkPath('Workspace', rootPath, true));
|
|
23
|
+
|
|
24
|
+
// 2. Index directory
|
|
25
|
+
results.push(checkPath('Index directory', indexDir, false));
|
|
26
|
+
|
|
27
|
+
// 3. Config
|
|
28
|
+
try {
|
|
29
|
+
const config = loadConfig(indexDir);
|
|
30
|
+
results.push({ category: 'Config', status: 'ok', message: `Loaded (model: ${config.model})` });
|
|
31
|
+
} catch (e: any) {
|
|
32
|
+
results.push({ category: 'Config', status: 'error', message: `Failed: ${e.message}` });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 4. Version
|
|
36
|
+
const version = loadVersion(indexDir);
|
|
37
|
+
if (version) {
|
|
38
|
+
results.push({ category: 'Version', status: 'ok', message: `Schema v${version.schema}, ${version.embeddingModel}` });
|
|
39
|
+
} else {
|
|
40
|
+
results.push({ category: 'Version', status: 'warn', message: 'No version file (not indexed yet)' });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 5. SQLite metadata
|
|
44
|
+
try {
|
|
45
|
+
const meta = new MetadataStore(indexDir);
|
|
46
|
+
const chunkCount = meta.getChunkCount();
|
|
47
|
+
const fileCount = meta.getFileCount();
|
|
48
|
+
meta.close();
|
|
49
|
+
results.push({ category: 'SQLite (metadata)', status: 'ok', message: `${chunkCount} chunks from ${fileCount} files` });
|
|
50
|
+
} catch (e: any) {
|
|
51
|
+
results.push({ category: 'SQLite (metadata)', status: 'error', message: `Failed: ${e.message}` });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 6. SQLite FTS
|
|
55
|
+
try {
|
|
56
|
+
const fts = new FTSStore(indexDir);
|
|
57
|
+
fts.close();
|
|
58
|
+
results.push({ category: 'SQLite (FTS)', status: 'ok', message: 'Accessible' });
|
|
59
|
+
} catch (e: any) {
|
|
60
|
+
results.push({ category: 'SQLite (FTS)', status: 'error', message: `Failed: ${e.message}` });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 7. LanceDB
|
|
64
|
+
try {
|
|
65
|
+
const mod = await import('@lancedb/lancedb');
|
|
66
|
+
const db = await mod.connect(indexDir);
|
|
67
|
+
const names = await db.tableNames();
|
|
68
|
+
await db.close();
|
|
69
|
+
results.push({ category: 'LanceDB', status: 'ok', message: `Tables: ${names.join(', ') || 'none'}` });
|
|
70
|
+
} catch (e: any) {
|
|
71
|
+
results.push({ category: 'LanceDB', status: 'warn', message: `Check failed: ${e.message}` });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 8. Embedding model
|
|
75
|
+
try {
|
|
76
|
+
const config = loadConfig(indexDir);
|
|
77
|
+
const modelTier: ModelTier = config.model;
|
|
78
|
+
const modelCfg = MODEL_CONFIGS[modelTier];
|
|
79
|
+
const mod = await import('@huggingface/transformers');
|
|
80
|
+
await mod.pipeline('feature-extraction', modelCfg.modelId, { local_files_only: true });
|
|
81
|
+
results.push({ category: 'Embedding model', status: 'ok', message: `${modelCfg.displayName} (cached)` });
|
|
82
|
+
} catch {
|
|
83
|
+
try {
|
|
84
|
+
const config = loadConfig(indexDir);
|
|
85
|
+
const modelTier: ModelTier = config.model;
|
|
86
|
+
const modelCfg = MODEL_CONFIGS[modelTier];
|
|
87
|
+
results.push({ category: 'Embedding model', status: 'warn', message: `${modelCfg.displayName} (not cached, will download on first use)` });
|
|
88
|
+
} catch {
|
|
89
|
+
results.push({ category: 'Embedding model', status: 'warn', message: 'Unknown (config issue)' });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 9. Disk space
|
|
94
|
+
try {
|
|
95
|
+
const stat = statSync(indexDir);
|
|
96
|
+
const free = stat.size > 0 ? 'has data' : 'empty';
|
|
97
|
+
results.push({ category: 'Disk space', status: 'ok', message: `Index dir ${free}` });
|
|
98
|
+
} catch {
|
|
99
|
+
results.push({ category: 'Disk space', status: 'warn', message: 'Index dir not created yet' });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 10. Permissions
|
|
103
|
+
try {
|
|
104
|
+
accessSync(rootPath, constants.R_OK | constants.X_OK);
|
|
105
|
+
results.push({ category: 'Permissions', status: 'ok', message: 'Readable' });
|
|
106
|
+
} catch (e: any) {
|
|
107
|
+
results.push({ category: 'Permissions', status: 'error', message: `Cannot read workspace: ${e.message}` });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 11. MCP
|
|
111
|
+
results.push({ category: 'MCP Server', status: 'ok', message: 'Available on stdio transport' });
|
|
112
|
+
|
|
113
|
+
return results;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function checkPath(category: string, path: string, mustExist: boolean): DoctorResult {
|
|
117
|
+
const exists = existsSync(path);
|
|
118
|
+
if (mustExist && !exists) {
|
|
119
|
+
return { category, status: 'error', message: `Not found: ${path}` };
|
|
120
|
+
}
|
|
121
|
+
if (!mustExist && !exists) {
|
|
122
|
+
return { category, status: 'warn', message: `Not found: ${path} (will create on first index)` };
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const s = statSync(path);
|
|
126
|
+
const type = s.isDirectory() ? 'directory' : 'file';
|
|
127
|
+
return { category, status: 'ok', message: `${type}: ${path}` };
|
|
128
|
+
} catch (e: any) {
|
|
129
|
+
return { category, status: 'error', message: `Cannot access: ${e.message}` };
|
|
130
|
+
}
|
|
131
|
+
}
|