graph-indexer 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/init.mjs CHANGED
@@ -18,18 +18,42 @@
18
18
  import fs from 'fs';
19
19
  import path from 'path';
20
20
  import os from 'os';
21
+ import readline from 'readline';
21
22
  import { fileURLToPath } from 'url';
22
23
 
23
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
25
  const isDryRun = process.argv.includes('--dry-run');
26
+ const isAllLanguages = process.argv.includes('--all-languages');
25
27
  const PROJECT_ROOT = process.cwd();
26
28
 
27
- // ─── MCP Server config block ──────────────────────────────────────────────────
29
+ // ─── Language Registry ────────────────────────────────────────────────────────
30
+
31
+ const LANGUAGES = [
32
+ { key: 'typescript', label: 'TypeScript / TSX', exts: '.ts, .tsx' },
33
+ { key: 'javascript', label: 'JavaScript', exts: '.js, .jsx, .mjs, .cjs' },
34
+ { key: 'python', label: 'Python', exts: '.py' },
35
+ { key: 'go', label: 'Go', exts: '.go' },
36
+ { key: 'rust', label: 'Rust', exts: '.rs' },
37
+ { key: 'php', label: 'PHP', exts: '.php' },
38
+ { key: 'java', label: 'Java', exts: '.java' },
39
+ { key: 'kotlin', label: 'Kotlin', exts: '.kt, .kts' },
40
+ { key: 'csharp', label: 'C#', exts: '.cs' },
41
+ { key: 'ruby', label: 'Ruby', exts: '.rb' },
42
+ { key: 'css', label: 'CSS / SCSS', exts: '.css, .scss' },
43
+ ];
44
+
45
+ // ─── MCP Server config blocks ─────────────────────────────────────────────────
28
46
 
29
47
  const SERVER_CONFIG = {
30
- command: 'node',
31
- args: [path.join(PROJECT_ROOT, 'node_modules', 'graph-indexer', 'mcp-server.mjs')],
32
- env: { MCP_PROJECT_ROOT: PROJECT_ROOT }
48
+ command: 'npm',
49
+ args: ['run', 'mcp:start'],
50
+ env: { MCP_PROJECT_ROOT: PROJECT_ROOT },
51
+ };
52
+
53
+ const SERVER_CONFIG_GLOBAL = {
54
+ command: 'npm',
55
+ args: ['run', '--prefix', PROJECT_ROOT, 'mcp:start'],
56
+ env: { MCP_PROJECT_ROOT: PROJECT_ROOT },
33
57
  };
34
58
 
35
59
  // ─── Utilities ────────────────────────────────────────────────────────────────
@@ -37,44 +61,120 @@ const SERVER_CONFIG = {
37
61
  function log(msg) { process.stdout.write(msg + '\n'); }
38
62
 
39
63
  function writeFile(filePath, content) {
40
- if (isDryRun) {
41
- log(` [dry-run] Would write: ${filePath}`);
42
- return;
43
- }
64
+ if (isDryRun) { log(` [dry-run] Would write: ${filePath}`); return; }
44
65
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
45
66
  fs.writeFileSync(filePath, content, 'utf-8');
46
67
  }
47
68
 
48
69
  function readJsonSafe(filePath) {
49
70
  try {
50
- if (fs.existsSync(filePath)) {
51
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
52
- }
71
+ if (fs.existsSync(filePath)) return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
53
72
  } catch { /* malformed JSON — start fresh */ }
54
73
  return null;
55
74
  }
56
75
 
57
- /** Deep-merge src into dst without duplicating top-level keys. */
58
- function mergeJson(dst, src) {
59
- for (const [k, v] of Object.entries(src)) {
60
- if (dst[k] && typeof dst[k] === 'object' && !Array.isArray(dst[k]) && typeof v === 'object' && !Array.isArray(v)) {
61
- mergeJson(dst[k], v);
62
- } else {
63
- dst[k] = v;
64
- }
76
+ // ─── Interactive Language Selection ───────────────────────────────────────────
77
+
78
+ function selectLanguages() {
79
+ return new Promise((resolve) => {
80
+ if (isAllLanguages || !process.stdin.isTTY) return resolve(null);
81
+
82
+ const { stdin, stdout } = process;
83
+ readline.emitKeypressEvents(stdin);
84
+ stdin.setRawMode(true);
85
+ stdout.write('\x1B[?25l');
86
+
87
+ let cursorIndex = 0;
88
+ const selected = new Set();
89
+ let hasRendered = false;
90
+
91
+ const render = () => {
92
+ if (hasRendered) {
93
+ readline.moveCursor(stdout, 0, -(LANGUAGES.length + 2));
94
+ readline.cursorTo(stdout, 0);
95
+ readline.clearScreenDown(stdout);
96
+ }
97
+ hasRendered = true;
98
+
99
+ stdout.write('⚙️ Select languages (Arrows/Tab: move, Space: toggle, Enter: confirm):\n\n');
100
+
101
+ LANGUAGES.forEach((lang, i) => {
102
+ const isHovered = i === cursorIndex;
103
+ const isSelected = selected.has(i);
104
+
105
+ const prefix = isHovered ? '❯' : ' ';
106
+ const checkbox = isSelected ? '◉' : '◯';
107
+
108
+ const line = ` ${prefix} ${checkbox} ${lang.label.padEnd(20)} ${lang.exts}\n`;
109
+ stdout.write(isHovered ? `\x1b[36m${line}\x1b[0m` : line);
110
+ });
111
+ };
112
+
113
+ const cleanup = () => {
114
+ stdin.setRawMode(false);
115
+ stdout.write('\x1B[?25h');
116
+ stdin.removeListener('keypress', onKeypress);
117
+ };
118
+
119
+ const onKeypress = (str, key) => {
120
+ if (key.ctrl && key.name === 'c') {
121
+ cleanup();
122
+ process.exit(0);
123
+ }
124
+
125
+ if (key.name === 'up' || (key.name === 'tab' && key.shift)) {
126
+ cursorIndex = (cursorIndex - 1 + LANGUAGES.length) % LANGUAGES.length;
127
+ render();
128
+ } else if (key.name === 'down' || (key.name === 'tab' && !key.shift)) {
129
+ cursorIndex = (cursorIndex + 1) % LANGUAGES.length;
130
+ render();
131
+ } else if (key.name === 'space') {
132
+ if (selected.has(cursorIndex)) selected.delete(cursorIndex);
133
+ else selected.add(cursorIndex);
134
+ render();
135
+ } else if (key.name === 'return') {
136
+ cleanup();
137
+ readline.moveCursor(stdout, 0, -(LANGUAGES.length + 2));
138
+ readline.cursorTo(stdout, 0);
139
+ readline.clearScreenDown(stdout);
140
+
141
+ if (selected.size === 0) {
142
+ resolve(null); // Enable all languages by default
143
+ } else {
144
+ resolve(Array.from(selected).map(i => LANGUAGES[i].key));
145
+ }
146
+ }
147
+ };
148
+
149
+ stdin.on('keypress', onKeypress);
150
+ render();
151
+ });
152
+ }
153
+
154
+ function saveLanguageConfig(languages) {
155
+ const configPath = path.join(PROJECT_ROOT, '.graph-indexer.json');
156
+ if (isDryRun) {
157
+ if (languages) log(` [dry-run] Would write: ${configPath}`);
158
+ return;
159
+ }
160
+ const existing = readJsonSafe(configPath) || {};
161
+ if (!languages) {
162
+ delete existing.languages;
163
+ } else {
164
+ existing.languages = languages;
165
+ }
166
+ if (languages || Object.keys(existing).length > 0) {
167
+ fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf-8');
65
168
  }
66
- return dst;
67
169
  }
68
170
 
69
171
  // ─── IDE Detectors & Configurators ───────────────────────────────────────────
70
172
 
71
173
  function configureVSCode() {
72
- const vscodeDir = path.join(PROJECT_ROOT, '.vscode');
73
- const configPath = path.join(vscodeDir, 'mcp.json');
74
-
174
+ const configPath = path.join(PROJECT_ROOT, '.vscode', 'mcp.json');
75
175
  const existing = readJsonSafe(configPath) || {};
76
176
  if (!existing.servers) existing.servers = {};
77
- if (existing.servers['graph-indexer']) return false; // already configured
177
+ if (existing.servers['graph-indexer']) return false;
78
178
 
79
179
  existing.servers['graph-indexer'] = SERVER_CONFIG;
80
180
  writeFile(configPath, JSON.stringify(existing, null, 2) + '\n');
@@ -82,15 +182,7 @@ function configureVSCode() {
82
182
  }
83
183
 
84
184
  function configureCursor() {
85
- // Check both project-level and global Cursor config
86
- const candidates = [
87
- path.join(PROJECT_ROOT, '.cursor', 'mcp.json'),
88
- path.join(os.homedir(), '.cursor', 'mcp.json'),
89
- ];
90
- const detected = candidates.some(p => fs.existsSync(path.dirname(p)));
91
-
92
- // Prefer project-level config
93
- const configPath = candidates[0];
185
+ const configPath = path.join(PROJECT_ROOT, '.cursor', 'mcp.json');
94
186
  const existing = readJsonSafe(configPath) || {};
95
187
  if (!existing.mcpServers) existing.mcpServers = {};
96
188
  if (existing.mcpServers['graph-indexer']) return false;
@@ -108,34 +200,28 @@ function configureClaudeDesktop() {
108
200
  configPath = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
109
201
  }
110
202
 
111
- if (!fs.existsSync(path.dirname(configPath))) return false; // Claude not installed
203
+ if (!fs.existsSync(path.dirname(configPath))) return false;
112
204
 
113
205
  const existing = readJsonSafe(configPath) || {};
114
206
  if (!existing.mcpServers) existing.mcpServers = {};
115
207
  if (existing.mcpServers['graph-indexer']) return false;
116
208
 
117
- existing.mcpServers['graph-indexer'] = SERVER_CONFIG;
209
+ existing.mcpServers['graph-indexer'] = SERVER_CONFIG_GLOBAL;
118
210
  writeFile(configPath, JSON.stringify(existing, null, 2) + '\n');
119
211
  return true;
120
212
  }
121
213
 
122
214
  function configureClaudeCode() {
123
- // Claude Code stores MCP config in .mcp.json or .claude/settings.json at project root
124
- const candidates = [
125
- path.join(PROJECT_ROOT, '.claude', 'settings.json'),
126
- path.join(PROJECT_ROOT, '.mcp.json'),
127
- ];
128
-
129
- // Prefer .claude/settings.json if directory exists, else .mcp.json
130
215
  const hasClaudeDir = fs.existsSync(path.join(PROJECT_ROOT, '.claude'));
131
- const configPath = hasClaudeDir ? candidates[0] : candidates[1];
216
+ const configPath = hasClaudeDir
217
+ ? path.join(PROJECT_ROOT, '.claude', 'settings.json')
218
+ : path.join(PROJECT_ROOT, '.mcp.json');
132
219
 
133
220
  const existing = readJsonSafe(configPath) || {};
134
- const serverKey = hasClaudeDir ? 'mcpServers' : 'mcpServers';
135
- if (!existing[serverKey]) existing[serverKey] = {};
136
- if (existing[serverKey]['graph-indexer']) return false;
221
+ if (!existing.mcpServers) existing.mcpServers = {};
222
+ if (existing.mcpServers['graph-indexer']) return false;
137
223
 
138
- existing[serverKey]['graph-indexer'] = SERVER_CONFIG;
224
+ existing.mcpServers['graph-indexer'] = SERVER_CONFIG;
139
225
  writeFile(configPath, JSON.stringify(existing, null, 2) + '\n');
140
226
  return true;
141
227
  }
@@ -174,7 +260,13 @@ function updateGitignore() {
174
260
  const gitignorePath = path.join(PROJECT_ROOT, '.gitignore');
175
261
  const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
176
262
 
177
- const entries = ['code-index.json', 'code-index.embeddings.bin'];
263
+ const entries = [
264
+ 'code-index.json',
265
+ 'code-index.embeddings.bin',
266
+ '.idx-daemon.pid',
267
+ '.idx-daemon.log'
268
+ ];
269
+
178
270
  const toAdd = entries.filter(e => !existing.includes(e));
179
271
  if (toAdd.length === 0) return false;
180
272
 
@@ -185,13 +277,23 @@ function updateGitignore() {
185
277
 
186
278
  // ─── Main ─────────────────────────────────────────────────────────────────────
187
279
 
188
- log('\n🔧 graph-indexer init' + (isDryRun ? ' [dry-run]' : '') + '\n');
189
- log(`📂 Project: ${PROJECT_ROOT}\n`);
280
+ log('\n🚀 graph-indexer init' + (isDryRun ? ' [dry-run]' : '') + '\n');
281
+ log(`Project: ${PROJECT_ROOT}`);
282
+
283
+ // Language selection (interactive menu).
284
+ const selectedLanguages = await selectLanguages();
285
+ saveLanguageConfig(selectedLanguages);
286
+
287
+ if (selectedLanguages) {
288
+ const names = selectedLanguages.map(k => LANGUAGES.find(l => l.key === k)?.label || k);
289
+ log(`\n✅ Enabled languages: ${names.join(', ')}\n`);
290
+ } else {
291
+ log('\n✅ Enabled languages: all (default)\n');
292
+ }
190
293
 
191
294
  const configured = [];
192
295
  const skipped = [];
193
296
 
194
- // IDE configuration
195
297
  const ides = [
196
298
  { name: 'VS Code', fn: configureVSCode },
197
299
  { name: 'Cursor', fn: configureCursor },
@@ -199,43 +301,52 @@ const ides = [
199
301
  { name: 'Claude Code', fn: configureClaudeCode },
200
302
  ];
201
303
 
202
- log('🔍 Detecting IDEs / agents...\n');
304
+ log('Detecting IDEs / agents...\n');
203
305
  for (const { name, fn } of ides) {
204
306
  try {
205
307
  const result = fn();
206
- if (result === false) {
207
- skipped.push(name + ' (already configured or not installed)');
208
- } else {
209
- configured.push(name);
210
- }
308
+ if (result === false) skipped.push(name + ' (already configured or not installed)');
309
+ else configured.push(name);
211
310
  } catch (e) {
212
311
  skipped.push(name + ' (error: ' + e.message + ')');
213
312
  }
214
313
  }
215
314
 
216
- // Package scripts
217
315
  const scriptsAdded = addPackageScripts();
218
316
  if (scriptsAdded) configured.push('package.json scripts (mcp:index, mcp:watch, mcp:start)');
219
317
  else skipped.push('package.json scripts (already present)');
220
318
 
221
- // .gitignore
222
319
  const gitignoreUpdated = updateGitignore();
223
- if (gitignoreUpdated) configured.push('.gitignore (code-index.json, code-index.embeddings.bin)');
224
- else skipped.push('.gitignore (already contains index entries)');
320
+ if (gitignoreUpdated) configured.push('.gitignore (code-index, daemon artifacts)');
321
+ else skipped.push('.gitignore (already contains index/daemon entries)');
225
322
 
226
323
  // ─── Summary ─────────────────────────────────────────────────────────────────
227
324
 
228
- log('\n✅ Configured:\n');
229
- if (configured.length) configured.forEach(c => log(' • ' + c));
325
+ log('\nConfigured:\n');
326
+ if (configured.length) configured.forEach(c => log(' * ' + c));
230
327
  else log(' (nothing new to configure)');
231
328
 
232
329
  if (skipped.length) {
233
- log('\n⏭️ Skipped:\n');
234
- skipped.forEach(s => log(' • ' + s));
330
+ log('\nSkipped:\n');
331
+ skipped.forEach(s => log(' - ' + s));
235
332
  }
236
333
 
237
- log('\n📋 Next steps:\n');
334
+ log('\nNext steps:\n');
238
335
  log(' 1. Run `npm run mcp:index` to index this project');
239
- log(' 2. Restart your IDE to activate MCP servers');
336
+ log(' 2. Restart your IDE to activate the MCP server');
240
337
  log(' 3. Run `npm run mcp:start` if your IDE needs the server started manually');
241
- log('\n');
338
+
339
+ // Mensaje final de agradecimiento
340
+ log('\n─────────────────────────────────────────────────────────────────');
341
+ log('✨ Thank you for setting up graph-indexer!');
342
+ log(' Enjoy your blazing-fast, AST-precise codebase search.');
343
+ log('─────────────────────────────────────────────────────────────────\n');
344
+ log('');
345
+ log('🤖 Agent Instructions:');
346
+ log(' To teach your AI agent how to use graph-indexer effectively,');
347
+ log(' copy the system prompt from our repository:');
348
+ log(' https://github.com/MaquinaTech/graph-indexer/blob/main/PROMPT.md?plain=1');
349
+ log('─────────────────────────────────────────────────────────────────\n');
350
+
351
+ // Cierre limpio de la ejecución para devolver el control a la terminal
352
+ process.exit(0);
package/mcp-server.mjs CHANGED
@@ -21,6 +21,15 @@ const PROJECT_ROOT = process.env.MCP_PROJECT_ROOT || process.cwd();
21
21
  const INDEX_PATH = resolve(PROJECT_ROOT, "code-index.json");
22
22
  const PID_FILE = resolve(PROJECT_ROOT, ".idx-daemon.pid");
23
23
 
24
+ // Single source of truth for the version: graph-indexer's own package.json
25
+ // (next to this file), never the consumer project's package.json.
26
+ const PACKAGE_DIR = path.dirname(fileURLToPath(import.meta.url));
27
+ function readPackageVersion() {
28
+ try {
29
+ return JSON.parse(fs.readFileSync(path.join(PACKAGE_DIR, "package.json"), "utf-8")).version || "0.0.0";
30
+ } catch { return "0.0.0"; }
31
+ }
32
+
24
33
  // ─── Daemon Orchestration ─────────────────────────────────────────────────────
25
34
 
26
35
  function ensureDaemonRunning() {
@@ -100,7 +109,7 @@ function pruneBodyByQuery(codeSnippet, queryTokens, maxLines = 40) {
100
109
  // ─── Server Initialization ────────────────────────────────────────────────────
101
110
 
102
111
  ensureDaemonRunning();
103
- const version = "1.0.2";
112
+ const version = readPackageVersion();
104
113
  const server = new McpServer({ name: "graph-indexer", version });
105
114
 
106
115
  // Use lazy (disk-backed) vector loading for large corpora — cacheEmbeddings:false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graph-indexer",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Zero-DB, Air-gapped, In-Memory Graph Indexer MCP Server with Hybrid RRF Search and Cross-Language Topology",
5
5
  "type": "module",
6
6
  "main": "core-engine.mjs",
@@ -26,6 +26,7 @@
26
26
  "scripts": {
27
27
  "start": "node ./mcp-server.mjs",
28
28
  "test": "node test/run.mjs",
29
+ "test:unit": "node test/unit.mjs",
29
30
  "test:setup": "node test/setup.mjs",
30
31
  "test:run": "node test/run.mjs",
31
32
  "test:embeddings": "OLLAMA_HOST=http://localhost:11435 node test/run-embeddings.mjs",
@@ -42,17 +43,6 @@
42
43
  "chokidar": "5.0.0",
43
44
  "ignore": "5.3.2",
44
45
  "tree-sitter": "0.21.1",
45
- "tree-sitter-c-sharp": "^0.21.3",
46
- "tree-sitter-css": "0.21.1",
47
- "tree-sitter-go": "0.21.0",
48
- "tree-sitter-java": "^0.21.0",
49
- "tree-sitter-javascript": "0.21.4",
50
- "tree-sitter-kotlin": "^0.3.8",
51
- "tree-sitter-php": "0.23.1",
52
- "tree-sitter-python": "0.21.0",
53
- "tree-sitter-ruby": "^0.23.1",
54
- "tree-sitter-rust": "0.21.0",
55
- "tree-sitter-typescript": "0.21.2",
56
46
  "zod": "3.23.8"
57
47
  },
58
48
  "keywords": [
@@ -88,6 +78,17 @@
88
78
  "access": "public"
89
79
  },
90
80
  "optionalDependencies": {
91
- "hnswlib-node": "^3.0.0"
81
+ "hnswlib-node": "^3.0.0",
82
+ "tree-sitter-c-sharp": "^0.21.3",
83
+ "tree-sitter-css": "0.21.1",
84
+ "tree-sitter-go": "0.21.0",
85
+ "tree-sitter-java": "^0.21.0",
86
+ "tree-sitter-javascript": "0.21.4",
87
+ "tree-sitter-kotlin": "^0.3.8",
88
+ "tree-sitter-php": "0.23.1",
89
+ "tree-sitter-python": "0.21.0",
90
+ "tree-sitter-ruby": "^0.23.1",
91
+ "tree-sitter-rust": "0.21.0",
92
+ "tree-sitter-typescript": "0.21.2"
92
93
  }
93
94
  }
package/parser-utils.mjs CHANGED
@@ -21,32 +21,63 @@ import ignore from 'ignore';
21
21
  import Parser from 'tree-sitter';
22
22
  const { Query } = Parser; // Native parser query helper
23
23
 
24
- import TypeScript from 'tree-sitter-typescript';
25
- import JavaScript from 'tree-sitter-javascript';
26
- import CSS from 'tree-sitter-css';
27
- import Python from 'tree-sitter-python';
28
- import Rust from 'tree-sitter-rust';
29
- import Go from 'tree-sitter-go';
30
- import PHP from 'tree-sitter-php';
31
- import Java from 'tree-sitter-java';
32
- import Kotlin from 'tree-sitter-kotlin';
33
- import CSharp from 'tree-sitter-c-sharp';
34
- import Ruby from 'tree-sitter-ruby';
35
24
  import { truncateForEmbedding } from './core-engine.mjs';
36
25
 
37
26
  export const MAX_FILE_SIZE_BYTES = 500000;
38
27
  export const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434";
39
28
 
29
+ // ─── Dynamic Language Loading ─────────────────────────────────────────────────
30
+
31
+ function _loadProjectConfig() {
32
+ const configPath = path.join(process.env.MCP_PROJECT_ROOT || process.cwd(), '.graph-indexer.json');
33
+ try {
34
+ if (fs.existsSync(configPath)) return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
35
+ } catch { /* ignore */ }
36
+ return null;
37
+ }
38
+
39
+ async function _tryLang(pkg, enabledLangs, key) {
40
+ if (enabledLangs && !enabledLangs.includes(key)) return null;
41
+ try {
42
+ return (await import(pkg)).default;
43
+ } catch {
44
+ process.stderr.write(`[graph-indexer] WARNING: ${pkg} not installed — ${key} files will be skipped\n`);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ const _cfg = _loadProjectConfig();
50
+ const _enabled = _cfg?.languages ?? null; // null = all languages
51
+
52
+ const [
53
+ TypeScript, JavaScript, CSS, Python, Rust,
54
+ Go, PHP, Java, Kotlin, CSharp, Ruby
55
+ ] = await Promise.all([
56
+ _tryLang('tree-sitter-typescript', _enabled, 'typescript'),
57
+ _tryLang('tree-sitter-javascript', _enabled, 'javascript'),
58
+ _tryLang('tree-sitter-css', _enabled, 'css'),
59
+ _tryLang('tree-sitter-python', _enabled, 'python'),
60
+ _tryLang('tree-sitter-rust', _enabled, 'rust'),
61
+ _tryLang('tree-sitter-go', _enabled, 'go'),
62
+ _tryLang('tree-sitter-php', _enabled, 'php'),
63
+ _tryLang('tree-sitter-java', _enabled, 'java'),
64
+ _tryLang('tree-sitter-kotlin', _enabled, 'kotlin'),
65
+ _tryLang('tree-sitter-c-sharp', _enabled, 'csharp'),
66
+ _tryLang('tree-sitter-ruby', _enabled, 'ruby'),
67
+ ]);
68
+
40
69
  const LANGUAGE_MAP = {
41
- '.ts': TypeScript.typescript, '.tsx': TypeScript.tsx,
42
- '.js': JavaScript, '.jsx': JavaScript, '.mjs': JavaScript, '.cjs': JavaScript,
43
- '.css': CSS, '.scss': CSS,
44
- '.py': Python, '.rs': Rust, '.go': Go,
45
- '.php': PHP.php,
46
- '.java': Java,
47
- '.kt': Kotlin, '.kts': Kotlin,
48
- '.cs': CSharp,
49
- '.rb': Ruby,
70
+ ...(TypeScript ? { '.ts': TypeScript.typescript, '.tsx': TypeScript.tsx } : {}),
71
+ ...(JavaScript ? { '.js': JavaScript, '.jsx': JavaScript, '.mjs': JavaScript, '.cjs': JavaScript } : {}),
72
+ ...(CSS ? { '.css': CSS, '.scss': CSS } : {}),
73
+ ...(Python ? { '.py': Python } : {}),
74
+ ...(Rust ? { '.rs': Rust } : {}),
75
+ ...(Go ? { '.go': Go } : {}),
76
+ ...(PHP ? { '.php': PHP.php } : {}),
77
+ ...(Java ? { '.java': Java } : {}),
78
+ ...(Kotlin ? { '.kt': Kotlin, '.kts': Kotlin } : {}),
79
+ ...(CSharp ? { '.cs': CSharp } : {}),
80
+ ...(Ruby ? { '.rb': Ruby } : {}),
50
81
  };
51
82
 
52
83
  // 🥇 ULTRA-GENERIC QUERIES: Immune to grammar changes between TS/JS/TSX
@@ -606,6 +637,38 @@ export function resolveLocalImports(rawImports, fromFileRelPath, projectRoot) {
606
637
  return resolved;
607
638
  }
608
639
 
640
+ /**
641
+ * Build the text payload sent to the embedding model for a chunk.
642
+ *
643
+ * Shared by the bootstrap indexer (indexer.mjs) and the watch daemon
644
+ * (watch-daemon.mjs) so a chunk yields the SAME embedding regardless of which
645
+ * path embedded it first. This matters because the cache key (content_hash) is
646
+ * derived from code + docstring only — it does NOT include this payload — so two
647
+ * divergent payloads for the same hash would silently produce inconsistent
648
+ * embeddings across a full re-index vs. an incremental update.
649
+ *
650
+ * @param {object} chunk Semantic chunk: { file_path, node_type, name, docstring, type_refs, code_snippet }.
651
+ * @param {string[]} depRelPaths Resolved local imports of the chunk's file (project-relative paths).
652
+ * @returns {string}
653
+ */
654
+ export function buildEmbeddingPayload(chunk, depRelPaths = []) {
655
+ const neighbors = depRelPaths
656
+ .map(d => path.basename(d, path.extname(d)))
657
+ .filter(Boolean);
658
+ const topologicalContext = neighbors.length
659
+ ? `This code architectural neighborhood connects with: ${neighbors.join(', ')}.`
660
+ : '';
661
+ return [
662
+ `File Location: ${chunk.file_path}`,
663
+ `Symbol Name: ${chunk.node_type} -> ${chunk.name}`,
664
+ chunk.docstring ? `Developer Documentation: ${chunk.docstring}` : '',
665
+ chunk.type_refs?.length ? `Type References: ${chunk.type_refs.join(', ')}` : '',
666
+ topologicalContext,
667
+ `--- Source Code ---`,
668
+ chunk.code_snippet,
669
+ ].filter(Boolean).join('\n');
670
+ }
671
+
609
672
  export async function getLocalEmbedding(text, graceful = true) {
610
673
  if (process.env.INDEXER_EMBEDDINGS === 'off') return null; // lexical-only mode
611
674
  const MAX_RETRIES = 3;
package/watch-daemon.mjs CHANGED
@@ -22,7 +22,8 @@ import chokidar from 'chokidar';
22
22
  import { MemoryGraphIndex } from './core-engine.mjs';
23
23
  import {
24
24
  MAX_FILE_SIZE_BYTES, EXTENSIONS, getParserForFile, buildIgnoreFilter,
25
- extractImportsFromAST, extractSemanticChunks, resolveLocalImports, getLocalEmbeddingsBatch
25
+ extractImportsFromAST, extractSemanticChunks, resolveLocalImports, getLocalEmbeddingsBatch,
26
+ buildEmbeddingPayload
26
27
  } from './parser-utils.mjs';
27
28
 
28
29
  const PROJECT_ROOT = process.env.MCP_PROJECT_ROOT || process.cwd();
@@ -79,23 +80,11 @@ async function processFileChange(absolutePath) {
79
80
  const chunksToEmbed = newChunks.filter(c => !db.embeddingCache.has(c.content_hash));
80
81
 
81
82
  if (chunksToEmbed.length > 0) {
82
- const textsToEmbed = chunksToEmbed.map(c => {
83
- const dependencies = db.graph.dependencies[c.file_path] || [];
84
- const cleanDeps = dependencies.map(d => path.basename(d, path.extname(d)));
85
- const topologicalContext = cleanDeps.length > 0
86
- ? `This code architectural neighborhood connects with: ${cleanDeps.join(', ')}.`
87
- : '';
88
-
89
- // 🥇 STRICT PAYLOAD PARITY: Eliminate real-time bleed
90
- return [
91
- `File Location: ${c.file_path}`,
92
- `Symbol Name: ${c.node_type} -> ${c.name}`,
93
- c.docstring ? `Developer Documentation: ${c.docstring}` : '',
94
- topologicalContext,
95
- `--- Source Code ---`,
96
- c.code_snippet
97
- ].filter(Boolean).join('\n');
98
- });
83
+ // 🥇 STRICT PAYLOAD PARITY: identical embedding payload to indexer.mjs
84
+ // (shared helper) so incremental updates match the bootstrap index.
85
+ const textsToEmbed = chunksToEmbed.map(c =>
86
+ buildEmbeddingPayload(c, db.graph.dependencies[c.file_path] || [])
87
+ );
99
88
 
100
89
  const embeddingsMatrix = await getLocalEmbeddingsBatch(textsToEmbed, true);
101
90
  if (embeddingsMatrix) {
@@ -137,8 +126,23 @@ async function processFileChange(absolutePath) {
137
126
  }
138
127
 
139
128
  process.stderr.write(`🚀 Chokidar Watcher Daemon started in: ${PROJECT_ROOT}\n`);
129
+
130
+ // Only watch source files we can actually index. Critically, this prevents
131
+ // chokidar from descending into node_modules / dist / .git / .gitignored dirs —
132
+ // watching those would exhaust the OS file-watcher limit (ENOSPC on Linux) and
133
+ // waste CPU/memory on large projects. The ignore decision happens at the watcher
134
+ // level, not just in processFileChange, so ignored trees are never traversed.
135
+ function shouldIgnore(absPath) {
136
+ const rel = path.relative(PROJECT_ROOT, absPath).replace(/\\/g, '/');
137
+ if (!rel) return false; // the project root itself
138
+ if (path.basename(absPath).startsWith('.')) return true; // dotfiles / dot-dirs
139
+ try { return ignoreFilter.ignores(rel); } // node_modules, dist, .gitignore, …
140
+ catch { return false; }
141
+ }
142
+
140
143
  const watcher = chokidar.watch(PROJECT_ROOT, {
141
- ignored: /(^|[\/\\])\../, persistent: true,
144
+ ignored: shouldIgnore,
145
+ persistent: true,
142
146
  awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 100 }
143
147
  });
144
148
  watcher.on('add', processFileChange).on('change', processFileChange).on('unlink', (p) => processFileChange(p));