graph-indexer 2.1.1 → 3.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/CHANGELOG.md +312 -1
- package/README.md +236 -312
- package/SECURITY.md +53 -58
- package/bin/graph-indexer.mjs +251 -0
- package/integrations/claude-code/.claude-plugin/plugin.json +9 -0
- package/integrations/claude-code/.mcp.json +8 -0
- package/integrations/claude-code/agents/code-structure.md +22 -0
- package/integrations/claude-code/hooks/gi-hook.mjs +103 -0
- package/integrations/claude-code/hooks/hooks.json +16 -0
- package/integrations/opencode/graph-indexer.js +129 -0
- package/package.json +32 -109
- package/src/cli/helper.mjs +67 -0
- package/src/cli/hook.mjs +366 -0
- package/src/cli/init.mjs +272 -0
- package/src/cli/resident.mjs +185 -0
- package/src/index/discover.mjs +128 -0
- package/src/index/fingerprint.mjs +30 -0
- package/src/index/graph.mjs +61 -0
- package/src/index/indexer.mjs +476 -0
- package/src/index/modules.mjs +332 -0
- package/src/index/resolver.mjs +961 -0
- package/src/mcp/render.mjs +60 -0
- package/src/mcp/server.mjs +147 -0
- package/src/mcp/tools.mjs +974 -0
- package/src/parse/arity.mjs +153 -0
- package/src/parse/extract.mjs +1038 -0
- package/src/parse/lang/csharp.mjs +106 -0
- package/src/parse/lang/ecma.mjs +438 -0
- package/src/parse/lang/go.mjs +194 -0
- package/src/parse/lang/jvm.mjs +118 -0
- package/src/parse/lang/kotlin.mjs +97 -0
- package/src/parse/lang/misc.mjs +115 -0
- package/src/parse/lang/native.mjs +126 -0
- package/src/parse/lang/python.mjs +153 -0
- package/src/parse/lang/rust.mjs +157 -0
- package/src/parse/lang/scripting.mjs +219 -0
- package/src/parse/languages.mjs +48 -0
- package/src/parse/runtime.mjs +56 -0
- package/src/query/check.mjs +345 -0
- package/src/query/facts.mjs +323 -0
- package/src/query/intel.mjs +683 -0
- package/src/query/read.mjs +81 -0
- package/src/query/testcmd.mjs +130 -0
- package/src/query/textsearch.mjs +178 -0
- package/src/search/search.mjs +210 -0
- package/src/search/tokenize.mjs +188 -0
- package/src/store/db.mjs +169 -0
- package/src/util/git.mjs +30 -0
- package/src/util/paths.mjs +63 -0
- package/src/util/regex.mjs +2 -0
- package/vendor/grammars/bash.wasm.gz +0 -0
- package/vendor/grammars/c.wasm.gz +0 -0
- package/vendor/grammars/c_sharp.wasm.gz +0 -0
- package/vendor/grammars/cpp.wasm.gz +0 -0
- package/vendor/grammars/css.wasm.gz +0 -0
- package/vendor/grammars/go.wasm.gz +0 -0
- package/vendor/grammars/java.wasm.gz +0 -0
- package/vendor/grammars/javascript.wasm.gz +0 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +148 -0
- package/vendor/grammars/php.wasm.gz +0 -0
- package/vendor/grammars/python.wasm.gz +0 -0
- package/vendor/grammars/ruby.wasm.gz +0 -0
- package/vendor/grammars/rust.wasm.gz +0 -0
- package/vendor/grammars/scala.wasm.gz +0 -0
- package/vendor/grammars/tsx.wasm.gz +0 -0
- package/vendor/grammars/typescript.wasm.gz +0 -0
- package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
- package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/cli-ui.mjs +0 -78
- package/colbert.mjs +0 -168
- package/config.mjs +0 -494
- package/daemon-ctl.mjs +0 -175
- package/daemon-lock.mjs +0 -69
- package/embedders/python/mlx_embed_server.py +0 -61
- package/embedders/python/requirements-mlx.txt +0 -5
- package/embedders/setup-mlx.mjs +0 -110
- package/embeddings.mjs +0 -571
- package/engine/binary.mjs +0 -437
- package/engine/memory.mjs +0 -1003
- package/engine/sqlite.mjs +0 -1136
- package/enrichment.mjs +0 -521
- package/git-signals.mjs +0 -248
- package/indexer.mjs +0 -546
- package/init.mjs +0 -1809
- package/layout.mjs +0 -123
- package/mcp/centrality.mjs +0 -117
- package/mcp/format.mjs +0 -61
- package/mcp/resolver.mjs +0 -116
- package/mcp/symbolgraph.mjs +0 -137
- package/mcp/taint.mjs +0 -225
- package/mcp/tools.mjs +0 -1582
- package/mcp/topology.mjs +0 -676
- package/mcp-server.mjs +0 -197
- package/parse/extractor.mjs +0 -628
- package/parse/imports.mjs +0 -365
- package/parse/interprocedural.mjs +0 -134
- package/parse/languages.mjs +0 -253
- package/parse/metadata.mjs +0 -701
- package/parse/routes.mjs +0 -327
- package/parse/scip.mjs +0 -391
- package/parse/taint-patterns.mjs +0 -119
- package/prompts/CORE.md +0 -91
- package/prompts/DOMAIN_TEMPLATE.md +0 -46
- package/prompts/INTEGRATION.md +0 -123
- package/prompts/frameworks/ANDROID.md +0 -17
- package/prompts/frameworks/ASPNET_CORE.md +0 -19
- package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
- package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
- package/prompts/frameworks/RAILS.md +0 -20
- package/prompts/frameworks/REACT.md +0 -20
- package/prompts/frameworks/SPRING_BOOT.md +0 -20
- package/prompts/languages/BASH.md +0 -22
- package/prompts/languages/C.md +0 -21
- package/prompts/languages/CSHARP.md +0 -24
- package/prompts/languages/CSS_SCSS.md +0 -22
- package/prompts/languages/GO.md +0 -24
- package/prompts/languages/JAVA.md +0 -23
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
- package/prompts/languages/KOTLIN.md +0 -24
- package/prompts/languages/PHP.md +0 -23
- package/prompts/languages/PYTHON.md +0 -23
- package/prompts/languages/RUBY.md +0 -23
- package/prompts/languages/RUST.md +0 -21
- package/prompts/languages/SWIFT.md +0 -21
- package/seal.mjs +0 -400
- package/search-core.mjs +0 -777
- package/search-sparse.mjs +0 -183
- package/storage.mjs +0 -73
- 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)
|
package/embedders/setup-mlx.mjs
DELETED
|
@@ -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
|
-
}
|