graph-indexer 1.2.0 → 2.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +854 -0
  2. package/README.md +258 -595
  3. package/SECURITY.md +85 -0
  4. package/TRADEMARK.md +31 -0
  5. package/cli-ui.mjs +78 -0
  6. package/colbert.mjs +168 -0
  7. package/config.mjs +408 -32
  8. package/daemon-ctl.mjs +175 -0
  9. package/daemon-lock.mjs +69 -0
  10. package/embedders/python/mlx_embed_server.py +61 -0
  11. package/embedders/python/requirements-mlx.txt +5 -0
  12. package/embedders/setup-mlx.mjs +110 -0
  13. package/embeddings.mjs +571 -0
  14. package/engine/binary.mjs +437 -0
  15. package/{core-engine.mjs → engine/memory.mjs} +321 -441
  16. package/{sqlite-store.mjs → engine/sqlite.mjs} +413 -45
  17. package/enrichment.mjs +206 -32
  18. package/git-signals.mjs +248 -0
  19. package/indexer.mjs +399 -45
  20. package/init.mjs +1626 -171
  21. package/layout.mjs +123 -0
  22. package/mcp/centrality.mjs +117 -0
  23. package/mcp/format.mjs +61 -0
  24. package/mcp/resolver.mjs +116 -0
  25. package/mcp/symbolgraph.mjs +137 -0
  26. package/mcp/taint.mjs +225 -0
  27. package/mcp/tools.mjs +1582 -0
  28. package/mcp/topology.mjs +676 -0
  29. package/mcp-server.mjs +80 -25
  30. package/package.json +66 -27
  31. package/parse/extractor.mjs +628 -0
  32. package/parse/imports.mjs +365 -0
  33. package/parse/interprocedural.mjs +134 -0
  34. package/parse/languages.mjs +253 -0
  35. package/parse/metadata.mjs +701 -0
  36. package/parse/routes.mjs +327 -0
  37. package/parse/scip.mjs +391 -0
  38. package/parse/taint-patterns.mjs +119 -0
  39. package/prompts/CORE.md +91 -0
  40. package/prompts/DOMAIN_TEMPLATE.md +46 -0
  41. package/prompts/INTEGRATION.md +123 -0
  42. package/prompts/frameworks/ANDROID.md +17 -0
  43. package/prompts/frameworks/ASPNET_CORE.md +19 -0
  44. package/prompts/frameworks/FASTAPI_DJANGO.md +20 -0
  45. package/prompts/frameworks/LARAVEL_SYMFONY.md +22 -0
  46. package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +19 -0
  47. package/prompts/frameworks/RAILS.md +20 -0
  48. package/prompts/frameworks/REACT.md +20 -0
  49. package/prompts/frameworks/SPRING_BOOT.md +20 -0
  50. package/prompts/languages/BASH.md +22 -0
  51. package/prompts/languages/C.md +21 -0
  52. package/prompts/languages/CSHARP.md +24 -0
  53. package/prompts/languages/CSS_SCSS.md +22 -0
  54. package/prompts/languages/GO.md +24 -0
  55. package/prompts/languages/JAVA.md +23 -0
  56. package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +24 -0
  57. package/prompts/languages/KOTLIN.md +24 -0
  58. package/prompts/languages/PHP.md +23 -0
  59. package/prompts/languages/PYTHON.md +23 -0
  60. package/prompts/languages/RUBY.md +23 -0
  61. package/prompts/languages/RUST.md +21 -0
  62. package/prompts/languages/SWIFT.md +21 -0
  63. package/seal.mjs +400 -0
  64. package/search-core.mjs +405 -44
  65. package/search-sparse.mjs +183 -0
  66. package/storage.mjs +33 -3
  67. package/watch-daemon.mjs +75 -24
  68. package/PROMPT.md +0 -182
  69. package/mcp-tools.mjs +0 -545
  70. package/parser-utils.mjs +0 -1170
package/mcp-server.mjs CHANGED
@@ -3,9 +3,9 @@
3
3
  * @file mcp-server.mjs
4
4
  * @description MCP server bootstrap. Resolves configuration, selects the storage
5
5
  * backend (in-memory by default, SQLite when configured), registers
6
- * the tool surface (mcp-tools.mjs) and connects over stdio. All
6
+ * the tool surface (mcp/tools.mjs) and connects over stdio. All
7
7
  * retrieval logic lives in the store + search-core; all tool logic in
8
- * mcp-tools — this file is wiring only.
8
+ * mcp/tools.mjs — this file is wiring only.
9
9
  * @author MaquinaTech <https://github.com/MaquinaTech>
10
10
  * @copyright (c) 2026 MaquinaTech. All rights reserved.
11
11
  * @license MIT
@@ -13,18 +13,47 @@
13
13
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
14
14
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
15
15
  import fs from 'fs';
16
- import path, { resolve } from 'path';
16
+ import path from 'path';
17
17
  import { fileURLToPath } from 'url';
18
18
  import { spawn } from 'child_process';
19
- import { resolveConfig } from './config.mjs';
19
+ import { resolveConfig, describeConfig, configNotices } from './config.mjs';
20
+ import { ensureDataDir, migrateLegacyLayout } from './layout.mjs';
21
+ import { daemonStatus } from './daemon-lock.mjs';
20
22
  import { createStore } from './storage.mjs';
21
- import { registerTools } from './mcp-tools.mjs';
23
+ import { registerTools } from './mcp/tools.mjs';
24
+ import { ensureLanguagesReady } from './parse/languages.mjs';
25
+ import { createEmbedder, readEmbedMeta } from './embeddings.mjs';
26
+ import { loadGitSignals } from './git-signals.mjs';
27
+ import { installEgressGuard } from './seal.mjs';
22
28
 
23
- const config = resolveConfig();
29
+ let config;
30
+ try {
31
+ config = resolveConfig();
32
+ } catch (err) {
33
+ if (err && err.name === 'SealViolation') { process.stderr.write(`🔒 ${err.message}\n`); process.exit(2); }
34
+ throw err;
35
+ }
36
+ // F1: install the deny-by-default egress guard before the store / embedder / providers load.
37
+ if (config.sealed !== 'off') {
38
+ installEgressGuard({ allow: config.sealed === 'local' ? ['loopback'] : [] });
39
+ }
24
40
  const PROJECT_ROOT = config.projectRoot;
25
- const PID_FILE = resolve(PROJECT_ROOT, '.idx-daemon.pid');
41
+ const PID_FILE = config.pidFile;
26
42
  const PACKAGE_DIR = path.dirname(fileURLToPath(import.meta.url));
27
43
 
44
+ ensureDataDir(PROJECT_ROOT);
45
+ migrateLegacyLayout(PROJECT_ROOT);
46
+
47
+ // get_file_skeleton parses on demand at query time — grammars must be resolved
48
+ // (and, outside sealed mode, installed if this server is starting before the
49
+ // first `idx-index` run) before the transport connects and starts serving tools.
50
+ await ensureLanguagesReady({
51
+ projectRoot: PROJECT_ROOT,
52
+ enabledLangs: config.languages,
53
+ autoInstall: config.sealed === 'off',
54
+ log: (msg) => process.stderr.write(msg + '\n'),
55
+ });
56
+
28
57
  function readPackageVersion() {
29
58
  try {
30
59
  return JSON.parse(fs.readFileSync(path.join(PACKAGE_DIR, 'package.json'), 'utf-8')).version || '0.0.0';
@@ -38,16 +67,17 @@ function readPackageVersion() {
38
67
  // PRAGMA data_version). Either way, a long-running MCP server stays consistent
39
68
  // with the working tree without restarts or full re-indexes.
40
69
  function ensureDaemonRunning() {
41
- if (fs.existsSync(PID_FILE)) {
42
- const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8'), 10);
43
- try { process.kill(pid, 0); process.stderr.write(`✅ Daemon already active (PID: ${pid}).\n`); return; }
44
- catch { fs.unlinkSync(PID_FILE); }
45
- }
70
+ // The daemon owns the PID lock exclusively (it acquires it atomically on
71
+ // startup — see daemon-lock.mjs). We only check whether one is already live;
72
+ // if not, we spawn it detached. Even if two servers race here, the losing
73
+ // daemon fails to acquire the lock and exits, so never more than one runs.
74
+ const { running, pid } = daemonStatus(PID_FILE);
75
+ if (running) { process.stderr.write(`✅ Daemon already active (PID: ${pid}).\n`); return; }
76
+
46
77
  const daemonPath = path.join(PACKAGE_DIR, 'watch-daemon.mjs');
47
- const logPath = path.join(PROJECT_ROOT, '.idx-daemon.log');
48
- process.stderr.write(`🚀 Starting Watcher Daemon...\n Log: ${logPath}\n`);
78
+ process.stderr.write(`🚀 Starting Watcher Daemon...\n Log: ${config.logFile}\n`);
49
79
  let logFd;
50
- try { logFd = fs.openSync(logPath, 'a'); } catch { logFd = null; }
80
+ try { logFd = fs.openSync(config.logFile, 'a'); } catch { logFd = null; }
51
81
  const child = spawn(process.execPath, [daemonPath], {
52
82
  detached: true,
53
83
  stdio: logFd !== null ? ['ignore', logFd, logFd] : 'ignore',
@@ -55,27 +85,29 @@ function ensureDaemonRunning() {
55
85
  });
56
86
  child.unref();
57
87
  if (logFd !== null) fs.closeSync(logFd);
58
- fs.writeFileSync(PID_FILE, child.pid.toString());
59
88
  }
60
89
 
61
90
  // ─── Boot ──────────────────────────────────────────────────────────────────────
62
91
 
63
- if (config.storage === 'sqlite') {
64
- process.stderr.write('🗄 Storage backend: SQLite (disk-backed, live daemon updates).\n');
65
- }
66
92
  ensureDaemonRunning();
67
93
 
68
94
  const version = readPackageVersion();
69
95
  const server = new McpServer({ name: 'graph-indexer', version });
70
96
 
71
97
  const db = await createStore(config, { cacheEmbeddings: false });
98
+ const backend = db.backend;
99
+
100
+ process.stderr.write('⚙️ Effective configuration:\n');
101
+ for (const line of describeConfig(config, { backend })) process.stderr.write(` ${line}\n`);
102
+ for (const notice of configNotices(config)) process.stderr.write(`⚠️ ${notice}\n`);
103
+
72
104
  try { db.load(); } catch (err) { process.stderr.write(`⏳ Waiting for initial indexing… (${err.message})\n`); }
73
105
 
74
106
  // In-memory backend: the daemon is a separate process that rewrites
75
107
  // code-index.json — without reloading, this server would answer from a stale
76
108
  // snapshot until restart. (The SQLite store refreshes itself per query via
77
109
  // PRAGMA data_version, so no watcher is needed there.)
78
- if (config.storage !== 'sqlite' && typeof db.reload === 'function') {
110
+ if (backend !== 'sqlite' && typeof db.reload === 'function') {
79
111
  let reloadTimer = null;
80
112
  const scheduleReload = () => {
81
113
  if (reloadTimer) clearTimeout(reloadTimer);
@@ -126,17 +158,40 @@ server.resource(
126
158
  }
127
159
  );
128
160
 
161
+ // ─── Query embedder ────────────────────────────────────────────────────────────
162
+ // Query with the SAME provider/model the index was built with (stamped in the
163
+ // embeddings-bin sidecar). No stamp → an index from an older version: keep the
164
+ // legacy behaviour (try Ollama, fall back to lexical). The local pipeline loads
165
+ // lazily on first query, so an unused provider costs nothing at startup.
166
+ const embedMeta = readEmbedMeta(config.embeddingPath);
167
+ const embedder = await createEmbedder(
168
+ config,
169
+ embedMeta?.provider
170
+ ? { provider: embedMeta.provider, model: embedMeta.model }
171
+ : { provider: config.embeddingsEnabled ? 'ollama' : 'off', model: config.embedModel }
172
+ );
173
+
174
+ // ─── Git signals (air-gapped sidecar; blast-radius hint + opt-in rank boost) ────
175
+ const gitSignals = config.gitSignals ? loadGitSignals(config.gitSignalsPath) : null;
176
+
129
177
  // ─── Tools ───────────────────────────────────────────────────────────────────
130
178
  registerTools(server, db, {
131
179
  projectRoot: PROJECT_ROOT,
132
- artifactPath: config.storage === 'sqlite' ? config.sqlitePath : config.indexPath,
180
+ artifactPath: backend === 'sqlite' ? config.sqlitePath : config.indexPath,
133
181
  pidFile: PID_FILE,
134
- embeddingsEnabled: config.embeddingsEnabled,
135
- ollamaHost: config.ollamaHost,
136
- embedModel: config.embedModel,
182
+ embeddingsEnabled: config.embeddingsEnabled && embedder.provider !== 'off',
183
+ embedder,
137
184
  rerank: config.rerank,
185
+ hyde: config.hyde,
186
+ ollamaHost: config.ollamaHost,
187
+ llmProvider: config.llmProvider,
188
+ mlxLmHost: config.mlxLmHost,
189
+ gitSignals,
190
+ gitRankBoost: config.gitRankBoost,
191
+ sealed: config.sealed,
192
+ ranker: config.ranker,
138
193
  });
139
194
 
140
195
  const transport = new StdioServerTransport();
141
196
  await server.connect(transport);
142
- process.stderr.write(`✅ graph-indexer MCP server running (v${version}, ${config.storage} backend).\n`);
197
+ process.stderr.write(`✅ graph-indexer MCP server running (v${version}, ${backend} backend).\n`);
package/package.json CHANGED
@@ -1,22 +1,33 @@
1
1
  {
2
2
  "name": "graph-indexer",
3
- "version": "1.2.0",
3
+ "version": "2.1.0",
4
4
  "description": "Air-gapped code-retrieval MCP server: AST chunking, hybrid RRF search and cross-language call graph, with an optional zero-dependency SQLite backend for monorepo scale and optional local-LLM semantic enrichment.",
5
5
  "type": "module",
6
- "main": "core-engine.mjs",
6
+ "main": "engine/memory.mjs",
7
7
  "exports": {
8
- ".": "./core-engine.mjs"
8
+ ".": "./engine/memory.mjs"
9
9
  },
10
10
  "bin": {
11
11
  "graph-indexer": "init.mjs",
12
12
  "idx-index": "indexer.mjs",
13
13
  "idx-watch": "watch-daemon.mjs",
14
+ "idx-daemon": "daemon-ctl.mjs",
14
15
  "idx-mcp": "mcp-server.mjs"
15
16
  },
16
17
  "files": [
17
18
  "*.mjs",
18
- "PROMPT.md",
19
+ "engine/",
20
+ "mcp/",
21
+ "parse/",
22
+ "embedders/setup-mlx.mjs",
23
+ "embedders/python/mlx_embed_server.py",
24
+ "embedders/python/requirements-mlx.txt",
25
+ "!embedders/python/test_servers.mjs",
26
+ "prompts/",
19
27
  "README.md",
28
+ "CHANGELOG.md",
29
+ "SECURITY.md",
30
+ "TRADEMARK.md",
20
31
  "LICENSE"
21
32
  ],
22
33
  "repository": {
@@ -31,28 +42,70 @@
31
42
  "test:enrich": "node test/enrichment.mjs",
32
43
  "test:mcp": "node test/mcp.mjs",
33
44
  "test:scale": "node test/scale.mjs",
34
- "test:all": "node test/unit.mjs && node test/sqlite.mjs && node test/enrichment.mjs && node test/mcp.mjs && node test/scale.mjs",
45
+ "test:callgraph": "node test/callgraph.mjs",
46
+ "test:edges": "node test/edges.mjs",
47
+ "test:references": "node test/references.mjs",
48
+ "test:routes": "node test/routes.mjs",
49
+ "test:jsonout": "node test/json-output.mjs",
50
+ "test:gitsignals": "node test/git-signals.mjs",
51
+ "test:security": "node test/security.mjs",
52
+ "test:seal": "node test/seal.mjs",
53
+ "test:taint": "node test/taint.mjs",
54
+ "test:scip": "node test/scip.mjs",
55
+ "test:ranker": "node test/ranker.mjs",
56
+ "test:sparse": "node test/sparse.mjs",
57
+ "test:colbert": "node test/colbert.mjs",
58
+ "test:embed": "node test/embeddings.mjs",
59
+ "test:languages": "node test/languages.mjs",
60
+ "test:all": "node test/unit.mjs && node test/sqlite.mjs && node test/enrichment.mjs && node test/mcp.mjs && node test/scale.mjs && node test/callgraph.mjs && node test/edges.mjs && node test/references.mjs && node test/routes.mjs && node test/json-output.mjs && node test/git-signals.mjs && node test/security.mjs && node test/seal.mjs && node test/taint.mjs && node test/scip.mjs && node test/ranker.mjs && node test/sparse.mjs && node test/colbert.mjs && node test/embeddings.mjs && node test/languages.mjs",
35
61
  "test:eval": "node test/evaluate.mjs",
36
- "test:eval:embeddings": "OLLAMA_HOST=http://localhost:11435 node test/evaluate.mjs --embeddings",
62
+ "test:eval:embeddings": "OLLAMA_HOST=http://localhost:11434 node test/evaluate.mjs --embeddings",
37
63
  "test:setup": "node test/setup.mjs",
38
64
  "test:run": "node test/run.mjs",
39
- "test:embeddings": "OLLAMA_HOST=http://localhost:11435 node test/run-embeddings.mjs",
40
- "test:embeddings:skip": "OLLAMA_HOST=http://localhost:11435 node test/run-embeddings.mjs --skip-indexing",
65
+ "test:embeddings": "OLLAMA_HOST=http://localhost:11434 node test/run-embeddings.mjs",
66
+ "test:embeddings:skip": "OLLAMA_HOST=http://localhost:11434 node test/run-embeddings.mjs --skip-indexing",
41
67
  "test:suite": "node test/run.mjs --suite",
42
68
  "test:json": "node test/run.mjs --json",
43
69
  "test:reset": "rm -rf test/fixtures && node test/setup.mjs",
44
- "mcp:index": "node indexer.mjs --repo .",
70
+ "embed:setup:mlx": "node embedders/setup-mlx.mjs",
71
+ "embed:setup:local": "npm install @huggingface/transformers",
72
+ "mcp:index": "idx-index --repo .",
45
73
  "mcp:index:sqlite": "node indexer.mjs --repo . --use-sqlite",
46
- "mcp:index:enriched": "node indexer.mjs --repo . --llm-enrichment",
74
+ "mcp:index:enriched": "node indexer.mjs --repo . --enrichment",
47
75
  "mcp:watch": "node watch-daemon.mjs",
48
- "mcp:start": "node mcp-server.mjs"
76
+ "mcp:start": "idx-mcp",
77
+ "mcp:daemon": "node daemon-ctl.mjs status",
78
+ "mcp:daemon:start": "node daemon-ctl.mjs start",
79
+ "mcp:daemon:stop": "node daemon-ctl.mjs stop",
80
+ "mcp:daemon:restart": "node daemon-ctl.mjs restart",
81
+ "mcp:daemon:status": "node daemon-ctl.mjs status",
82
+ "mcp:daemon:logs": "node daemon-ctl.mjs logs"
49
83
  },
50
84
  "dependencies": {
51
85
  "@modelcontextprotocol/sdk": "1.29.0",
52
86
  "chokidar": "5.0.0",
53
87
  "ignore": "5.3.2",
54
88
  "tree-sitter": "0.21.1",
55
- "zod": "3.23.8"
89
+ "zod": "^3.25.76"
90
+ },
91
+ "devDependencies": {
92
+ "@huggingface/transformers": "3.8.1",
93
+ "hnswlib-node": "3.0.0",
94
+ "tree-sitter-bash": "0.23.3",
95
+ "tree-sitter-c": "0.21.4",
96
+ "tree-sitter-c-sharp": "0.21.3",
97
+ "tree-sitter-css": "0.21.1",
98
+ "tree-sitter-go": "0.21.0",
99
+ "tree-sitter-java": "0.21.0",
100
+ "tree-sitter-javascript": "0.21.4",
101
+ "tree-sitter-kotlin": "0.3.8",
102
+ "tree-sitter-php": "0.23.1",
103
+ "tree-sitter-python": "0.21.0",
104
+ "tree-sitter-ruby": "0.23.1",
105
+ "tree-sitter-rust": "0.21.0",
106
+ "tree-sitter-scss": "1.0.0",
107
+ "tree-sitter-swift": "0.5.0",
108
+ "tree-sitter-typescript": "0.21.2"
56
109
  },
57
110
  "keywords": [
58
111
  "mcp",
@@ -85,19 +138,5 @@
85
138
  },
86
139
  "publishConfig": {
87
140
  "access": "public"
88
- },
89
- "optionalDependencies": {
90
- "hnswlib-node": "3.0.0",
91
- "tree-sitter-c-sharp": "0.21.3",
92
- "tree-sitter-css": "0.21.1",
93
- "tree-sitter-go": "0.21.0",
94
- "tree-sitter-java": "0.21.0",
95
- "tree-sitter-javascript": "0.21.4",
96
- "tree-sitter-kotlin": "0.3.8",
97
- "tree-sitter-php": "0.23.1",
98
- "tree-sitter-python": "0.21.0",
99
- "tree-sitter-ruby": "0.23.1",
100
- "tree-sitter-rust": "0.21.0",
101
- "tree-sitter-typescript": "0.21.2"
102
141
  }
103
- }
142
+ }