magector 2.13.3 → 2.14.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 +5 -5
- package/src/mcp-server.js +60 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"description": "Semantic code search for Magento 2 — index, search, MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp-server.js",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"ruvector": "^0.1.96"
|
|
34
34
|
},
|
|
35
35
|
"optionalDependencies": {
|
|
36
|
-
"@magector/cli-darwin-arm64": "2.
|
|
37
|
-
"@magector/cli-linux-x64": "2.
|
|
38
|
-
"@magector/cli-linux-arm64": "2.
|
|
39
|
-
"@magector/cli-win32-x64": "2.
|
|
36
|
+
"@magector/cli-darwin-arm64": "2.14.0",
|
|
37
|
+
"@magector/cli-linux-x64": "2.14.0",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.14.0",
|
|
39
|
+
"@magector/cli-win32-x64": "2.14.0"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -3459,12 +3459,15 @@ function hasNullGuard(lines, matchLineIdx, receiverExpr, guardRadius = 6) {
|
|
|
3459
3459
|
*/
|
|
3460
3460
|
async function enrichMethodChains(root) {
|
|
3461
3461
|
const dbPath = ENRICHMENT_DB_PATH(root);
|
|
3462
|
+
logToFile('INFO', `enrich: starting method-chain scan, db=${dbPath}`);
|
|
3463
|
+
const enrichStart = Date.now();
|
|
3462
3464
|
|
|
3463
3465
|
// Use node:sqlite (built-in, no deps)
|
|
3464
3466
|
let DatabaseSync;
|
|
3465
3467
|
try {
|
|
3466
3468
|
({ DatabaseSync } = await import('node:sqlite'));
|
|
3467
3469
|
} catch {
|
|
3470
|
+
logToFile('ERR', 'enrich: node:sqlite not available — requires Node.js 22.5+');
|
|
3468
3471
|
throw new Error('node:sqlite not available — requires Node.js 22.5+');
|
|
3469
3472
|
}
|
|
3470
3473
|
|
|
@@ -3491,8 +3494,10 @@ async function enrichMethodChains(root) {
|
|
|
3491
3494
|
const now = Date.now();
|
|
3492
3495
|
|
|
3493
3496
|
const phpFiles = await glob('vendor/**/*.php', { cwd: root, absolute: true, nodir: true });
|
|
3497
|
+
logToFile('INFO', `enrich: found ${phpFiles.length} PHP files in vendor/`);
|
|
3494
3498
|
let scanned = 0;
|
|
3495
3499
|
let chains = 0;
|
|
3500
|
+
let readErrors = 0;
|
|
3496
3501
|
|
|
3497
3502
|
const insertStmt = db.prepare(
|
|
3498
3503
|
'INSERT INTO method_chains (file, line, chain, first_method, second_method, has_null_guard, updated_at) VALUES (?,?,?,?,?,?,?)'
|
|
@@ -3519,11 +3524,18 @@ async function enrichMethodChains(root) {
|
|
|
3519
3524
|
return lo + 1; // 1-based
|
|
3520
3525
|
}
|
|
3521
3526
|
|
|
3527
|
+
// Progress logging every 10k files
|
|
3528
|
+
const progressInterval = 10000;
|
|
3529
|
+
|
|
3522
3530
|
db.exec('BEGIN');
|
|
3523
3531
|
try {
|
|
3524
3532
|
for (const phpFile of phpFiles) {
|
|
3525
3533
|
let content;
|
|
3526
|
-
try { content = readFileSync(phpFile, 'utf-8'); } catch {
|
|
3534
|
+
try { content = readFileSync(phpFile, 'utf-8'); } catch (err) {
|
|
3535
|
+
readErrors++;
|
|
3536
|
+
if (readErrors <= 5) logToFile('WARN', `enrich: cannot read ${phpFile}: ${err.code || err.message}`);
|
|
3537
|
+
continue;
|
|
3538
|
+
}
|
|
3527
3539
|
if (!content.includes('->')) continue;
|
|
3528
3540
|
|
|
3529
3541
|
const relPath = phpFile.replace(root + '/', '');
|
|
@@ -3551,14 +3563,20 @@ async function enrichMethodChains(root) {
|
|
|
3551
3563
|
}
|
|
3552
3564
|
}
|
|
3553
3565
|
scanned++;
|
|
3566
|
+
if (scanned % progressInterval === 0) {
|
|
3567
|
+
logToFile('INFO', `enrich: progress ${scanned}/${phpFiles.length} files, ${chains} chains so far (${Date.now() - enrichStart}ms)`);
|
|
3568
|
+
}
|
|
3554
3569
|
}
|
|
3555
3570
|
db.exec('COMMIT');
|
|
3556
3571
|
} catch (err) {
|
|
3572
|
+
logToFile('ERR', `enrich: transaction failed at file ${scanned}/${phpFiles.length}: ${err.message}`);
|
|
3557
3573
|
db.exec('ROLLBACK');
|
|
3558
3574
|
throw err;
|
|
3559
3575
|
}
|
|
3560
3576
|
|
|
3561
3577
|
db.close();
|
|
3578
|
+
const enrichElapsed = Date.now() - enrichStart;
|
|
3579
|
+
logToFile('INFO', `enrich: complete — ${scanned} files scanned, ${chains} chains indexed, ${readErrors} read errors, ${enrichElapsed}ms`);
|
|
3562
3580
|
return { scanned, chains };
|
|
3563
3581
|
}
|
|
3564
3582
|
|
|
@@ -3567,15 +3585,21 @@ async function enrichMethodChains(root) {
|
|
|
3567
3585
|
*/
|
|
3568
3586
|
async function queryNullRisks(root, firstMethod, limit = 100) {
|
|
3569
3587
|
const dbPath = ENRICHMENT_DB_PATH(root);
|
|
3570
|
-
if (!existsSync(dbPath))
|
|
3588
|
+
if (!existsSync(dbPath)) {
|
|
3589
|
+
logToFile('WARN', `null_risks: enrichment.db not found at ${dbPath} — run magento_enrich first`);
|
|
3590
|
+
return null;
|
|
3591
|
+
}
|
|
3571
3592
|
|
|
3572
3593
|
let DatabaseSync;
|
|
3573
3594
|
try {
|
|
3574
3595
|
({ DatabaseSync } = await import('node:sqlite'));
|
|
3575
|
-
} catch {
|
|
3596
|
+
} catch (err) {
|
|
3597
|
+
logToFile('ERR', `null_risks: node:sqlite not available: ${err.message}`);
|
|
3576
3598
|
return null;
|
|
3577
3599
|
}
|
|
3578
3600
|
|
|
3601
|
+
const queryStart = Date.now();
|
|
3602
|
+
logToFile('INFO', `null_risks: querying firstMethod=${firstMethod || '(all)'} limit=${limit}`);
|
|
3579
3603
|
const db = new DatabaseSync(dbPath, { open: true });
|
|
3580
3604
|
let rows;
|
|
3581
3605
|
try {
|
|
@@ -3591,6 +3615,7 @@ async function queryNullRisks(root, firstMethod, limit = 100) {
|
|
|
3591
3615
|
} finally {
|
|
3592
3616
|
db.close();
|
|
3593
3617
|
}
|
|
3618
|
+
logToFile('INFO', `null_risks: ${rows.length} unsafe chain(s) found in ${Date.now() - queryStart}ms`);
|
|
3594
3619
|
return rows;
|
|
3595
3620
|
}
|
|
3596
3621
|
|
|
@@ -3604,13 +3629,22 @@ async function astSearch(pattern, searchPath, lang, maxResults) {
|
|
|
3604
3629
|
const semgrepLang = lang || 'php';
|
|
3605
3630
|
const limit = Math.min(maxResults || 50, 200);
|
|
3606
3631
|
|
|
3632
|
+
logToFile('INFO', `ast_search: pattern="${pattern}" path="${searchPath || '.'}" lang=${semgrepLang} limit=${limit}`);
|
|
3633
|
+
const astStart = Date.now();
|
|
3634
|
+
|
|
3607
3635
|
// Create a temporary empty .semgrepignore in the target directory if none exists.
|
|
3608
3636
|
// Semgrep's default ignore list includes "vendor/" which is exactly what we need to scan.
|
|
3609
3637
|
// An empty .semgrepignore overrides the defaults: https://semgrep.dev/docs/ignoring-files-folders-code/
|
|
3610
3638
|
const semgrepIgnorePath = path.join(targetPath, '.semgrepignore');
|
|
3611
3639
|
let createdSemgrepIgnore = false;
|
|
3612
3640
|
if (!existsSync(semgrepIgnorePath)) {
|
|
3613
|
-
try {
|
|
3641
|
+
try {
|
|
3642
|
+
writeFileSync(semgrepIgnorePath, '# Magector: scan vendor/ and all project files\n');
|
|
3643
|
+
createdSemgrepIgnore = true;
|
|
3644
|
+
logToFile('INFO', `ast_search: created temporary .semgrepignore at ${targetPath}`);
|
|
3645
|
+
} catch (err) {
|
|
3646
|
+
logToFile('WARN', `ast_search: failed to create .semgrepignore: ${err.message}`);
|
|
3647
|
+
}
|
|
3614
3648
|
}
|
|
3615
3649
|
|
|
3616
3650
|
const semgrepArgs = [
|
|
@@ -3633,7 +3667,11 @@ async function astSearch(pattern, searchPath, lang, maxResults) {
|
|
|
3633
3667
|
} catch (err) {
|
|
3634
3668
|
// semgrep exits non-zero when it has findings — stdout still contains valid JSON
|
|
3635
3669
|
rawOutput = err.stdout || '';
|
|
3636
|
-
if (!rawOutput)
|
|
3670
|
+
if (!rawOutput) {
|
|
3671
|
+
const errMsg = (err.stderr || err.message || '').slice(0, 500);
|
|
3672
|
+
logToFile('ERR', `ast_search: semgrep failed after ${Date.now() - astStart}ms: ${errMsg}`);
|
|
3673
|
+
throw new Error(`semgrep failed: ${errMsg}`);
|
|
3674
|
+
}
|
|
3637
3675
|
} finally {
|
|
3638
3676
|
if (createdSemgrepIgnore) { try { unlinkSync(semgrepIgnorePath); } catch { /* best effort */ } }
|
|
3639
3677
|
}
|
|
@@ -3642,10 +3680,16 @@ async function astSearch(pattern, searchPath, lang, maxResults) {
|
|
|
3642
3680
|
try {
|
|
3643
3681
|
parsed = JSON.parse(rawOutput);
|
|
3644
3682
|
} catch {
|
|
3683
|
+
logToFile('ERR', `ast_search: failed to parse semgrep JSON output (${rawOutput.length} bytes)`);
|
|
3645
3684
|
throw new Error(`Failed to parse semgrep output. First 300 chars: ${rawOutput.slice(0, 300)}`);
|
|
3646
3685
|
}
|
|
3647
3686
|
|
|
3648
3687
|
const findings = (parsed.results || []).slice(0, limit);
|
|
3688
|
+
const astElapsed = Date.now() - astStart;
|
|
3689
|
+
logToFile('INFO', `ast_search: ${findings.length} match(es) in ${astElapsed}ms (semgrep returned ${(parsed.results || []).length} total)`);
|
|
3690
|
+
if (parsed.errors && parsed.errors.length > 0) {
|
|
3691
|
+
logToFile('WARN', `ast_search: semgrep reported ${parsed.errors.length} error(s): ${parsed.errors.slice(0, 3).map(e => e.message || e.type || JSON.stringify(e)).join('; ')}`);
|
|
3692
|
+
}
|
|
3649
3693
|
return findings.map(r => ({
|
|
3650
3694
|
file: r.path.replace(root + '/', ''),
|
|
3651
3695
|
line: r.start.line,
|
|
@@ -4765,6 +4809,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4765
4809
|
const root = args.path || config.magentoRoot;
|
|
4766
4810
|
const output = rustIndex(root);
|
|
4767
4811
|
// Auto-enrich after indexing: runs in background, doesn't block response
|
|
4812
|
+
logToFile('INFO', 'Auto-enrich: starting in background after index');
|
|
4768
4813
|
enrichMethodChains(root).then(({ scanned, chains }) => {
|
|
4769
4814
|
logToFile('INFO', `Auto-enrich complete: ${scanned} files, ${chains} chains`);
|
|
4770
4815
|
}).catch(err => {
|
|
@@ -6112,8 +6157,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6112
6157
|
if (queries.length > 10) {
|
|
6113
6158
|
return { content: [{ type: 'text', text: 'Maximum 10 queries per batch.' }], isError: true };
|
|
6114
6159
|
}
|
|
6160
|
+
logToFile('INFO', `batch: ${queries.length} queries: ${queries.map(q => q.tool).join(', ')}`);
|
|
6115
6161
|
// Run batch queries in parallel using existing standalone functions
|
|
6116
6162
|
const batchResults = await Promise.all(queries.map(async (q, idx) => {
|
|
6163
|
+
const batchItemStart = Date.now();
|
|
6117
6164
|
try {
|
|
6118
6165
|
const a = q.args || {};
|
|
6119
6166
|
let text = '';
|
|
@@ -6416,8 +6463,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6416
6463
|
default:
|
|
6417
6464
|
text = `Unsupported batch tool: ${q.tool}`;
|
|
6418
6465
|
}
|
|
6466
|
+
logToFile('INFO', `batch[${idx}]: ${q.tool} completed (${Date.now() - batchItemStart}ms)`);
|
|
6419
6467
|
return { idx, tool: q.tool, text };
|
|
6420
6468
|
} catch (err) {
|
|
6469
|
+
logToFile('ERR', `batch[${idx}]: ${q.tool} failed (${Date.now() - batchItemStart}ms): ${err.message}`);
|
|
6421
6470
|
return { idx, tool: q.tool, text: `Error: ${err.message}` };
|
|
6422
6471
|
}
|
|
6423
6472
|
}));
|
|
@@ -6446,6 +6495,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6446
6495
|
}
|
|
6447
6496
|
grepArgs.push('--', args.pattern, searchPath);
|
|
6448
6497
|
let output;
|
|
6498
|
+
const grepStart = Date.now();
|
|
6449
6499
|
try {
|
|
6450
6500
|
output = execFileSync('grep', grepArgs, {
|
|
6451
6501
|
cwd: root, encoding: 'utf-8', timeout: 30000,
|
|
@@ -6455,9 +6505,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6455
6505
|
} catch (err) {
|
|
6456
6506
|
// grep returns exit code 1 when no matches found
|
|
6457
6507
|
output = err.stdout || '';
|
|
6508
|
+
if (err.killed) logToFile('WARN', `grep: timed out after 30s for pattern "${args.pattern}"`);
|
|
6458
6509
|
}
|
|
6510
|
+
const grepElapsed = Date.now() - grepStart;
|
|
6459
6511
|
const lines = output.trim().split('\n').filter(Boolean);
|
|
6460
6512
|
const total = lines.length;
|
|
6513
|
+
if (grepElapsed > 5000) logToFile('WARN', `grep: slow query "${args.pattern}" — ${total} matches in ${grepElapsed}ms`);
|
|
6461
6514
|
const truncated = lines.slice(0, maxResults);
|
|
6462
6515
|
let text = filesOnly
|
|
6463
6516
|
? `## grep (files only): \`${args.pattern}\`\nFound **${total}** file(s)${total > maxResults ? ` (showing first ${maxResults})` : ''}. Use magento_read with methodName to read specific methods.\n\n`
|
|
@@ -6619,6 +6672,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6619
6672
|
const filePath = path.join(root, args.path);
|
|
6620
6673
|
let content;
|
|
6621
6674
|
try { content = readFileSync(filePath, 'utf-8'); } catch (err) {
|
|
6675
|
+
logToFile('WARN', `read: file not found: ${args.path} (${err.code || err.message})`);
|
|
6622
6676
|
return { content: [{ type: 'text', text: `File not found: ${args.path}` }], isError: true };
|
|
6623
6677
|
}
|
|
6624
6678
|
|
|
@@ -6626,6 +6680,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6626
6680
|
if (args.methodName) {
|
|
6627
6681
|
const body = readFullMethodBody(filePath, args.methodName);
|
|
6628
6682
|
if (!body) {
|
|
6683
|
+
logToFile('WARN', `read: method "${args.methodName}" not found in ${args.path}`);
|
|
6629
6684
|
return { content: [{ type: 'text', text: `## ${args.path}\n\nMethod \`${args.methodName}\` not found in file.` }] };
|
|
6630
6685
|
}
|
|
6631
6686
|
// Find line number of the method
|