magector 2.5.1 → 2.5.2

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 (2) hide show
  1. package/package.json +5 -5
  2. package/src/mcp-server.js +105 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.5.1",
3
+ "version": "2.5.2",
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.5.1",
37
- "@magector/cli-linux-x64": "2.5.1",
38
- "@magector/cli-linux-arm64": "2.5.1",
39
- "@magector/cli-win32-x64": "2.5.1"
36
+ "@magector/cli-darwin-arm64": "2.5.2",
37
+ "@magector/cli-linux-x64": "2.5.2",
38
+ "@magector/cli-linux-arm64": "2.5.2",
39
+ "@magector/cli-win32-x64": "2.5.2"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -1004,6 +1004,41 @@ function readMethodSnippet(filePath, methodName, maxLines = 15) {
1004
1004
  return null;
1005
1005
  }
1006
1006
 
1007
+ /**
1008
+ * Read the FULL method body using brace-counting to find the closing brace.
1009
+ * Unlike readMethodSnippet (fixed N lines), this extracts the complete method
1010
+ * regardless of length, up to a safety limit.
1011
+ * @param {string} filePath - absolute or relative PHP file path
1012
+ * @param {string} methodName - method name to find
1013
+ * @param {number} maxLines - safety limit to prevent reading huge methods (default: 60)
1014
+ * @returns {string|null} complete method source or null if not found
1015
+ */
1016
+ function readFullMethodBody(filePath, methodName, maxLines = 60) {
1017
+ const absPath = filePath.startsWith('/') ? filePath : path.join(config.magentoRoot, filePath);
1018
+ let content;
1019
+ try { content = readFileSync(absPath, 'utf-8'); } catch { return null; }
1020
+ const lines = content.split('\n');
1021
+ for (let i = 0; i < lines.length; i++) {
1022
+ if (lines[i].includes(`function ${methodName}(`)) {
1023
+ // Use brace counting to find the complete method body
1024
+ let braceCount = 0;
1025
+ let started = false;
1026
+ for (let j = i; j < lines.length && j < i + maxLines; j++) {
1027
+ for (const ch of lines[j]) {
1028
+ if (ch === '{') { braceCount++; started = true; }
1029
+ if (ch === '}') braceCount--;
1030
+ }
1031
+ if (started && braceCount <= 0) {
1032
+ return lines.slice(i, j + 1).join('\n');
1033
+ }
1034
+ }
1035
+ // Safety: if closing brace not found within maxLines, return what we have
1036
+ return lines.slice(i, Math.min(i + maxLines, lines.length)).join('\n');
1037
+ }
1038
+ }
1039
+ return null;
1040
+ }
1041
+
1007
1042
  /**
1008
1043
  * Parse all fieldset.xml files in the Magento root.
1009
1044
  * Returns array of { file, scope, fieldset, fields: [{ field, aspect }] }.
@@ -1568,8 +1603,12 @@ function formatSearchResults(results) {
1568
1603
  : r.searchText;
1569
1604
  }
1570
1605
 
1606
+ // Full method body — attached by magento_find_method for complete understanding
1607
+ if (r.fullMethodBody) {
1608
+ entry.codePreview = r.fullMethodBody;
1609
+ }
1571
1610
  // Code preview — read actual source lines for PHP files with known class/method
1572
- if (r.path && (r.path.endsWith('.php') || r.path.endsWith('.phtml'))) {
1611
+ else if (r.path && (r.path.endsWith('.php') || r.path.endsWith('.phtml'))) {
1573
1612
  if (r.methodName) {
1574
1613
  const preview = readMethodSnippet(r.path, r.methodName, 10);
1575
1614
  if (preview) entry.codePreview = preview;
@@ -2553,6 +2592,21 @@ async function findDiWiring(className) {
2553
2592
  const root = config.magentoRoot;
2554
2593
  const shortName = className.split('\\').pop();
2555
2594
  const shortLower = shortName.toLowerCase();
2595
+ // When a FQCN is provided (contains \), use it for precise matching
2596
+ const hasFqcn = className.includes('\\');
2597
+ // Normalize FQCN for XML matching: di.xml uses backslash-escaped names
2598
+ const fqcnNormalized = hasFqcn ? className.replace(/\\\\/g, '\\') : null;
2599
+ const fqcnLower = fqcnNormalized ? fqcnNormalized.toLowerCase() : null;
2600
+
2601
+ // Helper: check if a di.xml class name matches the requested class.
2602
+ // When FQCN is available, require full namespace match; otherwise fall back to short name.
2603
+ function matchesClass(xmlClassName) {
2604
+ const xmlLower = xmlClassName.toLowerCase().replace(/\\\\/g, '\\');
2605
+ if (fqcnLower) {
2606
+ return xmlLower === fqcnLower || xmlLower.endsWith('\\' + fqcnLower);
2607
+ }
2608
+ return xmlLower.includes(shortLower);
2609
+ }
2556
2610
 
2557
2611
  const result = {
2558
2612
  className,
@@ -2573,15 +2627,14 @@ async function findDiWiring(className) {
2573
2627
  const relativePath = diFile.replace(root + '/', '');
2574
2628
  const contentLower = content.toLowerCase();
2575
2629
 
2630
+ // Quick pre-filter: skip files that don't contain the short name at all
2576
2631
  if (!contentLower.includes(shortLower)) continue;
2577
2632
 
2578
2633
  // 1. Preferences where this class is the "for" or "type"
2579
2634
  const prefRegex = /<preference\s+for="([^"]+)"\s+type="([^"]+)"\s*\/?>/g;
2580
2635
  let m;
2581
2636
  while ((m = prefRegex.exec(content)) !== null) {
2582
- const forLower = m[1].toLowerCase();
2583
- const typeLower = m[2].toLowerCase();
2584
- if (forLower.includes(shortLower) || typeLower.includes(shortLower)) {
2637
+ if (matchesClass(m[1]) || matchesClass(m[2])) {
2585
2638
  result.preferences.push({ for: m[1], type: m[2], file: relativePath });
2586
2639
  }
2587
2640
  }
@@ -2591,10 +2644,9 @@ async function findDiWiring(className) {
2591
2644
  let typeMatch;
2592
2645
  while ((typeMatch = typeBlockRegex.exec(content)) !== null) {
2593
2646
  const typeName = typeMatch[1];
2594
- const typeNameLower = typeName.toLowerCase();
2595
2647
  const typeBlock = typeMatch[2];
2596
2648
 
2597
- if (!typeNameLower.includes(shortLower)) continue;
2649
+ if (!matchesClass(typeName)) continue;
2598
2650
 
2599
2651
  // Plugins
2600
2652
  const pluginRegex = /<plugin\s+name="([^"]+)"[^>]*type="([^"]+)"[^>]*/g;
@@ -2641,7 +2693,7 @@ async function findDiWiring(className) {
2641
2693
  const vtRegex = /<virtualType\s+name="([^"]+)"[^>]*type="([^"]+)"[^>]*(?:\/>|>([\s\S]*?)<\/virtualType>)/g;
2642
2694
  while ((m = vtRegex.exec(content)) !== null) {
2643
2695
  const vtType = m[2];
2644
- if (!vtType.toLowerCase().includes(shortLower)) continue;
2696
+ if (!matchesClass(vtType)) continue;
2645
2697
  const vtEntry = { name: m[1], type: vtType, file: relativePath };
2646
2698
  if (m[3]) {
2647
2699
  const argRegex = /<argument\s+name="([^"]+)"[^>]*xsi:type="([^"]+)"[^>]*>([^<]*)<\/argument>/g;
@@ -2657,6 +2709,7 @@ async function findDiWiring(className) {
2657
2709
  }
2658
2710
 
2659
2711
  // 4. Constructor arguments from PHP class
2712
+ // When FQCN is provided, verify namespace matches to avoid class name collisions
2660
2713
  const phpFiles = await glob(`**/${shortName}.php`, { cwd: root, absolute: true, nodir: true });
2661
2714
  for (const phpFile of phpFiles) {
2662
2715
  let content;
@@ -2664,6 +2717,17 @@ async function findDiWiring(className) {
2664
2717
  const classMatch = content.match(/(?:class|abstract\s+class)\s+(\w+)/);
2665
2718
  if (!classMatch || classMatch[1] !== shortName) continue;
2666
2719
 
2720
+ // FQCN namespace verification: when a full class name was provided,
2721
+ // check that the PHP file's namespace matches to avoid collisions
2722
+ // (e.g., two different ViewPlugin classes in different modules)
2723
+ if (fqcnNormalized) {
2724
+ const nsMatch = content.match(/namespace\s+([\w\\]+)\s*;/);
2725
+ if (nsMatch) {
2726
+ const fileFqcn = (nsMatch[1] + '\\' + shortName).toLowerCase();
2727
+ if (fileFqcn !== fqcnLower) continue; // Wrong class, skip
2728
+ }
2729
+ }
2730
+
2667
2731
  const ctorMatch = content.match(/function\s+__construct\s*\(([\s\S]*?)\)\s*[{:]/);
2668
2732
  if (ctorMatch) {
2669
2733
  const paramRegex = /(?:([\w\\]+)\s+)?(\$\w+)/g;
@@ -2672,6 +2736,7 @@ async function findDiWiring(className) {
2672
2736
  result.constructorArguments.push({ typeHint: pm[1] || null, variable: pm[2] });
2673
2737
  }
2674
2738
  }
2739
+ result.constructorSourceFile = phpFile.replace(root + '/', '');
2675
2740
  break;
2676
2741
  }
2677
2742
 
@@ -3992,10 +4057,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3992
4057
  if (r.methodName?.toLowerCase() === methodLower) bonus += 0.3;
3993
4058
  return { ...r, score: (r.score || 0) + bonus };
3994
4059
  }).sort((a, b) => b.score - a.score);
4060
+ // Attach full method body to each result for complete understanding
4061
+ const sliced = results.slice(0, 10);
4062
+ for (const r of sliced) {
4063
+ if (r.path && r.path.endsWith('.php')) {
4064
+ const body = readFullMethodBody(r.path, args.methodName);
4065
+ if (body) r.fullMethodBody = body;
4066
+ }
4067
+ }
3995
4068
  return {
3996
4069
  content: [{
3997
4070
  type: 'text',
3998
- text: formatSearchResults(results.slice(0, 10))
4071
+ text: formatSearchResults(sliced)
3999
4072
  }]
4000
4073
  };
4001
4074
  }
@@ -5394,6 +5467,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5394
5467
  for (const o of flow.observers.slice(0, 10)) text += `- ${o.name}: ${o.instance} (${o.file})\n`;
5395
5468
  break;
5396
5469
  }
5470
+ case 'magento_find_di_wiring': {
5471
+ const wiring = await findDiWiring(a.className);
5472
+ text = `Prefs: ${wiring.preferences.length}, Plugins: ${wiring.plugins.length}, VTs: ${wiring.virtualTypes.length}, Ctor: ${wiring.constructorArguments.length}\n`;
5473
+ for (const p of wiring.plugins.slice(0, 5)) text += `- plugin: ${p.pluginName} → ${p.pluginClass}\n`;
5474
+ for (const c of wiring.constructorArguments.slice(0, 10)) text += `- ctor: ${c.typeHint || '?'} ${c.variable}\n`;
5475
+ if (wiring.constructorSourceFile) text += `Source: ${wiring.constructorSourceFile}\n`;
5476
+ break;
5477
+ }
5478
+ case 'magento_find_method': {
5479
+ const qr = `method ${a.methodName} function ${a.className || ''}`.trim();
5480
+ const raw = await rustSearchAsync(qr, 50);
5481
+ const ml = a.methodName.toLowerCase();
5482
+ let res = raw.map(normalizeResult).filter(r =>
5483
+ r.methodName?.toLowerCase() === ml || r.methods?.some(m => m.toLowerCase() === ml)
5484
+ );
5485
+ for (const r of res.slice(0, 5)) {
5486
+ if (r.path?.endsWith('.php')) {
5487
+ const body = readFullMethodBody(r.path, a.methodName);
5488
+ if (body) r.fullMethodBody = body;
5489
+ }
5490
+ }
5491
+ text = formatSearchResults(res.slice(0, 5));
5492
+ break;
5493
+ }
5397
5494
  default:
5398
5495
  text = `Unsupported batch tool: ${q.tool}`;
5399
5496
  }