graph-indexer 1.2.0 → 2.0.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 (60) hide show
  1. package/CHANGELOG.md +412 -0
  2. package/README.md +216 -596
  3. package/SECURITY.md +68 -0
  4. package/TRADEMARK.md +31 -0
  5. package/cli-ui.mjs +78 -0
  6. package/config.mjs +179 -28
  7. package/daemon-ctl.mjs +175 -0
  8. package/daemon-lock.mjs +69 -0
  9. package/embedders/python/mlx_embed_server.py +61 -0
  10. package/embedders/python/requirements-mlx.txt +5 -0
  11. package/embedders/setup-mlx.mjs +110 -0
  12. package/embeddings.mjs +489 -0
  13. package/engine/binary.mjs +399 -0
  14. package/{core-engine.mjs → engine/memory.mjs} +107 -440
  15. package/{sqlite-store.mjs → engine/sqlite.mjs} +137 -42
  16. package/enrichment.mjs +111 -32
  17. package/git-signals.mjs +248 -0
  18. package/indexer.mjs +151 -42
  19. package/init.mjs +1603 -171
  20. package/layout.mjs +123 -0
  21. package/mcp/format.mjs +61 -0
  22. package/mcp/tools.mjs +1157 -0
  23. package/mcp/topology.mjs +551 -0
  24. package/mcp-server.mjs +55 -24
  25. package/package.json +44 -13
  26. package/parse/extractor.mjs +617 -0
  27. package/parse/imports.mjs +365 -0
  28. package/parse/languages.mjs +89 -0
  29. package/parse/metadata.mjs +655 -0
  30. package/parse/routes.mjs +327 -0
  31. package/prompts/CORE.md +86 -0
  32. package/prompts/DOMAIN_TEMPLATE.md +46 -0
  33. package/prompts/INTEGRATION.md +123 -0
  34. package/prompts/frameworks/ANDROID.md +17 -0
  35. package/prompts/frameworks/ASPNET_CORE.md +19 -0
  36. package/prompts/frameworks/FASTAPI_DJANGO.md +19 -0
  37. package/prompts/frameworks/LARAVEL_SYMFONY.md +22 -0
  38. package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +18 -0
  39. package/prompts/frameworks/RAILS.md +20 -0
  40. package/prompts/frameworks/REACT.md +20 -0
  41. package/prompts/frameworks/SPRING_BOOT.md +19 -0
  42. package/prompts/languages/BASH.md +22 -0
  43. package/prompts/languages/C.md +21 -0
  44. package/prompts/languages/CSHARP.md +24 -0
  45. package/prompts/languages/CSS_SCSS.md +22 -0
  46. package/prompts/languages/GO.md +23 -0
  47. package/prompts/languages/JAVA.md +23 -0
  48. package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +24 -0
  49. package/prompts/languages/KOTLIN.md +24 -0
  50. package/prompts/languages/PHP.md +23 -0
  51. package/prompts/languages/PYTHON.md +23 -0
  52. package/prompts/languages/RUBY.md +23 -0
  53. package/prompts/languages/RUST.md +21 -0
  54. package/prompts/languages/SWIFT.md +21 -0
  55. package/search-core.mjs +254 -43
  56. package/storage.mjs +28 -3
  57. package/watch-daemon.mjs +69 -23
  58. package/PROMPT.md +0 -182
  59. package/mcp-tools.mjs +0 -545
  60. package/parser-utils.mjs +0 -1170
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @file daemon-lock.mjs
3
+ * @description Single-instance guard for the watch daemon. One — and only one —
4
+ * daemon may run per project, regardless of how it was launched
5
+ * (auto-spawned by the MCP server, `idx-daemon start`, or `idx-watch`
6
+ * directly). The guard is a PID lock file owned exclusively by the
7
+ * live daemon: acquisition is atomic (O_EXCL create) and tolerant of
8
+ * stale locks left by a crashed process.
9
+ * @author MaquinaTech <https://github.com/MaquinaTech>
10
+ * @copyright (c) 2026 MaquinaTech. All rights reserved.
11
+ * @license MIT
12
+ */
13
+ import fs from 'fs';
14
+
15
+ /** Read a PID from a lock file, or null if absent/garbage. */
16
+ export function readPid(pidFile) {
17
+ try {
18
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
19
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
20
+ } catch { return null; }
21
+ }
22
+
23
+ /** True if a process with this PID currently exists. */
24
+ export function isAlive(pid) {
25
+ if (!pid) return false;
26
+ try { process.kill(pid, 0); return true; }
27
+ catch (err) { return err.code === 'EPERM'; } // exists but owned by another user
28
+ }
29
+
30
+ /**
31
+ * Inspect the lock without acquiring it.
32
+ * @returns {{running:boolean, pid:number|null}}
33
+ */
34
+ export function daemonStatus(pidFile) {
35
+ const pid = readPid(pidFile);
36
+ return { running: isAlive(pid), pid: isAlive(pid) ? pid : null };
37
+ }
38
+
39
+ /**
40
+ * Atomically claim the daemon lock for the current process. A stale lock (PID no
41
+ * longer alive) is cleared and retried; a lock held by a *live* daemon is left
42
+ * untouched.
43
+ *
44
+ * @returns {boolean} true if this process now owns the lock; false if another
45
+ * live daemon already holds it (caller should exit).
46
+ */
47
+ export function acquireLock(pidFile) {
48
+ for (let attempt = 0; attempt < 3; attempt++) {
49
+ try {
50
+ const fd = fs.openSync(pidFile, 'wx'); // O_EXCL: fails if the file exists
51
+ try { fs.writeSync(fd, String(process.pid)); } finally { fs.closeSync(fd); }
52
+ return true;
53
+ } catch (err) {
54
+ if (err.code !== 'EEXIST') throw err;
55
+ const pid = readPid(pidFile);
56
+ if (pid === process.pid) return true;
57
+ if (isAlive(pid)) return false;
58
+ try { fs.unlinkSync(pidFile); } catch { /* another racer cleared it — retry */ }
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+
64
+ /** Release the lock, but only if this process owns it (never steal another's). */
65
+ export function releaseLock(pidFile) {
66
+ if (readPid(pidFile) === process.pid) {
67
+ try { fs.unlinkSync(pidFile); } catch { }
68
+ }
69
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MLX Embedder Server — stdin/stdout JSON Lines protocol.
4
+
5
+ Input: {"texts": ["text1", "text2", ...]} (batch, up to 32)
6
+ Output: {"embeddings": [[...], [...]]} or {"error": "message"}
7
+
8
+ Prints "READY\n" to stdout once the model is loaded.
9
+ Logs to stderr only (stdout is reserved for the JSON protocol).
10
+
11
+ The model id is passed by the Node side as argv[1] (default below). Whatever id is
12
+ loaded here is also what the Node side stamps into the index meta sidecar, so the
13
+ {provider, model, dim} always reflects the exact model that produced the vectors.
14
+ Override it with --mlx-embed-model / INDEXER_MLX_EMBED_MODEL on the graph-indexer side.
15
+ """
16
+ import sys, json
17
+ import numpy as np
18
+
19
+ DEFAULT_MODEL_ID = "mlx-community/all-MiniLM-L6-v2-4bit"
20
+ MODEL_ID = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1].strip() else DEFAULT_MODEL_ID
21
+
22
+
23
+ def load_model():
24
+ try:
25
+ from mlx_embeddings.utils import load
26
+ model, tokenizer = load(MODEL_ID)
27
+ return model, tokenizer
28
+ except ImportError as e:
29
+ print(json.dumps({"error": f"mlx_embeddings not installed: {e}"}), flush=True)
30
+ sys.exit(1)
31
+ except Exception as e:
32
+ print(json.dumps({"error": f"model load failed: {e}"}), flush=True)
33
+ sys.exit(1)
34
+
35
+
36
+ def embed_batch(model, tokenizer, texts):
37
+ # Empty / falsy texts → "" so the tokenizer never sees None.
38
+ texts = [t[:8000] if t else "" for t in texts]
39
+ inputs = tokenizer.batch_encode_plus(
40
+ texts, return_tensors="mlx",
41
+ padding=True, truncation=True, max_length=512
42
+ )
43
+ out = model(inputs["input_ids"], attention_mask=inputs.get("attention_mask"))
44
+ vecs = out.text_embeds # already mean-pooled and L2-normalized
45
+ return np.asarray(vecs, dtype=np.float32).tolist()
46
+
47
+
48
+ model, tokenizer = load_model()
49
+ print("READY", flush=True)
50
+
51
+ for line in sys.stdin:
52
+ line = line.strip()
53
+ if not line:
54
+ continue
55
+ try:
56
+ req = json.loads(line)
57
+ texts = req.get("texts", [])
58
+ embeddings = embed_batch(model, tokenizer, texts)
59
+ print(json.dumps({"embeddings": embeddings}), flush=True)
60
+ except Exception as e:
61
+ print(json.dumps({"error": str(e)}), flush=True)
@@ -0,0 +1,5 @@
1
+ # Apple Silicon only (macOS darwin).
2
+ # Install: pip install -r requirements-mlx.txt --break-system-packages
3
+ mlx>=0.21.0
4
+ mlx-embeddings>=0.0.3
5
+ numpy>=1.24.0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @file embedders/setup-mlx.mjs
3
+ * @description Lifecycle for the optional MLX (Apple Metal) embedder's dedicated Python
4
+ * virtualenv. graph-indexer runs the MLX server under `embedders/venv-mlx`
5
+ * rather than the system Python so its (large) deps never pollute the host
6
+ * and the interpreter is found by absolute path regardless of `$PATH`.
7
+ *
8
+ * Exposes the venv interpreter path + a readiness probe to embeddings.mjs,
9
+ * and a one-shot `ensureMlxEnv()` that creates the venv and pip-installs
10
+ * requirements-mlx.txt into it. Runnable as a CLI (`npm run embed:setup:mlx`)
11
+ * so users can pre-provision the environment with a single command. This
12
+ * module intentionally does NOT import embeddings.mjs (avoids a cycle).
13
+ *
14
+ * @author MaquinaTech <https://github.com/MaquinaTech>
15
+ * @copyright (c) 2026 MaquinaTech. All rights reserved.
16
+ * @license MIT
17
+ */
18
+ import fs from 'fs';
19
+ import { spawnSync } from 'child_process';
20
+ import { fileURLToPath } from 'url';
21
+ import { dirname, join } from 'path';
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url)); // embedders/
24
+ export const mlxVenvDir = join(__dirname, 'venv-mlx');
25
+ const REQUIREMENTS = join(__dirname, 'python', 'requirements-mlx.txt');
26
+ // Candidate base interpreters to bootstrap the venv, newest first. MLX needs Python
27
+ // 3.9+; preferring a modern one avoids a stale system python (e.g. macOS 3.9).
28
+ const BASE_PYTHON_CANDIDATES = ['python3.12', 'python3.11', 'python3.10', 'python3'];
29
+
30
+ // venv layout is POSIX (bin/python3). MLX is macOS-only, so we never need Windows Scripts/.
31
+ function venvPythonPath() { return join(mlxVenvDir, 'bin', 'python3'); }
32
+
33
+ /** Absolute path to the venv interpreter, or bare 'python3' if the venv is absent. */
34
+ export function mlxVenvPython() {
35
+ const p = venvPythonPath();
36
+ return fs.existsSync(p) ? p : 'python3';
37
+ }
38
+
39
+ /** True iff the venv exists AND `import mlx, mlx_embeddings` succeeds under it. */
40
+ export function mlxEnvReady() {
41
+ const p = venvPythonPath();
42
+ if (!fs.existsSync(p)) return false;
43
+ const r = spawnSync(p, ['-c', 'import mlx, mlx_embeddings'], { stdio: 'ignore', timeout: 30000 });
44
+ return r.status === 0;
45
+ }
46
+
47
+ /** First base interpreter on PATH that runs, to seed the venv (newest preferred). */
48
+ function pickBasePython() {
49
+ for (const c of BASE_PYTHON_CANDIDATES) {
50
+ const r = spawnSync(c, ['--version'], { stdio: 'ignore' });
51
+ if (r.status === 0) return c;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Ensure the MLX embedder venv exists and its deps are importable.
58
+ *
59
+ * @param {object} [opts]
60
+ * @param {boolean} [opts.autoInstall=true] When false, only report readiness (no side effects).
61
+ * @param {(s:string)=>void} [opts.log] Progress sink (defaults to no-op).
62
+ * @returns {{ready:boolean, created:boolean, error:(string|null)}}
63
+ */
64
+ export function ensureMlxEnv({ autoInstall = true, log = () => {} } = {}) {
65
+ if (process.platform !== 'darwin') {
66
+ return { ready: false, created: false, error: 'MLX requires macOS (Apple Silicon).' };
67
+ }
68
+ if (mlxEnvReady()) { log('MLX environment already present.'); return { ready: true, created: false, error: null }; }
69
+ if (!autoInstall) {
70
+ return { ready: false, created: false, error: 'MLX environment not ready — run `npm run embed:setup:mlx`.' };
71
+ }
72
+
73
+ let created = false;
74
+ const venvPy = venvPythonPath();
75
+ if (!fs.existsSync(venvPy)) {
76
+ const base = pickBasePython();
77
+ if (!base) return { ready: false, created: false, error: 'No python3 found on PATH to create the venv.' };
78
+ log(`creating virtualenv (${base} -m venv embedders/venv-mlx)`);
79
+ const mk = spawnSync(base, ['-m', 'venv', mlxVenvDir], { stdio: 'inherit' });
80
+ if (mk.status !== 0) return { ready: false, created: false, error: 'virtualenv creation failed.' };
81
+ created = true;
82
+ }
83
+
84
+ log('installing MLX deps (pip install -r embedders/python/requirements-mlx.txt)');
85
+ // Upgrade pip first so wheels resolve cleanly on older venv seeds; ignore its exit
86
+ // code (a failed upgrade is non-fatal — the install below is what matters).
87
+ spawnSync(venvPy, ['-m', 'pip', 'install', '-q', '--upgrade', 'pip'], { stdio: 'inherit' });
88
+ const pip = spawnSync(venvPy, ['-m', 'pip', 'install', '-r', REQUIREMENTS], { stdio: 'inherit' });
89
+ if (pip.status !== 0) return { ready: false, created, error: 'pip install failed.' };
90
+
91
+ const ready = mlxEnvReady();
92
+ return { ready, created, error: ready ? null : 'deps installed but `import mlx` still fails.' };
93
+ }
94
+
95
+ // ── CLI: `node embedders/setup-mlx.mjs` (npm run embed:setup:mlx) ─────────────
96
+ const _invokedDirectly = process.argv[1]
97
+ && (() => { try { return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })();
98
+
99
+ if (_invokedDirectly) {
100
+ console.log('Setting up the MLX (Apple Metal) embedder…\n');
101
+ const res = ensureMlxEnv({ autoInstall: true, log: (s) => console.log(' • ' + s) });
102
+ if (res.ready) {
103
+ console.log(`\n✓ MLX embedder ready — vectors will run under embedders/venv-mlx.`);
104
+ console.log(` Use it: npx idx-index --repo . --embeddings --embed-provider mlx`);
105
+ process.exit(0);
106
+ }
107
+ console.error(`\n✗ MLX setup failed: ${res.error}`);
108
+ if (process.platform !== 'darwin') console.error(' MLX is macOS-only; use --embed-provider local or ollama instead.');
109
+ process.exit(1);
110
+ }