graph-indexer 2.1.0 → 3.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 (129) hide show
  1. package/CHANGELOG.md +260 -1
  2. package/README.md +232 -312
  3. package/SECURITY.md +37 -59
  4. package/bin/graph-indexer.mjs +251 -0
  5. package/integrations/claude-code/.claude-plugin/plugin.json +8 -0
  6. package/integrations/claude-code/.mcp.json +8 -0
  7. package/integrations/claude-code/agents/code-structure.md +22 -0
  8. package/integrations/claude-code/hooks/gi-hook.mjs +99 -0
  9. package/integrations/claude-code/hooks/hooks.json +16 -0
  10. package/integrations/opencode/graph-indexer.js +124 -0
  11. package/package.json +31 -105
  12. package/src/cli/helper.mjs +67 -0
  13. package/src/cli/hook.mjs +356 -0
  14. package/src/cli/init.mjs +255 -0
  15. package/src/cli/resident.mjs +175 -0
  16. package/src/index/discover.mjs +130 -0
  17. package/src/index/fingerprint.mjs +30 -0
  18. package/src/index/graph.mjs +61 -0
  19. package/src/index/indexer.mjs +476 -0
  20. package/src/index/modules.mjs +331 -0
  21. package/src/index/resolver.mjs +954 -0
  22. package/src/mcp/render.mjs +60 -0
  23. package/src/mcp/server.mjs +147 -0
  24. package/src/mcp/tools.mjs +973 -0
  25. package/src/parse/arity.mjs +152 -0
  26. package/src/parse/extract.mjs +1031 -0
  27. package/src/parse/lang/csharp.mjs +106 -0
  28. package/src/parse/lang/ecma.mjs +438 -0
  29. package/src/parse/lang/go.mjs +194 -0
  30. package/src/parse/lang/jvm.mjs +118 -0
  31. package/src/parse/lang/kotlin.mjs +97 -0
  32. package/src/parse/lang/misc.mjs +115 -0
  33. package/src/parse/lang/native.mjs +126 -0
  34. package/src/parse/lang/python.mjs +153 -0
  35. package/src/parse/lang/rust.mjs +157 -0
  36. package/src/parse/lang/scripting.mjs +219 -0
  37. package/src/parse/languages.mjs +48 -0
  38. package/src/parse/runtime.mjs +56 -0
  39. package/src/query/check.mjs +343 -0
  40. package/src/query/facts.mjs +323 -0
  41. package/src/query/intel.mjs +680 -0
  42. package/src/query/read.mjs +81 -0
  43. package/src/query/testcmd.mjs +119 -0
  44. package/src/query/textsearch.mjs +160 -0
  45. package/src/search/search.mjs +210 -0
  46. package/src/search/tokenize.mjs +188 -0
  47. package/src/store/db.mjs +169 -0
  48. package/src/util/paths.mjs +24 -0
  49. package/vendor/grammars/bash.wasm.gz +0 -0
  50. package/vendor/grammars/c.wasm.gz +0 -0
  51. package/vendor/grammars/c_sharp.wasm.gz +0 -0
  52. package/vendor/grammars/cpp.wasm.gz +0 -0
  53. package/vendor/grammars/css.wasm.gz +0 -0
  54. package/vendor/grammars/go.wasm.gz +0 -0
  55. package/vendor/grammars/java.wasm.gz +0 -0
  56. package/vendor/grammars/javascript.wasm.gz +0 -0
  57. package/vendor/grammars/kotlin.wasm.gz +0 -0
  58. package/vendor/grammars/manifest.json +148 -0
  59. package/vendor/grammars/php.wasm.gz +0 -0
  60. package/vendor/grammars/python.wasm.gz +0 -0
  61. package/vendor/grammars/ruby.wasm.gz +0 -0
  62. package/vendor/grammars/rust.wasm.gz +0 -0
  63. package/vendor/grammars/scala.wasm.gz +0 -0
  64. package/vendor/grammars/tsx.wasm.gz +0 -0
  65. package/vendor/grammars/typescript.wasm.gz +0 -0
  66. package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
  67. package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
  68. package/cli-ui.mjs +0 -78
  69. package/colbert.mjs +0 -168
  70. package/config.mjs +0 -494
  71. package/daemon-ctl.mjs +0 -175
  72. package/daemon-lock.mjs +0 -69
  73. package/embedders/python/mlx_embed_server.py +0 -61
  74. package/embedders/python/requirements-mlx.txt +0 -5
  75. package/embedders/setup-mlx.mjs +0 -110
  76. package/embeddings.mjs +0 -571
  77. package/engine/binary.mjs +0 -437
  78. package/engine/memory.mjs +0 -1003
  79. package/engine/sqlite.mjs +0 -1136
  80. package/enrichment.mjs +0 -521
  81. package/git-signals.mjs +0 -248
  82. package/indexer.mjs +0 -546
  83. package/init.mjs +0 -1809
  84. package/layout.mjs +0 -123
  85. package/mcp/centrality.mjs +0 -117
  86. package/mcp/format.mjs +0 -61
  87. package/mcp/resolver.mjs +0 -116
  88. package/mcp/symbolgraph.mjs +0 -137
  89. package/mcp/taint.mjs +0 -225
  90. package/mcp/tools.mjs +0 -1582
  91. package/mcp/topology.mjs +0 -676
  92. package/mcp-server.mjs +0 -197
  93. package/parse/extractor.mjs +0 -628
  94. package/parse/imports.mjs +0 -365
  95. package/parse/interprocedural.mjs +0 -134
  96. package/parse/languages.mjs +0 -253
  97. package/parse/metadata.mjs +0 -701
  98. package/parse/routes.mjs +0 -327
  99. package/parse/scip.mjs +0 -391
  100. package/parse/taint-patterns.mjs +0 -119
  101. package/prompts/CORE.md +0 -91
  102. package/prompts/DOMAIN_TEMPLATE.md +0 -46
  103. package/prompts/INTEGRATION.md +0 -123
  104. package/prompts/frameworks/ANDROID.md +0 -17
  105. package/prompts/frameworks/ASPNET_CORE.md +0 -19
  106. package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
  107. package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
  108. package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
  109. package/prompts/frameworks/RAILS.md +0 -20
  110. package/prompts/frameworks/REACT.md +0 -20
  111. package/prompts/frameworks/SPRING_BOOT.md +0 -20
  112. package/prompts/languages/BASH.md +0 -22
  113. package/prompts/languages/C.md +0 -21
  114. package/prompts/languages/CSHARP.md +0 -24
  115. package/prompts/languages/CSS_SCSS.md +0 -22
  116. package/prompts/languages/GO.md +0 -24
  117. package/prompts/languages/JAVA.md +0 -23
  118. package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
  119. package/prompts/languages/KOTLIN.md +0 -24
  120. package/prompts/languages/PHP.md +0 -23
  121. package/prompts/languages/PYTHON.md +0 -23
  122. package/prompts/languages/RUBY.md +0 -23
  123. package/prompts/languages/RUST.md +0 -21
  124. package/prompts/languages/SWIFT.md +0 -21
  125. package/seal.mjs +0 -400
  126. package/search-core.mjs +0 -777
  127. package/search-sparse.mjs +0 -183
  128. package/storage.mjs +0 -73
  129. package/watch-daemon.mjs +0 -289
package/daemon-ctl.mjs DELETED
@@ -1,175 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * @file daemon-ctl.mjs
4
- * @description `idx-daemon` — control the graph-indexer watch daemon from the
5
- * command line (and therefore from npm scripts): start, stop,
6
- * restart, status, logs. Exactly one daemon runs per project; the
7
- * daemon self-guards via an atomic PID lock (daemon-lock.mjs), so
8
- * `start` is safe to call repeatedly and from any launcher.
9
- *
10
- * idx-daemon start Start it (no-op if already running)
11
- * idx-daemon stop Gracefully stop it
12
- * idx-daemon restart Stop then start
13
- * idx-daemon status Show daemon + index state (default)
14
- * idx-daemon logs [-f] Print recent daemon logs (-f to follow)
15
- * @author MaquinaTech <https://github.com/MaquinaTech>
16
- * @copyright (c) 2026 MaquinaTech. All rights reserved.
17
- * @license MIT
18
- */
19
- import fs from 'fs';
20
- import path from 'path';
21
- import { fileURLToPath } from 'url';
22
- import { spawn } from 'child_process';
23
- import { resolveConfig } from './config.mjs';
24
- import { resolveBackend } from './storage.mjs';
25
- import { ensureDataDir, migrateLegacyLayout } from './layout.mjs';
26
- import { daemonStatus, readPid, isAlive } from './daemon-lock.mjs';
27
- import { c, glyph, log } from './cli-ui.mjs';
28
-
29
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
- const DAEMON_PATH = path.join(__dirname, 'watch-daemon.mjs');
31
-
32
- const config = resolveConfig();
33
- const ROOT = config.projectRoot;
34
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
35
-
36
- function relArtifact() {
37
- const p = resolveBackend(config) === 'sqlite' ? config.sqlitePath : config.indexPath;
38
- return path.relative(ROOT, p) || p;
39
- }
40
-
41
- function freshness(file) {
42
- try {
43
- const sec = Math.floor((Date.now() - fs.statSync(file).mtimeMs) / 1000);
44
- if (sec < 60) return `${sec}s ago`;
45
- if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
46
- if (sec < 86400) return `${Math.floor(sec / 3600)}h ago`;
47
- return `${Math.floor(sec / 86400)}d ago`;
48
- } catch { return 'never indexed'; }
49
- }
50
-
51
- function spawnDaemon() {
52
- ensureDataDir(ROOT);
53
- let logFd = null;
54
- try { logFd = fs.openSync(config.logFile, 'a'); } catch { }
55
- const child = spawn(process.execPath, [DAEMON_PATH], {
56
- detached: true,
57
- stdio: logFd !== null ? ['ignore', logFd, logFd] : 'ignore',
58
- env: { ...process.env, MCP_PROJECT_ROOT: ROOT },
59
- });
60
- child.unref();
61
- if (logFd !== null) fs.closeSync(logFd);
62
- return child.pid;
63
- }
64
-
65
- function cmdStatus() {
66
- const { running, pid } = daemonStatus(config.pidFile);
67
- const backend = resolveBackend(config);
68
- const artifact = backend === 'sqlite' ? config.sqlitePath : config.indexPath;
69
- log('');
70
- log(` ${c.bold('graph-indexer')} ${c.dim('· daemon status')}`);
71
- log(` ${running ? glyph.run : glyph.stop} Daemon ${running ? c.green(`running (PID ${pid})`) : c.dim('stopped')}`);
72
- log(` ${glyph.arrow} Project ${c.dim(ROOT)}`);
73
- log(` ${glyph.arrow} Backend ${backend === 'sqlite' ? 'SQLite (disk-backed)' : 'in-memory'}${config.storage === 'auto' ? c.dim(' · auto') : ''}`);
74
- log(` ${glyph.arrow} Index ${c.dim(relArtifact())} ${c.dim('· ' + freshness(artifact))}`);
75
- log(` ${glyph.arrow} Logs ${c.dim(path.relative(ROOT, config.logFile) || config.logFile)}`);
76
- log('');
77
- if (!running) log(c.dim(` Start it with: ${c.cyan('npm run mcp:daemon:start')}\n`));
78
- return 0;
79
- }
80
-
81
- function cmdStart() {
82
- ensureDataDir(ROOT);
83
- migrateLegacyLayout(ROOT);
84
- const { running, pid } = daemonStatus(config.pidFile);
85
- if (running) {
86
- log(`${glyph.keep} Daemon already running ${c.dim(`(PID ${pid})`)} — only one runs per project.`);
87
- return 0;
88
- }
89
- const newPid = spawnDaemon();
90
- log(`${glyph.ok} Watch daemon started ${c.dim(`(PID ${newPid})`)}`);
91
- log(c.dim(` Logs: ${path.relative(ROOT, config.logFile) || config.logFile}`));
92
- return 0;
93
- }
94
-
95
- async function cmdStop() {
96
- const pid = readPid(config.pidFile);
97
- if (!isAlive(pid)) {
98
- log(`${glyph.keep} No daemon running.`);
99
- try { if (pid) fs.unlinkSync(config.pidFile); } catch { }
100
- return 0;
101
- }
102
- try { process.kill(pid, 'SIGTERM'); } catch { /* already gone */ }
103
- for (let i = 0; i < 50 && isAlive(readPid(config.pidFile) ?? pid); i++) await sleep(100);
104
- if (isAlive(pid)) {
105
- log(`${glyph.warn} Daemon ${c.dim(`(PID ${pid})`)} did not stop within 5s.`);
106
- return 1;
107
- }
108
- log(`${glyph.ok} Daemon stopped ${c.dim(`(PID ${pid})`)}.`);
109
- return 0;
110
- }
111
-
112
- async function cmdRestart() {
113
- await cmdStop();
114
- await sleep(200);
115
- return cmdStart();
116
- }
117
-
118
- function tail(file, n) {
119
- try {
120
- const size = fs.statSync(file).size;
121
- const start = Math.max(0, size - 256 * 1024); // last 256 KB is plenty
122
- const fd = fs.openSync(file, 'r');
123
- try {
124
- const buf = Buffer.alloc(size - start);
125
- fs.readSync(fd, buf, 0, buf.length, start);
126
- const lines = buf.toString('utf-8').split('\n');
127
- return { lines: lines.slice(-n - 1), size };
128
- } finally { fs.closeSync(fd); }
129
- } catch { return { lines: [], size: 0 }; }
130
- }
131
-
132
- async function cmdLogs(args) {
133
- const follow = args.includes('-f') || args.includes('--follow');
134
- const nArg = args.find((a) => /^\d+$/.test(a));
135
- const n = nArg ? Number(nArg) : 50;
136
- if (!fs.existsSync(config.logFile)) {
137
- log(c.dim(`No daemon log yet (${path.relative(ROOT, config.logFile) || config.logFile}).`));
138
- return 0;
139
- }
140
- const { lines, size } = tail(config.logFile, n);
141
- process.stdout.write(lines.join('\n'));
142
- if (!follow) { log(''); return 0; }
143
-
144
- let pos = size;
145
- log(c.dim(`\n— following ${path.relative(ROOT, config.logFile)} (Ctrl-C to stop) —`));
146
- fs.watchFile(config.logFile, { interval: 500 }, (cur) => {
147
- if (cur.size < pos) { pos = 0; return; } // truncated/rotated
148
- if (cur.size === pos) return;
149
- const fd = fs.openSync(config.logFile, 'r');
150
- try {
151
- const buf = Buffer.alloc(cur.size - pos);
152
- fs.readSync(fd, buf, 0, buf.length, pos);
153
- process.stdout.write(buf.toString('utf-8'));
154
- pos = cur.size;
155
- } finally { fs.closeSync(fd); }
156
- });
157
- return new Promise(() => { /* runs until Ctrl-C */ });
158
- }
159
-
160
- const [cmd = 'status', ...rest] = process.argv.slice(2);
161
- const run = {
162
- start: cmdStart,
163
- stop: cmdStop,
164
- restart: cmdRestart,
165
- status: cmdStatus,
166
- logs: () => cmdLogs(rest),
167
- }[cmd];
168
-
169
- if (!run) {
170
- log(`${glyph.err} Unknown command: ${c.bold(cmd)}`);
171
- log(c.dim(' Usage: idx-daemon [start|stop|restart|status|logs]'));
172
- process.exit(2);
173
- }
174
-
175
- Promise.resolve(run()).then((code) => process.exit(code ?? 0));
package/daemon-lock.mjs DELETED
@@ -1,69 +0,0 @@
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
- }
@@ -1,61 +0,0 @@
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)
@@ -1,5 +0,0 @@
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
@@ -1,110 +0,0 @@
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
- }