magector 2.13.3 → 2.14.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/README.md CHANGED
@@ -1204,6 +1204,67 @@ gantt
1204
1204
 
1205
1205
  ---
1206
1206
 
1207
+ ## Troubleshooting
1208
+
1209
+ All MCP server activity is logged to `.magector/magector.log` in the Magento project root. The log persists across MCP restarts and uses the format:
1210
+
1211
+ ```
1212
+ [2026-04-12T18:30:00.000Z] [LEVEL] message
1213
+ ```
1214
+
1215
+ ### Log Levels
1216
+
1217
+ | Level | Meaning |
1218
+ |-------|---------|
1219
+ | `INFO` | Normal operations: startup config, tool completion, search fallbacks, enrichment progress |
1220
+ | `WARN` | Recoverable issues: slow grep queries (>5s), missing enrichment.db, file read errors, serve process disconnects |
1221
+ | `ERR` | Failures: semgrep crashes, transaction rollbacks, serve process errors, tool execution errors |
1222
+ | `REQ` | Every tool call with full input parameters (JSON) |
1223
+ | `RES` | Tool completion with elapsed time in milliseconds |
1224
+ | `QUERY` | Rust serve process queries (search, feedback) |
1225
+ | `CACHE` | Search cache hits |
1226
+ | `INDEX` | Background reindex progress |
1227
+ | `SERVE` | Rust serve process stderr (watcher events, model loading) |
1228
+ | `FATAL` | Server startup failures |
1229
+
1230
+ ### Common Diagnostic Commands
1231
+
1232
+ ```bash
1233
+ # Recent errors
1234
+ grep '\[ERR\]\|\[FATAL\]' .magector/magector.log | tail -20
1235
+
1236
+ # Tool timing (find slow tools)
1237
+ grep '\[RES\]' .magector/magector.log | tail -20
1238
+
1239
+ # Enrichment/null-risk analysis
1240
+ grep 'enrich:\|null_risks:' .magector/magector.log | tail -20
1241
+
1242
+ # AST search (semgrep) issues
1243
+ grep 'ast_search:' .magector/magector.log | tail -20
1244
+
1245
+ # Batch query breakdown (per-tool timing)
1246
+ grep 'batch\[' .magector/magector.log | tail -20
1247
+
1248
+ # Slow grep queries
1249
+ grep 'grep: slow\|grep: timed' .magector/magento.log | tail -20
1250
+
1251
+ # Full startup sequence
1252
+ grep 'server starting\|Config:\|primary\|Serve process' .magector/magector.log | tail -30
1253
+ ```
1254
+
1255
+ ### What Gets Logged (v2.14+)
1256
+
1257
+ Every tool call logs `[REQ]` with input parameters and `[RES]` with elapsed time. Additionally:
1258
+
1259
+ - **`magento_ast_search`** — semgrep pattern, target path, execution time, result count, semgrep errors
1260
+ - **`magento_enrich`** — file count, progress every 10k files, read errors, transaction failures, final summary
1261
+ - **`magento_find_null_risks`** — query parameters, result count, query timing, missing DB warnings
1262
+ - **`magento_batch`** — query list on entry, per-sub-tool timing and errors
1263
+ - **`magento_grep`** — slow query warnings (>5s), timeout detection
1264
+ - **`magento_read`** — file-not-found with error codes, failed method extractions
1265
+
1266
+ ---
1267
+
1207
1268
  ## License
1208
1269
 
1209
1270
  MIT License. See [LICENSE](LICENSE) for details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.13.3",
3
+ "version": "2.14.1",
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.13.3",
37
- "@magector/cli-linux-x64": "2.13.3",
38
- "@magector/cli-linux-arm64": "2.13.3",
39
- "@magector/cli-win32-x64": "2.13.3"
36
+ "@magector/cli-darwin-arm64": "2.14.1",
37
+ "@magector/cli-linux-x64": "2.14.1",
38
+ "@magector/cli-linux-arm64": "2.14.1",
39
+ "@magector/cli-win32-x64": "2.14.1"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -149,6 +149,44 @@ const SOCK_PATH = path.join(config.magentoRoot, '.magector', 'serve.sock');
149
149
  const FORMAT_CACHE_PATH = path.join(config.magentoRoot, '.magector', 'format-ok.json');
150
150
  const PRIMARY_LOCK_PATH = path.join(config.magentoRoot, '.magector', 'primary.lock');
151
151
 
152
+ /**
153
+ * Expand brace patterns in include globs for GNU grep compatibility.
154
+ * GNU grep --include does NOT support brace expansion (that's a shell feature).
155
+ * Transforms "*.{php,xml,graphqls}" → ["*.php", "*.xml", "*.graphqls"]
156
+ * Also handles comma-separated patterns: "*.php, *.xml" → ["*.php", "*.xml"]
157
+ * And mixed: "*.{php,xml}, *.phtml" → ["*.php", "*.xml", "*.phtml"]
158
+ */
159
+ function expandIncludePattern(include) {
160
+ // Split on commas NOT inside braces
161
+ const parts = [];
162
+ let depth = 0, current = '';
163
+ for (const ch of include) {
164
+ if (ch === '{') depth++;
165
+ if (ch === '}') depth--;
166
+ if (ch === ',' && depth === 0) {
167
+ parts.push(current.trim());
168
+ current = '';
169
+ } else {
170
+ current += ch;
171
+ }
172
+ }
173
+ if (current.trim()) parts.push(current.trim());
174
+ // Expand brace patterns in each part
175
+ const patterns = [];
176
+ const braceRegex = /^(.*?)\{([^}]+)\}(.*)$/;
177
+ for (const part of parts) {
178
+ const m = part.match(braceRegex);
179
+ if (m) {
180
+ for (const alt of m[2].split(',').map(a => a.trim())) {
181
+ patterns.push(m[1] + alt + m[3]);
182
+ }
183
+ } else {
184
+ patterns.push(part);
185
+ }
186
+ }
187
+ return patterns;
188
+ }
189
+
152
190
  /**
153
191
  * Try to acquire the primary lock (O_EXCL = atomic create-or-fail).
154
192
  * Returns true if we are the primary instance, false if another instance holds the lock.
@@ -3459,12 +3497,15 @@ function hasNullGuard(lines, matchLineIdx, receiverExpr, guardRadius = 6) {
3459
3497
  */
3460
3498
  async function enrichMethodChains(root) {
3461
3499
  const dbPath = ENRICHMENT_DB_PATH(root);
3500
+ logToFile('INFO', `enrich: starting method-chain scan, db=${dbPath}`);
3501
+ const enrichStart = Date.now();
3462
3502
 
3463
3503
  // Use node:sqlite (built-in, no deps)
3464
3504
  let DatabaseSync;
3465
3505
  try {
3466
3506
  ({ DatabaseSync } = await import('node:sqlite'));
3467
3507
  } catch {
3508
+ logToFile('ERR', 'enrich: node:sqlite not available — requires Node.js 22.5+');
3468
3509
  throw new Error('node:sqlite not available — requires Node.js 22.5+');
3469
3510
  }
3470
3511
 
@@ -3491,8 +3532,10 @@ async function enrichMethodChains(root) {
3491
3532
  const now = Date.now();
3492
3533
 
3493
3534
  const phpFiles = await glob('vendor/**/*.php', { cwd: root, absolute: true, nodir: true });
3535
+ logToFile('INFO', `enrich: found ${phpFiles.length} PHP files in vendor/`);
3494
3536
  let scanned = 0;
3495
3537
  let chains = 0;
3538
+ let readErrors = 0;
3496
3539
 
3497
3540
  const insertStmt = db.prepare(
3498
3541
  'INSERT INTO method_chains (file, line, chain, first_method, second_method, has_null_guard, updated_at) VALUES (?,?,?,?,?,?,?)'
@@ -3519,11 +3562,18 @@ async function enrichMethodChains(root) {
3519
3562
  return lo + 1; // 1-based
3520
3563
  }
3521
3564
 
3565
+ // Progress logging every 10k files
3566
+ const progressInterval = 10000;
3567
+
3522
3568
  db.exec('BEGIN');
3523
3569
  try {
3524
3570
  for (const phpFile of phpFiles) {
3525
3571
  let content;
3526
- try { content = readFileSync(phpFile, 'utf-8'); } catch { continue; }
3572
+ try { content = readFileSync(phpFile, 'utf-8'); } catch (err) {
3573
+ readErrors++;
3574
+ if (readErrors <= 5) logToFile('WARN', `enrich: cannot read ${phpFile}: ${err.code || err.message}`);
3575
+ continue;
3576
+ }
3527
3577
  if (!content.includes('->')) continue;
3528
3578
 
3529
3579
  const relPath = phpFile.replace(root + '/', '');
@@ -3551,14 +3601,20 @@ async function enrichMethodChains(root) {
3551
3601
  }
3552
3602
  }
3553
3603
  scanned++;
3604
+ if (scanned % progressInterval === 0) {
3605
+ logToFile('INFO', `enrich: progress ${scanned}/${phpFiles.length} files, ${chains} chains so far (${Date.now() - enrichStart}ms)`);
3606
+ }
3554
3607
  }
3555
3608
  db.exec('COMMIT');
3556
3609
  } catch (err) {
3610
+ logToFile('ERR', `enrich: transaction failed at file ${scanned}/${phpFiles.length}: ${err.message}`);
3557
3611
  db.exec('ROLLBACK');
3558
3612
  throw err;
3559
3613
  }
3560
3614
 
3561
3615
  db.close();
3616
+ const enrichElapsed = Date.now() - enrichStart;
3617
+ logToFile('INFO', `enrich: complete — ${scanned} files scanned, ${chains} chains indexed, ${readErrors} read errors, ${enrichElapsed}ms`);
3562
3618
  return { scanned, chains };
3563
3619
  }
3564
3620
 
@@ -3567,15 +3623,21 @@ async function enrichMethodChains(root) {
3567
3623
  */
3568
3624
  async function queryNullRisks(root, firstMethod, limit = 100) {
3569
3625
  const dbPath = ENRICHMENT_DB_PATH(root);
3570
- if (!existsSync(dbPath)) return null;
3626
+ if (!existsSync(dbPath)) {
3627
+ logToFile('WARN', `null_risks: enrichment.db not found at ${dbPath} — run magento_enrich first`);
3628
+ return null;
3629
+ }
3571
3630
 
3572
3631
  let DatabaseSync;
3573
3632
  try {
3574
3633
  ({ DatabaseSync } = await import('node:sqlite'));
3575
- } catch {
3634
+ } catch (err) {
3635
+ logToFile('ERR', `null_risks: node:sqlite not available: ${err.message}`);
3576
3636
  return null;
3577
3637
  }
3578
3638
 
3639
+ const queryStart = Date.now();
3640
+ logToFile('INFO', `null_risks: querying firstMethod=${firstMethod || '(all)'} limit=${limit}`);
3579
3641
  const db = new DatabaseSync(dbPath, { open: true });
3580
3642
  let rows;
3581
3643
  try {
@@ -3591,6 +3653,7 @@ async function queryNullRisks(root, firstMethod, limit = 100) {
3591
3653
  } finally {
3592
3654
  db.close();
3593
3655
  }
3656
+ logToFile('INFO', `null_risks: ${rows.length} unsafe chain(s) found in ${Date.now() - queryStart}ms`);
3594
3657
  return rows;
3595
3658
  }
3596
3659
 
@@ -3604,13 +3667,22 @@ async function astSearch(pattern, searchPath, lang, maxResults) {
3604
3667
  const semgrepLang = lang || 'php';
3605
3668
  const limit = Math.min(maxResults || 50, 200);
3606
3669
 
3670
+ logToFile('INFO', `ast_search: pattern="${pattern}" path="${searchPath || '.'}" lang=${semgrepLang} limit=${limit}`);
3671
+ const astStart = Date.now();
3672
+
3607
3673
  // Create a temporary empty .semgrepignore in the target directory if none exists.
3608
3674
  // Semgrep's default ignore list includes "vendor/" which is exactly what we need to scan.
3609
3675
  // An empty .semgrepignore overrides the defaults: https://semgrep.dev/docs/ignoring-files-folders-code/
3610
3676
  const semgrepIgnorePath = path.join(targetPath, '.semgrepignore');
3611
3677
  let createdSemgrepIgnore = false;
3612
3678
  if (!existsSync(semgrepIgnorePath)) {
3613
- try { writeFileSync(semgrepIgnorePath, '# Magector: scan vendor/ and all project files\n'); createdSemgrepIgnore = true; } catch { /* best effort */ }
3679
+ try {
3680
+ writeFileSync(semgrepIgnorePath, '# Magector: scan vendor/ and all project files\n');
3681
+ createdSemgrepIgnore = true;
3682
+ logToFile('INFO', `ast_search: created temporary .semgrepignore at ${targetPath}`);
3683
+ } catch (err) {
3684
+ logToFile('WARN', `ast_search: failed to create .semgrepignore: ${err.message}`);
3685
+ }
3614
3686
  }
3615
3687
 
3616
3688
  const semgrepArgs = [
@@ -3633,7 +3705,11 @@ async function astSearch(pattern, searchPath, lang, maxResults) {
3633
3705
  } catch (err) {
3634
3706
  // semgrep exits non-zero when it has findings — stdout still contains valid JSON
3635
3707
  rawOutput = err.stdout || '';
3636
- if (!rawOutput) throw new Error(`semgrep failed: ${(err.stderr || err.message || '').slice(0, 500)}`);
3708
+ if (!rawOutput) {
3709
+ const errMsg = (err.stderr || err.message || '').slice(0, 500);
3710
+ logToFile('ERR', `ast_search: semgrep failed after ${Date.now() - astStart}ms: ${errMsg}`);
3711
+ throw new Error(`semgrep failed: ${errMsg}`);
3712
+ }
3637
3713
  } finally {
3638
3714
  if (createdSemgrepIgnore) { try { unlinkSync(semgrepIgnorePath); } catch { /* best effort */ } }
3639
3715
  }
@@ -3642,10 +3718,16 @@ async function astSearch(pattern, searchPath, lang, maxResults) {
3642
3718
  try {
3643
3719
  parsed = JSON.parse(rawOutput);
3644
3720
  } catch {
3721
+ logToFile('ERR', `ast_search: failed to parse semgrep JSON output (${rawOutput.length} bytes)`);
3645
3722
  throw new Error(`Failed to parse semgrep output. First 300 chars: ${rawOutput.slice(0, 300)}`);
3646
3723
  }
3647
3724
 
3648
3725
  const findings = (parsed.results || []).slice(0, limit);
3726
+ const astElapsed = Date.now() - astStart;
3727
+ logToFile('INFO', `ast_search: ${findings.length} match(es) in ${astElapsed}ms (semgrep returned ${(parsed.results || []).length} total)`);
3728
+ if (parsed.errors && parsed.errors.length > 0) {
3729
+ 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('; ')}`);
3730
+ }
3649
3731
  return findings.map(r => ({
3650
3732
  file: r.path.replace(root + '/', ''),
3651
3733
  line: r.start.line,
@@ -4765,6 +4847,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4765
4847
  const root = args.path || config.magentoRoot;
4766
4848
  const output = rustIndex(root);
4767
4849
  // Auto-enrich after indexing: runs in background, doesn't block response
4850
+ logToFile('INFO', 'Auto-enrich: starting in background after index');
4768
4851
  enrichMethodChains(root).then(({ scanned, chains }) => {
4769
4852
  logToFile('INFO', `Auto-enrich complete: ${scanned} files, ${chains} chains`);
4770
4853
  }).catch(err => {
@@ -6112,8 +6195,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6112
6195
  if (queries.length > 10) {
6113
6196
  return { content: [{ type: 'text', text: 'Maximum 10 queries per batch.' }], isError: true };
6114
6197
  }
6198
+ logToFile('INFO', `batch: ${queries.length} queries: ${queries.map(q => q.tool).join(', ')}`);
6115
6199
  // Run batch queries in parallel using existing standalone functions
6116
6200
  const batchResults = await Promise.all(queries.map(async (q, idx) => {
6201
+ const batchItemStart = Date.now();
6117
6202
  try {
6118
6203
  const a = q.args || {};
6119
6204
  let text = '';
@@ -6374,7 +6459,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6374
6459
  const gArgs = batchFilesOnly ? ['-rl', '-E'] : ['-rn', '-E'];
6375
6460
  if (a.ignoreCase) gArgs.push('-i');
6376
6461
  if (!batchFilesOnly && batchCtx > 0) gArgs.push('-C', String(batchCtx));
6377
- for (const pat of include.split(',').map(p => p.trim())) gArgs.push('--include=' + pat);
6462
+ for (const pat of expandIncludePattern(include)) gArgs.push('--include=' + pat);
6378
6463
  gArgs.push('--', a.pattern, searchPath);
6379
6464
  let out;
6380
6465
  try {
@@ -6416,8 +6501,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6416
6501
  default:
6417
6502
  text = `Unsupported batch tool: ${q.tool}`;
6418
6503
  }
6504
+ logToFile('INFO', `batch[${idx}]: ${q.tool} completed (${Date.now() - batchItemStart}ms)`);
6419
6505
  return { idx, tool: q.tool, text };
6420
6506
  } catch (err) {
6507
+ logToFile('ERR', `batch[${idx}]: ${q.tool} failed (${Date.now() - batchItemStart}ms): ${err.message}`);
6421
6508
  return { idx, tool: q.tool, text: `Error: ${err.message}` };
6422
6509
  }
6423
6510
  }));
@@ -6440,12 +6527,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6440
6527
  const grepArgs = filesOnly ? ['-rl', '-E'] : ['-rn', '-E'];
6441
6528
  if (args.ignoreCase) grepArgs.push('-i');
6442
6529
  if (!filesOnly && ctxLines > 0) grepArgs.push('-C', String(ctxLines));
6443
- // Support multiple include patterns (e.g., "*.{php,xml}")
6444
- for (const pat of include.split(',').map(p => p.trim())) {
6530
+ for (const pat of expandIncludePattern(include)) {
6445
6531
  grepArgs.push('--include=' + pat);
6446
6532
  }
6447
6533
  grepArgs.push('--', args.pattern, searchPath);
6448
6534
  let output;
6535
+ const grepStart = Date.now();
6449
6536
  try {
6450
6537
  output = execFileSync('grep', grepArgs, {
6451
6538
  cwd: root, encoding: 'utf-8', timeout: 30000,
@@ -6455,9 +6542,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6455
6542
  } catch (err) {
6456
6543
  // grep returns exit code 1 when no matches found
6457
6544
  output = err.stdout || '';
6545
+ if (err.killed) logToFile('WARN', `grep: timed out after 30s for pattern "${args.pattern}"`);
6458
6546
  }
6547
+ const grepElapsed = Date.now() - grepStart;
6459
6548
  const lines = output.trim().split('\n').filter(Boolean);
6460
6549
  const total = lines.length;
6550
+ if (grepElapsed > 5000) logToFile('WARN', `grep: slow query "${args.pattern}" — ${total} matches in ${grepElapsed}ms`);
6461
6551
  const truncated = lines.slice(0, maxResults);
6462
6552
  let text = filesOnly
6463
6553
  ? `## 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 +6709,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6619
6709
  const filePath = path.join(root, args.path);
6620
6710
  let content;
6621
6711
  try { content = readFileSync(filePath, 'utf-8'); } catch (err) {
6712
+ logToFile('WARN', `read: file not found: ${args.path} (${err.code || err.message})`);
6622
6713
  return { content: [{ type: 'text', text: `File not found: ${args.path}` }], isError: true };
6623
6714
  }
6624
6715
 
@@ -6626,6 +6717,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6626
6717
  if (args.methodName) {
6627
6718
  const body = readFullMethodBody(filePath, args.methodName);
6628
6719
  if (!body) {
6720
+ logToFile('WARN', `read: method "${args.methodName}" not found in ${args.path}`);
6629
6721
  return { content: [{ type: 'text', text: `## ${args.path}\n\nMethod \`${args.methodName}\` not found in file.` }] };
6630
6722
  }
6631
6723
  // Find line number of the method