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.
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ export interface EmbeddingProvider {
2
+ embed(texts: string[]): Promise<number[][]>;
3
+ dimensions(): number;
4
+ name(): string;
5
+ load(): Promise<void>;
6
+ dispose(): void;
7
+ }
8
+
9
+ export type EmbeddingModelId = 'light' | 'balanced' | 'max';