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/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 GPL-3.0-only
7
- * * This program is free software: you can redistribute it and/or modify
8
- * it under the terms of the GNU General Public License as published by
9
- * the Free Software Foundation, either version 3 of the License, or
10
- * (at your option) any later version.
11
- *
12
- * This program is distributed in the hope that it will be useful,
13
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- * GNU General Public License for more details.
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 = `${chunk.name} ${chunk.docstring || ''} ${cleanDeps.join(' ')} ${(chunk.calls || []).join(' ')} ${chunk.code_snippet}`;
144
- this._indexLexical(chunk.id, enrichedContext);
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
- for (const { id, rank } of allResults) {
317
- let baseScore = 1 / (K + rank);
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
- if (/\.(test|spec)\.|[/\\]__tests__[/\\]|_test\./.test(chunk.file_path)) {
323
- if (!queryLower.includes('test') && !queryLower.includes('spec')) baseScore *= 0.6;
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 GPL-3.0-only
8
- * * This program is free software: you can redistribute it and/or modify
9
- * it under the terms of the GNU General Public License as published by
10
- * the Free Software Foundation, either version 3 of the License, or
11
- * (at your option) any later version.
12
- *
13
- * This program is distributed in the hope that it will be useful,
14
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
- * GNU General Public License for more details.
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 GPL-3.0-only
8
- * * This program is free software: you can redistribute it and/or modify
9
- * it under the terms of the GNU General Public License as published by
10
- * the Free Software Foundation, either version 3 of the License, or
11
- * (at your option) any later version.
12
- *
13
- * This program is distributed in the hope that it will be useful,
14
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
- * GNU General Public License for more details.
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
- "Hybrid semantic + lexical search (RRF). Returns compact signature cards for all results and fills remaining token budget with code bodies. Use get_chunk(id) to expand any result.",
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
- "Returns the full source body of a specific indexed code chunk by ID. Use AFTER search_code when you need more than the signature.",
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.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 tests/eval-harness.mjs --index ./code-index.json",
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": "GPL-3.0-only",
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
+ }