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/PROMPT.md +143 -218
- package/README.md +179 -156
- package/indexer.mjs +4 -22
- package/init.mjs +179 -68
- package/mcp-server.mjs +10 -1
- package/package.json +14 -13
- package/parser-utils.mjs +83 -20
- package/watch-daemon.mjs +23 -19
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
|
-
// ───
|
|
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: '
|
|
31
|
-
args: [
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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;
|
|
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
|
-
|
|
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;
|
|
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'] =
|
|
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
|
|
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
|
-
|
|
135
|
-
if (
|
|
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[
|
|
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 = [
|
|
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
|
|
189
|
-
log(
|
|
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('
|
|
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
|
-
|
|
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
|
|
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('\
|
|
229
|
-
if (configured.length) configured.forEach(c => log('
|
|
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('\
|
|
234
|
-
skipped.forEach(s => log('
|
|
330
|
+
log('\nSkipped:\n');
|
|
331
|
+
skipped.forEach(s => log(' - ' + s));
|
|
235
332
|
}
|
|
236
333
|
|
|
237
|
-
log('\
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
45
|
-
'.
|
|
46
|
-
'.
|
|
47
|
-
'.
|
|
48
|
-
'.
|
|
49
|
-
'.
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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:
|
|
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));
|