magector 1.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.
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "magector",
3
+ "version": "1.0.0",
4
+ "description": "Semantic code search for Magento 2 — index, search, MCP server",
5
+ "type": "module",
6
+ "main": "src/mcp-server.js",
7
+ "bin": {
8
+ "magector": "./src/cli.js"
9
+ },
10
+ "files": [
11
+ "src/",
12
+ "config/"
13
+ ],
14
+ "scripts": {
15
+ "index": "node src/cli.js index",
16
+ "search": "node src/cli.js search",
17
+ "mcp": "node src/mcp-server.js",
18
+ "start": "node src/mcp-server.js",
19
+ "validate": "node src/cli.js validate",
20
+ "validate:verbose": "node src/cli.js validate --verbose",
21
+ "validate:keep": "node src/cli.js validate --verbose --keep",
22
+ "benchmark": "node src/cli.js benchmark",
23
+ "test": "node tests/mcp-server.test.js",
24
+ "test:no-index": "node tests/mcp-server.test.js --no-index"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.0.0",
28
+ "chalk": "^5.3.0",
29
+ "glob": "^10.3.10",
30
+ "ora": "^8.0.1",
31
+ "ruvector": "^0.1.96"
32
+ },
33
+ "optionalDependencies": {
34
+ "@magector/cli-darwin-arm64": "1.0.0",
35
+ "@magector/cli-linux-x64": "1.0.0",
36
+ "@magector/cli-linux-arm64": "1.0.0",
37
+ "@magector/cli-win32-x64": "1.0.0"
38
+ },
39
+ "keywords": [
40
+ "magento",
41
+ "indexer",
42
+ "vector-search",
43
+ "mcp",
44
+ "claude-code",
45
+ "semantic-search",
46
+ "cursor"
47
+ ],
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/krejcif/magector.git"
51
+ },
52
+ "license": "MIT"
53
+ }
package/src/binary.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Resolve the platform-specific Rust binary (magector-core).
3
+ *
4
+ * Resolution order:
5
+ * 1. MAGECTOR_BIN env var
6
+ * 2. @magector/cli-{os}-{arch} optionalDependency
7
+ * 3. rust-core/target/release/magector-core (dev fallback)
8
+ * 4. magector-core in PATH
9
+ */
10
+ import { existsSync } from 'fs';
11
+ import { execFileSync } from 'child_process';
12
+ import path from 'path';
13
+ import { fileURLToPath } from 'url';
14
+ import { createRequire } from 'module';
15
+
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ const require = createRequire(import.meta.url);
18
+
19
+ const BINARY_NAME = process.platform === 'win32' ? 'magector-core.exe' : 'magector-core';
20
+
21
+ export function resolveBinary() {
22
+ // 1. Explicit env var
23
+ if (process.env.MAGECTOR_BIN) {
24
+ if (existsSync(process.env.MAGECTOR_BIN)) {
25
+ return process.env.MAGECTOR_BIN;
26
+ }
27
+ throw new Error(`MAGECTOR_BIN set to ${process.env.MAGECTOR_BIN} but file not found`);
28
+ }
29
+
30
+ // 2. Platform-specific npm package
31
+ const platformPkg = `@magector/cli-${process.platform}-${process.arch}`;
32
+ try {
33
+ const pkgDir = path.dirname(require.resolve(`${platformPkg}/package.json`));
34
+ const binPath = path.join(pkgDir, 'bin', BINARY_NAME);
35
+ if (existsSync(binPath)) {
36
+ return binPath;
37
+ }
38
+ } catch {
39
+ // Package not installed — continue
40
+ }
41
+
42
+ // 3. Dev fallback: local Rust build
43
+ const devPath = path.join(__dirname, '..', 'rust-core', 'target', 'release', BINARY_NAME);
44
+ if (existsSync(devPath)) {
45
+ return devPath;
46
+ }
47
+
48
+ // 4. Global PATH
49
+ try {
50
+ const which = process.platform === 'win32' ? 'where' : 'which';
51
+ const result = execFileSync(which, ['magector-core'], {
52
+ encoding: 'utf-8',
53
+ stdio: ['pipe', 'pipe', 'pipe']
54
+ }).trim();
55
+ if (result) return result.split('\n')[0];
56
+ } catch {
57
+ // Not in PATH
58
+ }
59
+
60
+ throw new Error(
61
+ `Could not find magector-core binary.\n` +
62
+ `Install the platform package: npm install ${platformPkg}\n` +
63
+ `Or build from source: cd rust-core && cargo build --release\n` +
64
+ `Or set MAGECTOR_BIN environment variable.`
65
+ );
66
+ }
package/src/cli.js ADDED
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Magector CLI — npx magector <command>
4
+ *
5
+ * All search/index/stats commands delegate to the Rust binary (magector-core).
6
+ * The CLI resolves the binary and model paths, then shells out.
7
+ */
8
+ import { execFileSync, spawn } from 'child_process';
9
+ import path from 'path';
10
+ import { resolveBinary } from './binary.js';
11
+ import { ensureModels, resolveModels } from './model.js';
12
+ import { init, setup } from './init.js';
13
+
14
+ const args = process.argv.slice(2);
15
+ const command = args[0];
16
+
17
+ function showHelp() {
18
+ console.log(`
19
+ Magector — Semantic code search for Magento 2
20
+
21
+ Usage:
22
+ npx magector init [path] Full setup: index + IDE config
23
+ npx magector index [path] Index (or re-index) Magento codebase
24
+ npx magector search <query> Search indexed code
25
+ npx magector mcp Start MCP server (for Claude Code / Cursor)
26
+ npx magector stats Show index statistics
27
+ npx magector setup [path] IDE setup only (no indexing)
28
+ npx magector help Show this help
29
+
30
+ Options:
31
+ -l, --limit <n> Number of search results (default: 10)
32
+ -f, --format <fmt> Output format: text, json (default: text)
33
+
34
+ Environment Variables:
35
+ MAGENTO_ROOT Path to Magento installation (default: cwd)
36
+ MAGECTOR_DB Path to index database (default: ./magector.db)
37
+ MAGECTOR_BIN Path to magector-core binary
38
+ MAGECTOR_MODELS Path to ONNX model directory
39
+
40
+ Examples:
41
+ npx magector init /var/www/magento
42
+ npx magector search "product price calculation"
43
+ npx magector search "checkout controller" -l 20
44
+ npx magector index
45
+ npx magector mcp
46
+ `);
47
+ }
48
+
49
+ function getConfig() {
50
+ return {
51
+ dbPath: process.env.MAGECTOR_DB || './magector.db',
52
+ magentoRoot: process.env.MAGENTO_ROOT || process.cwd()
53
+ };
54
+ }
55
+
56
+ function parseArgs(argv) {
57
+ const opts = {};
58
+ for (let i = 0; i < argv.length; i++) {
59
+ if (argv[i] === '-l' || argv[i] === '--limit') {
60
+ opts.limit = argv[++i];
61
+ } else if (argv[i] === '-f' || argv[i] === '--format') {
62
+ opts.format = argv[++i];
63
+ } else if (argv[i] === '-v' || argv[i] === '--verbose') {
64
+ opts.verbose = true;
65
+ }
66
+ }
67
+ return opts;
68
+ }
69
+
70
+ async function runIndex(targetPath) {
71
+ const config = getConfig();
72
+ const root = targetPath || config.magentoRoot;
73
+ const binary = resolveBinary();
74
+ const modelPath = await ensureModels();
75
+
76
+ console.log(`\nIndexing: ${path.resolve(root)}`);
77
+ console.log(`Database: ${path.resolve(config.dbPath)}\n`);
78
+
79
+ try {
80
+ const output = execFileSync(binary, [
81
+ 'index',
82
+ '-m', path.resolve(root),
83
+ '-d', path.resolve(config.dbPath),
84
+ '-c', modelPath
85
+ ], { encoding: 'utf-8', timeout: 600000, stdio: ['pipe', 'pipe', 'pipe'] });
86
+ if (output.trim()) console.log(output.trim());
87
+ console.log('\nIndexing complete.');
88
+ } catch (err) {
89
+ const output = err.stderr || err.stdout || err.message;
90
+ console.error(`Indexing error: ${output}`);
91
+ process.exit(1);
92
+ }
93
+ }
94
+
95
+ function runSearch(query, opts = {}) {
96
+ const config = getConfig();
97
+ const binary = resolveBinary();
98
+ const modelPath = resolveModels();
99
+
100
+ if (!modelPath) {
101
+ console.error('ONNX model not found. Run `npx magector init` or `npx magector index` first.');
102
+ process.exit(1);
103
+ }
104
+
105
+ const searchArgs = [
106
+ 'search', query,
107
+ '-d', path.resolve(config.dbPath),
108
+ '-c', modelPath,
109
+ '-l', String(opts.limit || 10),
110
+ '-f', opts.format || 'text'
111
+ ];
112
+
113
+ try {
114
+ const output = execFileSync(binary, searchArgs, {
115
+ encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe']
116
+ });
117
+ console.log(output);
118
+ } catch (err) {
119
+ const output = err.stderr || err.stdout || err.message;
120
+ console.error(`Search error: ${output}`);
121
+ process.exit(1);
122
+ }
123
+ }
124
+
125
+ function runStats() {
126
+ const config = getConfig();
127
+ const binary = resolveBinary();
128
+
129
+ try {
130
+ const output = execFileSync(binary, [
131
+ 'stats', '-d', path.resolve(config.dbPath)
132
+ ], { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
133
+ console.log(output);
134
+ } catch (err) {
135
+ const output = err.stderr || err.stdout || err.message;
136
+ console.error(`Stats error: ${output}`);
137
+ process.exit(1);
138
+ }
139
+ }
140
+
141
+ async function main() {
142
+ switch (command) {
143
+ case 'init':
144
+ await init(args[1]);
145
+ break;
146
+
147
+ case 'index':
148
+ await runIndex(args[1]);
149
+ break;
150
+
151
+ case 'search': {
152
+ const query = args.slice(1).filter(a => !a.startsWith('-')).join(' ');
153
+ if (!query) {
154
+ console.error('Usage: npx magector search <query>');
155
+ process.exit(1);
156
+ }
157
+ const opts = parseArgs(args.slice(1));
158
+ runSearch(query, opts);
159
+ break;
160
+ }
161
+
162
+ case 'mcp':
163
+ await import('./mcp-server.js');
164
+ break;
165
+
166
+ case 'stats':
167
+ runStats();
168
+ break;
169
+
170
+ case 'setup':
171
+ await setup(args[1]);
172
+ break;
173
+
174
+ case 'validate': {
175
+ const { runFullValidation } = await import('./validation/validator.js');
176
+ const verbose = args.includes('--verbose') || args.includes('-v');
177
+ const keepData = args.includes('--keep');
178
+ await runFullValidation({ verbose, keepTestData: keepData });
179
+ break;
180
+ }
181
+
182
+ case 'benchmark':
183
+ await import('./validation/benchmark.js');
184
+ break;
185
+
186
+ case 'help':
187
+ case '--help':
188
+ case '-h':
189
+ case undefined:
190
+ showHelp();
191
+ break;
192
+
193
+ default:
194
+ console.error(`Unknown command: ${command}`);
195
+ showHelp();
196
+ process.exit(1);
197
+ }
198
+ }
199
+
200
+ main().catch(err => {
201
+ console.error('Error:', err.message);
202
+ process.exit(1);
203
+ });
package/src/init.js ADDED
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Full init command: verify Magento project, index, detect IDE, write configs.
3
+ */
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
5
+ import { execFileSync } from 'child_process';
6
+ import path from 'path';
7
+ import { resolveBinary } from './binary.js';
8
+ import { ensureModels } from './model.js';
9
+ import { CURSORRULES } from './templates/cursorrules.js';
10
+ import { CLAUDE_MD } from './templates/claude-md.js';
11
+
12
+ /**
13
+ * Detect if the given path is a Magento 2 project root.
14
+ */
15
+ function isMagentoProject(projectPath) {
16
+ // Check app/etc/env.php
17
+ if (existsSync(path.join(projectPath, 'app', 'etc', 'env.php'))) {
18
+ return true;
19
+ }
20
+ // Check composer.json for magento packages
21
+ const composerPath = path.join(projectPath, 'composer.json');
22
+ if (existsSync(composerPath)) {
23
+ try {
24
+ const content = readFileSync(composerPath, 'utf-8');
25
+ if (content.includes('magento/') || content.includes('"magento-')) {
26
+ return true;
27
+ }
28
+ } catch {
29
+ // ignore read errors
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+
35
+ /**
36
+ * Detect which IDEs are present.
37
+ * Returns { cursor: boolean, claude: boolean }
38
+ */
39
+ function detectIDEs(projectPath) {
40
+ const cursor =
41
+ existsSync(path.join(projectPath, '.cursor')) ||
42
+ existsSync(path.join(projectPath, '.cursorrules'));
43
+ const claude =
44
+ existsSync(path.join(projectPath, '.claude')) ||
45
+ existsSync(path.join(projectPath, 'CLAUDE.md')) ||
46
+ existsSync(path.join(projectPath, '.mcp.json'));
47
+ return { cursor, claude };
48
+ }
49
+
50
+ /**
51
+ * Write MCP server configuration for the given IDE(s).
52
+ */
53
+ function writeMcpConfig(projectPath, ides, dbPath) {
54
+ const mcpConfig = {
55
+ mcpServers: {
56
+ magector: {
57
+ command: 'npx',
58
+ args: ['-y', 'magector', 'mcp'],
59
+ env: {
60
+ MAGENTO_ROOT: projectPath,
61
+ MAGECTOR_DB: dbPath
62
+ }
63
+ }
64
+ }
65
+ };
66
+
67
+ const configJson = JSON.stringify(mcpConfig, null, 2);
68
+ const written = [];
69
+
70
+ if (ides.cursor) {
71
+ const cursorDir = path.join(projectPath, '.cursor');
72
+ mkdirSync(cursorDir, { recursive: true });
73
+ writeFileSync(path.join(cursorDir, 'mcp.json'), configJson);
74
+ written.push('.cursor/mcp.json');
75
+ }
76
+
77
+ if (ides.claude) {
78
+ writeFileSync(path.join(projectPath, '.mcp.json'), configJson);
79
+ written.push('.mcp.json');
80
+ }
81
+
82
+ // If neither detected, set up both
83
+ if (!ides.cursor && !ides.claude) {
84
+ writeFileSync(path.join(projectPath, '.mcp.json'), configJson);
85
+ written.push('.mcp.json');
86
+ const cursorDir = path.join(projectPath, '.cursor');
87
+ mkdirSync(cursorDir, { recursive: true });
88
+ writeFileSync(path.join(cursorDir, 'mcp.json'), configJson);
89
+ written.push('.cursor/mcp.json');
90
+ }
91
+
92
+ return written;
93
+ }
94
+
95
+ /**
96
+ * Write IDE rules files.
97
+ */
98
+ function writeRules(projectPath, ides) {
99
+ const written = [];
100
+
101
+ const writeCursor = ides.cursor || (!ides.cursor && !ides.claude);
102
+ const writeClaude = ides.claude || (!ides.cursor && !ides.claude);
103
+
104
+ if (writeCursor) {
105
+ const rulesPath = path.join(projectPath, '.cursorrules');
106
+ if (!existsSync(rulesPath)) {
107
+ writeFileSync(rulesPath, CURSORRULES);
108
+ written.push('.cursorrules (created)');
109
+ } else {
110
+ const existing = readFileSync(rulesPath, 'utf-8');
111
+ if (!existing.includes('Magector')) {
112
+ appendFileSync(rulesPath, '\n\n' + CURSORRULES);
113
+ written.push('.cursorrules (appended)');
114
+ } else {
115
+ written.push('.cursorrules (already configured)');
116
+ }
117
+ }
118
+ }
119
+
120
+ if (writeClaude) {
121
+ const claudePath = path.join(projectPath, 'CLAUDE.md');
122
+ if (!existsSync(claudePath)) {
123
+ writeFileSync(claudePath, CLAUDE_MD);
124
+ written.push('CLAUDE.md (created)');
125
+ } else {
126
+ const existing = readFileSync(claudePath, 'utf-8');
127
+ if (!existing.includes('Magector')) {
128
+ appendFileSync(claudePath, '\n\n' + CLAUDE_MD);
129
+ written.push('CLAUDE.md (appended)');
130
+ } else {
131
+ written.push('CLAUDE.md (already configured)');
132
+ }
133
+ }
134
+ }
135
+
136
+ return written;
137
+ }
138
+
139
+ /**
140
+ * Add magector.db to .gitignore if not already present.
141
+ */
142
+ function updateGitignore(projectPath) {
143
+ const giPath = path.join(projectPath, '.gitignore');
144
+ if (existsSync(giPath)) {
145
+ const content = readFileSync(giPath, 'utf-8');
146
+ if (!content.includes('magector.db')) {
147
+ appendFileSync(giPath, '\n# Magector index\nmagector.db\n');
148
+ return true;
149
+ }
150
+ return false;
151
+ }
152
+ writeFileSync(giPath, '# Magector index\nmagector.db\n');
153
+ return true;
154
+ }
155
+
156
+ /**
157
+ * Main init function.
158
+ */
159
+ export async function init(projectPath) {
160
+ projectPath = path.resolve(projectPath || process.cwd());
161
+ const dbPath = path.join(projectPath, 'magector.db');
162
+
163
+ console.log('\nMagector Init\n');
164
+
165
+ // 1. Verify Magento project
166
+ console.log('Checking Magento project...');
167
+ if (!isMagentoProject(projectPath)) {
168
+ console.error(
169
+ `Error: ${projectPath} does not appear to be a Magento 2 project.\n` +
170
+ `Expected app/etc/env.php or composer.json with "magento/" dependencies.`
171
+ );
172
+ process.exit(1);
173
+ }
174
+ console.log(` Magento project: ${projectPath}`);
175
+
176
+ // 2. Resolve binary
177
+ console.log('\nResolving binary...');
178
+ let binary;
179
+ try {
180
+ binary = resolveBinary();
181
+ } catch (err) {
182
+ console.error(`Error: ${err.message}`);
183
+ process.exit(1);
184
+ }
185
+ console.log(` Binary: ${binary}`);
186
+
187
+ // 3. Ensure ONNX model
188
+ console.log('\nChecking ONNX model...');
189
+ let modelPath;
190
+ try {
191
+ modelPath = await ensureModels();
192
+ } catch (err) {
193
+ console.error(`Error downloading model: ${err.message}`);
194
+ process.exit(1);
195
+ }
196
+ console.log(` Models: ${modelPath}`);
197
+
198
+ // 4. Run indexing
199
+ console.log('\nIndexing codebase...');
200
+ const startTime = Date.now();
201
+ try {
202
+ const output = execFileSync(binary, [
203
+ 'index',
204
+ '-m', projectPath,
205
+ '-d', dbPath,
206
+ '-c', modelPath
207
+ ], { encoding: 'utf-8', timeout: 600000, stdio: ['pipe', 'pipe', 'pipe'] });
208
+ if (output.trim()) {
209
+ console.log(output.trim().split('\n').map(l => ` ${l}`).join('\n'));
210
+ }
211
+ } catch (err) {
212
+ const output = err.stderr || err.stdout || err.message;
213
+ console.error(`Indexing error: ${output}`);
214
+ process.exit(1);
215
+ }
216
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
217
+
218
+ // 5. Detect IDE
219
+ console.log('\nDetecting IDE...');
220
+ const ides = detectIDEs(projectPath);
221
+ const ideNames = [];
222
+ if (ides.cursor) ideNames.push('Cursor');
223
+ if (ides.claude) ideNames.push('Claude Code');
224
+ if (ideNames.length === 0) ideNames.push('Cursor', 'Claude Code');
225
+ console.log(` Detected: ${ideNames.join(' + ') || 'none (configuring both)'}`);
226
+
227
+ // 6. Write MCP config
228
+ console.log('\nWriting MCP config...');
229
+ const mcpFiles = writeMcpConfig(projectPath, ides, dbPath);
230
+ mcpFiles.forEach(f => console.log(` ${f}`));
231
+
232
+ // 7. Write rules
233
+ console.log('\nWriting IDE rules...');
234
+ const rulesFiles = writeRules(projectPath, ides);
235
+ rulesFiles.forEach(f => console.log(` ${f}`));
236
+
237
+ // 8. Update .gitignore
238
+ const giUpdated = updateGitignore(projectPath);
239
+ if (giUpdated) {
240
+ console.log('\nUpdated .gitignore with magector.db');
241
+ }
242
+
243
+ // 9. Get stats and print summary
244
+ let vectorCount = '?';
245
+ try {
246
+ const statsOutput = execFileSync(binary, ['stats', '-d', dbPath], {
247
+ encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe']
248
+ });
249
+ const match = statsOutput.match(/Total vectors:\s*(\d+)/);
250
+ if (match) vectorCount = match[1];
251
+ } catch {
252
+ // ignore
253
+ }
254
+
255
+ console.log(`\n${'='.repeat(50)}`);
256
+ console.log(`Setup complete!`);
257
+ console.log(` Indexed ${vectorCount} vectors in ${elapsed}s`);
258
+ console.log(` Configured for: ${ideNames.join(' + ')}`);
259
+ console.log(` Database: ${dbPath}`);
260
+ console.log(`\nTest it:`);
261
+ console.log(` npx magector search "product price calculation"`);
262
+ console.log(`${'='.repeat(50)}\n`);
263
+ }
264
+
265
+ /**
266
+ * IDE setup only (no indexing). For projects already indexed.
267
+ */
268
+ export async function setup(projectPath) {
269
+ projectPath = path.resolve(projectPath || process.cwd());
270
+ const dbPath = path.join(projectPath, 'magector.db');
271
+
272
+ console.log('\nMagector IDE Setup\n');
273
+
274
+ const ides = detectIDEs(projectPath);
275
+ const ideNames = [];
276
+ if (ides.cursor) ideNames.push('Cursor');
277
+ if (ides.claude) ideNames.push('Claude Code');
278
+ if (ideNames.length === 0) ideNames.push('Cursor', 'Claude Code');
279
+
280
+ console.log(`Detected: ${ideNames.join(' + ')}`);
281
+
282
+ const mcpFiles = writeMcpConfig(projectPath, ides, dbPath);
283
+ console.log('\nMCP config:');
284
+ mcpFiles.forEach(f => console.log(` ${f}`));
285
+
286
+ const rulesFiles = writeRules(projectPath, ides);
287
+ console.log('\nIDE rules:');
288
+ rulesFiles.forEach(f => console.log(` ${f}`));
289
+
290
+ updateGitignore(projectPath);
291
+
292
+ console.log(`\nDone. Configured for: ${ideNames.join(' + ')}\n`);
293
+ }