graph-indexer 1.0.0 → 1.0.1
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/LICENSE +21 -677
- package/PROMPT.md +35 -191
- package/README.md +794 -242
- package/core-engine.mjs +91 -20
- package/indexer.mjs +11 -11
- package/init.mjs +241 -0
- package/mcp-server.mjs +90 -13
- package/package.json +16 -4
- package/parser-utils.mjs +316 -16
- package/watch-daemon.mjs +23 -14
package/core-engine.mjs
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* @description In-Memory Graph Indexer Core Engine.
|
|
4
4
|
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
5
5
|
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
6
|
-
* @license
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
6
|
+
* @license MIT
|
|
7
|
+
* Copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions: The above copyright
|
|
14
|
+
* notice and this permission notice shall be included in all copies or
|
|
15
|
+
* substantial portions of the Software.
|
|
16
16
|
*/
|
|
17
17
|
import fs from 'fs';
|
|
18
18
|
|
|
@@ -140,12 +140,21 @@ export class MemoryGraphIndex {
|
|
|
140
140
|
const deps = this.graph.dependencies[chunk.file_path] || [];
|
|
141
141
|
const cleanDeps = deps.map(d => d.split('/').pop().split('.')[0]);
|
|
142
142
|
|
|
143
|
-
const enrichedContext =
|
|
144
|
-
|
|
143
|
+
const enrichedContext = [
|
|
144
|
+
chunk.name,
|
|
145
|
+
chunk.docstring || '',
|
|
146
|
+
cleanDeps.join(' '),
|
|
147
|
+
(chunk.calls || []).join(' '),
|
|
148
|
+
(chunk.params || []).join(' '),
|
|
149
|
+
chunk.return_type || '',
|
|
150
|
+
chunk.class_context ? `${chunk.class_context}.${chunk.name}` : '',
|
|
151
|
+
chunk.code_snippet
|
|
152
|
+
].join(' ');
|
|
153
|
+
this._indexLexical(chunk.id, enrichedContext, chunk.file_path);
|
|
145
154
|
}
|
|
146
155
|
}
|
|
147
156
|
|
|
148
|
-
_indexLexical(chunkId, text) {
|
|
157
|
+
_indexLexical(chunkId, text, filePath = '') {
|
|
149
158
|
const tokens = tokenize(text);
|
|
150
159
|
if (tokens.length === 0) return;
|
|
151
160
|
|
|
@@ -160,6 +169,19 @@ export class MemoryGraphIndex {
|
|
|
160
169
|
this.df.set(term, (this.df.get(term) || 0) + 1);
|
|
161
170
|
}
|
|
162
171
|
|
|
172
|
+
// 🥇 FILENAME TOKEN BOOST: Add file path tokens with fixed TF weight 1.0 (not sublinear)
|
|
173
|
+
// Splits on /, -, _, ., CamelCase so "authStore.ts" → ["auth", "store", "ts", "authstore"]
|
|
174
|
+
if (filePath) {
|
|
175
|
+
const fileTokens = tokenize(filePath.replace(/[\/\-_.]/g, ' '));
|
|
176
|
+
for (const token of fileTokens) {
|
|
177
|
+
if (!tfMap.has(token)) {
|
|
178
|
+
// Only add if not already from the body (avoids double-counting)
|
|
179
|
+
tfMap.set(token, 1.0);
|
|
180
|
+
this.df.set(token, (this.df.get(token) || 0) + 1);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
163
185
|
this.tf.set(chunkId, tfMap);
|
|
164
186
|
this.docCount++;
|
|
165
187
|
}
|
|
@@ -310,23 +332,71 @@ export class MemoryGraphIndex {
|
|
|
310
332
|
const vectorResults = queryVector ? this._searchVector(queryVector, minScore) : [];
|
|
311
333
|
const rrfScores = new Map();
|
|
312
334
|
const K = this.rrfK;
|
|
313
|
-
const allResults = [...vectorResults, ...lexicalResults];
|
|
314
335
|
|
|
315
336
|
const queryLower = queryText.toLowerCase();
|
|
316
|
-
|
|
317
|
-
|
|
337
|
+
|
|
338
|
+
// 🥇 FILENAME PATH BOOST: when Ollama offline (no vectors), boost chunks from
|
|
339
|
+
// files whose path contains a token from the query (e.g. "auth middleware" → authStore.ts)
|
|
340
|
+
const vectorEmpty = vectorResults.length === 0;
|
|
341
|
+
const queryFileTokens = vectorEmpty
|
|
342
|
+
? queryLower.split(/[\s\W_]+/).filter(t => t.length >= 3)
|
|
343
|
+
: [];
|
|
344
|
+
|
|
345
|
+
// Weighted RRF: lexical weight 1.5×, vector weight 1.0×
|
|
346
|
+
// Keeps semantic lift without drowning precise TF-IDF matches.
|
|
347
|
+
const LEXICAL_WEIGHT = 1.5;
|
|
348
|
+
const VECTOR_WEIGHT = 1.0;
|
|
349
|
+
|
|
350
|
+
const allResults = [
|
|
351
|
+
...vectorResults.map(r => ({ ...r, _w: VECTOR_WEIGHT })),
|
|
352
|
+
...lexicalResults.map(r => ({ ...r, _w: LEXICAL_WEIGHT })),
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
for (const { id, rank, _w } of allResults) {
|
|
356
|
+
let baseScore = (_w ?? 1.0) / (K + rank);
|
|
318
357
|
const chunk = this.chunks.get(id);
|
|
319
358
|
if (!chunk) continue;
|
|
320
359
|
|
|
321
|
-
// Generic: demote test/spec files when the query is not about tests
|
|
322
|
-
|
|
323
|
-
|
|
360
|
+
// Generic: demote test/spec files when the query is not about tests.
|
|
361
|
+
// Catches: foo.test.js, foo.spec.js, __tests__/foo.js, foo_test.go,
|
|
362
|
+
// test/foo.js, tests/foo.js, spec/foo.js (plain test dirs).
|
|
363
|
+
if (/\.(test|spec)\.|[/\\]__tests__[/\\]|_test\.|^tests?[/\\]|[/\\]tests?[/\\]|[/\\]spec[/\\]/.test(chunk.file_path)) {
|
|
364
|
+
if (!queryLower.includes('test') && !queryLower.includes('spec')) baseScore *= 0.25;
|
|
365
|
+
}
|
|
366
|
+
// Demote example/sample/demo/docs directories — prefer canonical lib/src definitions
|
|
367
|
+
if (/^examples?[/\\]|[/\\]examples?[/\\]|^samples?[/\\]|[/\\]samples?[/\\]|^demos?[/\\]|[/\\]demos?[/\\]|^docs?_?src[/\\]|[/\\]docs?_?src[/\\]|^docs?[/\\]|[/\\]tutorials?[/\\]/.test(chunk.file_path)) {
|
|
368
|
+
baseScore *= 0.5;
|
|
324
369
|
}
|
|
325
370
|
// Prefer definitions (functions/classes/impls) over pure usage/expression sites
|
|
326
371
|
if (chunk.node_type === 'expression_statement' || chunk.node_type === 'call_expression') {
|
|
327
372
|
baseScore *= 0.8;
|
|
328
373
|
}
|
|
329
374
|
|
|
375
|
+
// Filename path boost when no vectors available.
|
|
376
|
+
// Also checks if any query token is a prefix of a path segment (≥4 chars),
|
|
377
|
+
// so "initialise" matches "init.js", "decorator" matches "decorators/", etc.
|
|
378
|
+
if (vectorEmpty && queryFileTokens.length > 0) {
|
|
379
|
+
const filePathLower = chunk.file_path.toLowerCase();
|
|
380
|
+
const hasExact = queryFileTokens.some(t => filePathLower.includes(t));
|
|
381
|
+
const hasPrefix = !hasExact && queryFileTokens.some(t =>
|
|
382
|
+
t.length >= 4 && filePathLower.split(/[/\\._-]/).some(seg => seg.startsWith(t.slice(0, 5)))
|
|
383
|
+
);
|
|
384
|
+
if (hasExact) baseScore *= 1.5;
|
|
385
|
+
else if (hasPrefix) baseScore *= 1.25;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Name-exact-token boost (applies in both lexical and hybrid mode):
|
|
389
|
+
// If the chunk name exactly matches any significant query token (≥4 chars),
|
|
390
|
+
// give it an extra 1.4× lift. This promotes WriteJSON over WriteContentType
|
|
391
|
+
// when the query contains "JSON", and GET/POST/Handle over GetPostForm.
|
|
392
|
+
if (chunk.name) {
|
|
393
|
+
const nameLower = chunk.name.toLowerCase();
|
|
394
|
+
const queryTokensAll = queryLower.split(/[\s\W_]+/).filter(t => t.length >= 4);
|
|
395
|
+
if (queryTokensAll.some(t => nameLower === t || nameLower.endsWith('.' + t))) {
|
|
396
|
+
baseScore *= 1.4;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
330
400
|
rrfScores.set(id, (rrfScores.get(id) || 0) + baseScore);
|
|
331
401
|
}
|
|
332
402
|
|
|
@@ -364,7 +434,8 @@ export class MemoryGraphIndex {
|
|
|
364
434
|
id: c.id, file_path: c.file_path, node_type: c.node_type,
|
|
365
435
|
name: c.name, docstring: c.docstring || '', code_snippet: c.code_snippet,
|
|
366
436
|
content_hash: c.content_hash, start_line: c.start_line, end_line: c.end_line,
|
|
367
|
-
calls: c.calls || []
|
|
437
|
+
calls: c.calls || [],
|
|
438
|
+
params: c.params || [], return_type: c.return_type || '', class_context: c.class_context || ''
|
|
368
439
|
}));
|
|
369
440
|
// Embeddings go to a compact binary sidecar — NOT in the JSON payload
|
|
370
441
|
const payload = JSON.stringify({ chunks: chunksData, graph: this.graph });
|
package/indexer.mjs
CHANGED
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
* @description In-Memory Graph Indexer — Bootstrap Engine. Reads .ts/.tsx/.js/.jsx files → Tree-sitter AST → Local Embeddings → code-index.json. Zero external dependencies (only Tree-sitter). No ChromaDB.
|
|
5
5
|
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
6
6
|
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
7
|
-
* @license
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
7
|
+
* @license MIT
|
|
8
|
+
* Copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
9
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
* in the Software without restriction, including without limitation the rights
|
|
12
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
* furnished to do so, subject to the following conditions: The above copyright
|
|
15
|
+
* notice and this permission notice shall be included in all copies or
|
|
16
|
+
* substantial portions of the Software.
|
|
17
17
|
*/
|
|
18
18
|
import fs from 'fs';
|
|
19
19
|
import path from 'path';
|
|
@@ -76,7 +76,7 @@ async function main() {
|
|
|
76
76
|
const parser = getParserForFile(ext);
|
|
77
77
|
if (!parser) continue;
|
|
78
78
|
|
|
79
|
-
let tree = parser.parse(content);
|
|
79
|
+
let tree = parser.parse((offset) => offset < content.length ? content.slice(offset, offset + 4096) : null);
|
|
80
80
|
const rawImports = extractImportsFromAST(tree.rootNode, ext);
|
|
81
81
|
const imports = resolveLocalImports(rawImports, relPath, PROJECT_ROOT);
|
|
82
82
|
indexData.graph.dependencies[relPath] = imports;
|
package/init.mjs
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file init.mjs
|
|
4
|
+
* @description graph-indexer init CLI — auto-configures all detected IDEs/agents.
|
|
5
|
+
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
6
|
+
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
7
|
+
* @license MIT
|
|
8
|
+
* Copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
9
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
* in the Software without restriction, including without limitation the rights
|
|
12
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
* furnished to do so, subject to the following conditions: The above copyright
|
|
15
|
+
* notice and this permission notice shall be included in all copies or
|
|
16
|
+
* substantial portions of the Software.
|
|
17
|
+
*/
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
|
|
23
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const isDryRun = process.argv.includes('--dry-run');
|
|
25
|
+
const PROJECT_ROOT = process.cwd();
|
|
26
|
+
|
|
27
|
+
// ─── MCP Server config block ──────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
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 }
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ─── Utilities ────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function log(msg) { process.stdout.write(msg + '\n'); }
|
|
38
|
+
|
|
39
|
+
function writeFile(filePath, content) {
|
|
40
|
+
if (isDryRun) {
|
|
41
|
+
log(` [dry-run] Would write: ${filePath}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
45
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readJsonSafe(filePath) {
|
|
49
|
+
try {
|
|
50
|
+
if (fs.existsSync(filePath)) {
|
|
51
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
52
|
+
}
|
|
53
|
+
} catch { /* malformed JSON — start fresh */ }
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
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
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return dst;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── IDE Detectors & Configurators ───────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
function configureVSCode() {
|
|
72
|
+
const vscodeDir = path.join(PROJECT_ROOT, '.vscode');
|
|
73
|
+
const configPath = path.join(vscodeDir, 'mcp.json');
|
|
74
|
+
|
|
75
|
+
const existing = readJsonSafe(configPath) || {};
|
|
76
|
+
if (!existing.servers) existing.servers = {};
|
|
77
|
+
if (existing.servers['graph-indexer']) return false; // already configured
|
|
78
|
+
|
|
79
|
+
existing.servers['graph-indexer'] = SERVER_CONFIG;
|
|
80
|
+
writeFile(configPath, JSON.stringify(existing, null, 2) + '\n');
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
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];
|
|
94
|
+
const existing = readJsonSafe(configPath) || {};
|
|
95
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
96
|
+
if (existing.mcpServers['graph-indexer']) return false;
|
|
97
|
+
|
|
98
|
+
existing.mcpServers['graph-indexer'] = SERVER_CONFIG;
|
|
99
|
+
writeFile(configPath, JSON.stringify(existing, null, 2) + '\n');
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function configureClaudeDesktop() {
|
|
104
|
+
let configPath;
|
|
105
|
+
if (process.platform === 'win32') {
|
|
106
|
+
configPath = path.join(process.env.APPDATA || '', 'Claude', 'claude_desktop_config.json');
|
|
107
|
+
} else {
|
|
108
|
+
configPath = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(path.dirname(configPath))) return false; // Claude not installed
|
|
112
|
+
|
|
113
|
+
const existing = readJsonSafe(configPath) || {};
|
|
114
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
115
|
+
if (existing.mcpServers['graph-indexer']) return false;
|
|
116
|
+
|
|
117
|
+
existing.mcpServers['graph-indexer'] = SERVER_CONFIG;
|
|
118
|
+
writeFile(configPath, JSON.stringify(existing, null, 2) + '\n');
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
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
|
+
const hasClaudeDir = fs.existsSync(path.join(PROJECT_ROOT, '.claude'));
|
|
131
|
+
const configPath = hasClaudeDir ? candidates[0] : candidates[1];
|
|
132
|
+
|
|
133
|
+
const existing = readJsonSafe(configPath) || {};
|
|
134
|
+
const serverKey = hasClaudeDir ? 'mcpServers' : 'mcpServers';
|
|
135
|
+
if (!existing[serverKey]) existing[serverKey] = {};
|
|
136
|
+
if (existing[serverKey]['graph-indexer']) return false;
|
|
137
|
+
|
|
138
|
+
existing[serverKey]['graph-indexer'] = SERVER_CONFIG;
|
|
139
|
+
writeFile(configPath, JSON.stringify(existing, null, 2) + '\n');
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── package.json scripts ─────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
function addPackageScripts() {
|
|
146
|
+
const pkgPath = path.join(PROJECT_ROOT, 'package.json');
|
|
147
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
148
|
+
|
|
149
|
+
const pkg = readJsonSafe(pkgPath);
|
|
150
|
+
if (!pkg) return false;
|
|
151
|
+
|
|
152
|
+
const scripts = pkg.scripts || {};
|
|
153
|
+
const toAdd = {
|
|
154
|
+
'mcp:index': 'idx-index --repo .',
|
|
155
|
+
'mcp:watch': 'idx-watch',
|
|
156
|
+
'mcp:start': 'idx-mcp',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let changed = false;
|
|
160
|
+
for (const [k, v] of Object.entries(toAdd)) {
|
|
161
|
+
if (!scripts[k]) { scripts[k] = v; changed = true; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (changed) {
|
|
165
|
+
pkg.scripts = scripts;
|
|
166
|
+
writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
167
|
+
}
|
|
168
|
+
return changed;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─── .gitignore ──────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
function updateGitignore() {
|
|
174
|
+
const gitignorePath = path.join(PROJECT_ROOT, '.gitignore');
|
|
175
|
+
const existing = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
|
|
176
|
+
|
|
177
|
+
const entries = ['code-index.json', 'code-index.embeddings.bin'];
|
|
178
|
+
const toAdd = entries.filter(e => !existing.includes(e));
|
|
179
|
+
if (toAdd.length === 0) return false;
|
|
180
|
+
|
|
181
|
+
const newContent = existing.trimEnd() + '\n\n# graph-indexer runtime artifacts\n' + toAdd.join('\n') + '\n';
|
|
182
|
+
writeFile(gitignorePath, newContent);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
log('\n🔧 graph-indexer init' + (isDryRun ? ' [dry-run]' : '') + '\n');
|
|
189
|
+
log(`📂 Project: ${PROJECT_ROOT}\n`);
|
|
190
|
+
|
|
191
|
+
const configured = [];
|
|
192
|
+
const skipped = [];
|
|
193
|
+
|
|
194
|
+
// IDE configuration
|
|
195
|
+
const ides = [
|
|
196
|
+
{ name: 'VS Code', fn: configureVSCode },
|
|
197
|
+
{ name: 'Cursor', fn: configureCursor },
|
|
198
|
+
{ name: 'Claude Desktop', fn: configureClaudeDesktop },
|
|
199
|
+
{ name: 'Claude Code', fn: configureClaudeCode },
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
log('🔍 Detecting IDEs / agents...\n');
|
|
203
|
+
for (const { name, fn } of ides) {
|
|
204
|
+
try {
|
|
205
|
+
const result = fn();
|
|
206
|
+
if (result === false) {
|
|
207
|
+
skipped.push(name + ' (already configured or not installed)');
|
|
208
|
+
} else {
|
|
209
|
+
configured.push(name);
|
|
210
|
+
}
|
|
211
|
+
} catch (e) {
|
|
212
|
+
skipped.push(name + ' (error: ' + e.message + ')');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Package scripts
|
|
217
|
+
const scriptsAdded = addPackageScripts();
|
|
218
|
+
if (scriptsAdded) configured.push('package.json scripts (mcp:index, mcp:watch, mcp:start)');
|
|
219
|
+
else skipped.push('package.json scripts (already present)');
|
|
220
|
+
|
|
221
|
+
// .gitignore
|
|
222
|
+
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)');
|
|
225
|
+
|
|
226
|
+
// ─── Summary ─────────────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
log('\n✅ Configured:\n');
|
|
229
|
+
if (configured.length) configured.forEach(c => log(' • ' + c));
|
|
230
|
+
else log(' (nothing new to configure)');
|
|
231
|
+
|
|
232
|
+
if (skipped.length) {
|
|
233
|
+
log('\n⏭️ Skipped:\n');
|
|
234
|
+
skipped.forEach(s => log(' • ' + s));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
log('\n📋 Next steps:\n');
|
|
238
|
+
log(' 1. Run `npm run mcp:index` to index this project');
|
|
239
|
+
log(' 2. Restart your IDE to activate MCP servers');
|
|
240
|
+
log(' 3. Run `npm run mcp:start` if your IDE needs the server started manually');
|
|
241
|
+
log('\n');
|
package/mcp-server.mjs
CHANGED
|
@@ -4,16 +4,16 @@
|
|
|
4
4
|
* @description MCP Server to expose in-memory graph index with hybrid search capabilities. Connects to local Ollama instance for embedding generation. Zero external dependencies.
|
|
5
5
|
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
6
6
|
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
7
|
-
* @license
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
7
|
+
* @license MIT
|
|
8
|
+
* Copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
9
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
* in the Software without restriction, including without limitation the rights
|
|
12
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
* furnished to do so, subject to the following conditions: The above copyright
|
|
15
|
+
* notice and this permission notice shall be included in all copies or
|
|
16
|
+
* substantial portions of the Software.
|
|
17
17
|
*/
|
|
18
18
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
19
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -127,7 +127,7 @@ server.resource(
|
|
|
127
127
|
|
|
128
128
|
server.tool(
|
|
129
129
|
"search_code",
|
|
130
|
-
"
|
|
130
|
+
"CRITICAL: ALWAYS USE THIS TOOL FIRST to find code. DO NOT use native file search or grep. High-precision AST search returning exact chunks and topology. Use natural language query.",
|
|
131
131
|
{
|
|
132
132
|
query: z.string().describe("What logic to search for. Example: 'JWT authentication in middleware'"),
|
|
133
133
|
exact_tokens: z.string().optional().describe("Exact variable or function name to boost in results"),
|
|
@@ -205,7 +205,7 @@ server.tool(
|
|
|
205
205
|
|
|
206
206
|
server.tool(
|
|
207
207
|
"get_chunk",
|
|
208
|
-
"
|
|
208
|
+
"CRITICAL: Use this INSTEAD of reading full files to get the body of a function or class. Requires the chunk_id returned by search_code.",
|
|
209
209
|
{ chunk_id: z.string().describe("The chunk ID shown in search_code results (e.g. \"abc123def456\").") },
|
|
210
210
|
async ({ chunk_id }) => {
|
|
211
211
|
try {
|
|
@@ -250,7 +250,7 @@ server.tool(
|
|
|
250
250
|
|
|
251
251
|
if (!parser) return { content: [{ type: "text", text: "Language not supported for AST parsing." }] };
|
|
252
252
|
|
|
253
|
-
const tree = parser.parse(content);
|
|
253
|
+
const tree = parser.parse((offset) => offset < content.length ? content.slice(offset, offset + 4096) : null);
|
|
254
254
|
const skeleton = extractFileSkeleton(tree.rootNode, content);
|
|
255
255
|
|
|
256
256
|
return { content: [{ type: "text", text: `# Skeleton: ${file_path}\n\n${skeleton || "_No semantic signatures found_"}` }] };
|
|
@@ -290,6 +290,83 @@ server.tool(
|
|
|
290
290
|
}
|
|
291
291
|
);
|
|
292
292
|
|
|
293
|
+
server.tool(
|
|
294
|
+
"list_index_stats",
|
|
295
|
+
"Returns the current state of the graph-indexer index: chunk count, file count, embedding status, daemon status, and index freshness. Call this first if search results seem wrong or stale.",
|
|
296
|
+
{},
|
|
297
|
+
async () => {
|
|
298
|
+
try {
|
|
299
|
+
const chunkCount = db.chunks.size;
|
|
300
|
+
const fileSet = new Set();
|
|
301
|
+
for (const chunk of db.chunks.values()) fileSet.add(chunk.file_path);
|
|
302
|
+
const fileCount = fileSet.size;
|
|
303
|
+
|
|
304
|
+
const hasEmbeddings = db.vectors.size > 0;
|
|
305
|
+
const embeddingsEnabled = process.env.INDEXER_EMBEDDINGS !== 'off';
|
|
306
|
+
|
|
307
|
+
let indexAge = 'unknown';
|
|
308
|
+
try {
|
|
309
|
+
const stat = fs.statSync(INDEX_PATH);
|
|
310
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
311
|
+
const ageSec = Math.floor(ageMs / 1000);
|
|
312
|
+
if (ageSec < 60) indexAge = `${ageSec}s ago`;
|
|
313
|
+
else if (ageSec < 3600) indexAge = `${Math.floor(ageSec / 60)}m ago`;
|
|
314
|
+
else indexAge = `${Math.floor(ageSec / 3600)}h ago`;
|
|
315
|
+
} catch { /* file not found */ }
|
|
316
|
+
|
|
317
|
+
let daemonStatus = 'unknown';
|
|
318
|
+
try {
|
|
319
|
+
if (fs.existsSync(PID_FILE)) {
|
|
320
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
|
|
321
|
+
process.kill(pid, 0);
|
|
322
|
+
daemonStatus = `running (PID: ${pid})`;
|
|
323
|
+
} else {
|
|
324
|
+
daemonStatus = 'not running';
|
|
325
|
+
}
|
|
326
|
+
} catch { daemonStatus = 'not running (stale PID)'; }
|
|
327
|
+
|
|
328
|
+
// Extension distribution
|
|
329
|
+
const extCounts = new Map();
|
|
330
|
+
for (const chunk of db.chunks.values()) {
|
|
331
|
+
const ext = chunk.file_path.split('.').pop() || 'unknown';
|
|
332
|
+
extCounts.set(ext, (extCounts.get(ext) || 0) + 1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const searchMode = !embeddingsEnabled
|
|
336
|
+
? '🔤 Lexical only (INDEXER_EMBEDDINGS=off)'
|
|
337
|
+
: hasEmbeddings
|
|
338
|
+
? '🧠 Hybrid (semantic + lexical RRF)'
|
|
339
|
+
: '🔤 Lexical only (Ollama unavailable or not yet indexed)';
|
|
340
|
+
|
|
341
|
+
const lines = [
|
|
342
|
+
`# 📊 graph-indexer Index Stats`,
|
|
343
|
+
``,
|
|
344
|
+
`| Metric | Value |`,
|
|
345
|
+
`| :--- | :--- |`,
|
|
346
|
+
`| **Chunks** | ${chunkCount} |`,
|
|
347
|
+
`| **Files indexed** | ${fileCount} |`,
|
|
348
|
+
`| **Embeddings loaded** | ${db.vectors.size} |`,
|
|
349
|
+
`| **Search mode** | ${searchMode} |`,
|
|
350
|
+
`| **Daemon** | ${daemonStatus} |`,
|
|
351
|
+
`| **Index age** | ${indexAge} |`,
|
|
352
|
+
``,
|
|
353
|
+
`## Extension Breakdown`,
|
|
354
|
+
...Array.from(extCounts.entries())
|
|
355
|
+
.sort((a, b) => b[1] - a[1])
|
|
356
|
+
.map(([ext, count]) => `- .${ext}: ${count} chunks`),
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
if (chunkCount === 0) {
|
|
360
|
+
lines.push(``, `⚠️ Index is empty. Run \`npm run mcp:index\` to build it.`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { content: [{ type: "text", text: lines.join('\n') }] };
|
|
364
|
+
} catch (err) {
|
|
365
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
);
|
|
369
|
+
|
|
293
370
|
const transport = new StdioServerTransport();
|
|
294
371
|
await server.connect(transport);
|
|
295
372
|
process.stderr.write("✅ graph-indexer MCP server running (v1.0.0).\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graph-indexer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
".": "./core-engine.mjs"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
|
+
"graph-indexer": "init.mjs",
|
|
11
12
|
"idx-index": "indexer.mjs",
|
|
12
13
|
"idx-watch": "watch-daemon.mjs",
|
|
13
14
|
"idx-mcp": "mcp-server.mjs"
|
|
@@ -24,7 +25,14 @@
|
|
|
24
25
|
},
|
|
25
26
|
"scripts": {
|
|
26
27
|
"start": "node ./mcp-server.mjs",
|
|
27
|
-
"test": "node
|
|
28
|
+
"test": "node test/run.mjs",
|
|
29
|
+
"test:setup": "node test/setup.mjs",
|
|
30
|
+
"test:run": "node test/run.mjs",
|
|
31
|
+
"test:embeddings": "OLLAMA_HOST=http://localhost:11435 node test/run-embeddings.mjs",
|
|
32
|
+
"test:embeddings:skip": "OLLAMA_HOST=http://localhost:11435 node test/run-embeddings.mjs --skip-indexing",
|
|
33
|
+
"test:suite": "node test/run.mjs --suite",
|
|
34
|
+
"test:json": "node test/run.mjs --json",
|
|
35
|
+
"test:reset": "rm -rf test/fixtures && node test/setup.mjs",
|
|
28
36
|
"mcp:index": "node indexer.mjs --repo .",
|
|
29
37
|
"mcp:watch": "node watch-daemon.mjs",
|
|
30
38
|
"mcp:start": "node mcp-server.mjs"
|
|
@@ -34,11 +42,15 @@
|
|
|
34
42
|
"chokidar": "5.0.0",
|
|
35
43
|
"ignore": "5.3.2",
|
|
36
44
|
"tree-sitter": "0.21.1",
|
|
45
|
+
"tree-sitter-c-sharp": "^0.21.3",
|
|
37
46
|
"tree-sitter-css": "0.21.1",
|
|
38
47
|
"tree-sitter-go": "0.21.0",
|
|
48
|
+
"tree-sitter-java": "^0.21.0",
|
|
39
49
|
"tree-sitter-javascript": "0.21.4",
|
|
50
|
+
"tree-sitter-kotlin": "^0.3.8",
|
|
40
51
|
"tree-sitter-php": "0.23.1",
|
|
41
52
|
"tree-sitter-python": "0.21.0",
|
|
53
|
+
"tree-sitter-ruby": "^0.23.1",
|
|
42
54
|
"tree-sitter-rust": "0.21.0",
|
|
43
55
|
"tree-sitter-typescript": "0.21.2",
|
|
44
56
|
"zod": "3.23.8"
|
|
@@ -60,7 +72,7 @@
|
|
|
60
72
|
"url": "https://github.com/MaquinaTech",
|
|
61
73
|
"email": "nicolopezdelerma@gmail.com"
|
|
62
74
|
},
|
|
63
|
-
"license": "
|
|
75
|
+
"license": "MIT",
|
|
64
76
|
"engines": {
|
|
65
77
|
"node": ">=18.0.0"
|
|
66
78
|
},
|
|
@@ -78,4 +90,4 @@
|
|
|
78
90
|
"optionalDependencies": {
|
|
79
91
|
"hnswlib-node": "^3.0.0"
|
|
80
92
|
}
|
|
81
|
-
}
|
|
93
|
+
}
|