magector 2.7.0 → 2.7.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 +99 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.7.0",
3
+ "version": "2.7.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.7.0",
37
- "@magector/cli-linux-x64": "2.7.0",
38
- "@magector/cli-linux-arm64": "2.7.0",
39
- "@magector/cli-win32-x64": "2.7.0"
36
+ "@magector/cli-darwin-arm64": "2.7.2",
37
+ "@magector/cli-linux-x64": "2.7.2",
38
+ "@magector/cli-linux-arm64": "2.7.2",
39
+ "@magector/cli-win32-x64": "2.7.2"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -2303,6 +2303,33 @@ async function analyzeImpact(className) {
2303
2303
  ...diTrace.virtualTypes.map(v => ({ type: 'virtualType', file: v.file, detail: `${v.name} extends ${v.type}` })),
2304
2304
  ...diTrace.argumentOverrides.map(a => ({ type: 'argument', file: a.file, detail: `${a.target}.${a.argumentName}` }))
2305
2305
  ];
2306
+ // Also find where this class is used AS a plugin/preference/virtualType implementation
2307
+ if (references.diXmlReferences.length === 0 && root) {
2308
+ const diFiles = await getDiXmlFiles(root);
2309
+ for (const { content, relPath } of diFiles) {
2310
+ if (!content.toLowerCase().includes(shortName.toLowerCase())) continue;
2311
+ // Check if class appears as plugin type
2312
+ const pluginTypeRegex = /<plugin\s+[^>]*type="([^"]+)"[^>]*\/?>/g;
2313
+ let pm;
2314
+ while ((pm = pluginTypeRegex.exec(content)) !== null) {
2315
+ if (pm[1].toLowerCase().includes(shortName.toLowerCase())) {
2316
+ // Find parent <type> to know the target
2317
+ const beforePlugin = content.slice(0, pm.index);
2318
+ const typeMatch = beforePlugin.match(/<type\s+name="([^"]+)"[^>]*>\s*$/m);
2319
+ const target = typeMatch ? typeMatch[1] : 'unknown';
2320
+ references.diXmlReferences.push({ type: 'registered-as-plugin', file: relPath, detail: `plugin on ${target} → ${pm[1]}` });
2321
+ }
2322
+ }
2323
+ // Check if class appears as preference type
2324
+ const prefRegex = /<preference\s+for="([^"]+)"\s+type="([^"]+)"\s*\/?>/g;
2325
+ let prm;
2326
+ while ((prm = prefRegex.exec(content)) !== null) {
2327
+ if (prm[2].toLowerCase().includes(shortName.toLowerCase())) {
2328
+ references.diXmlReferences.push({ type: 'registered-as-preference', file: relPath, detail: `preference for ${prm[1]} → ${prm[2]}` });
2329
+ }
2330
+ }
2331
+ }
2332
+ }
2306
2333
 
2307
2334
  // Check PHP files for direct references
2308
2335
  const escapedShort = shortName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -5640,18 +5667,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5640
5667
  const res = raw.map(normalizeResult).filter(r =>
5641
5668
  r.magentoType === 'plugin' || r.path?.toLowerCase().includes('plugin')
5642
5669
  );
5643
- text = formatSearchResults(res.slice(0, 5));
5644
- // DI registrations + method bodies (same as standalone find_plugin)
5670
+ text = formatSearchResults(res.slice(0, 3));
5671
+ // DI registrations + method bodies (compact: only targetMethod bodies)
5645
5672
  if (a.targetClass) {
5646
5673
  const diFiles = await getDiXmlFiles(config.magentoRoot);
5647
5674
  const normalizedTarget = a.targetClass.replace(/\\\\/g, '\\');
5648
5675
  const isFqcn = normalizedTarget.includes('\\');
5649
5676
  const shortTarget = normalizedTarget.split('\\').pop().toLowerCase();
5677
+ let regCount = 0;
5678
+ text += '\n\n### DI Registrations\n';
5650
5679
  for (const { content: diContent, relPath } of diFiles) {
5680
+ if (regCount >= 8) break;
5651
5681
  if (!diContent.includes(isFqcn ? normalizedTarget : a.targetClass)) continue;
5652
5682
  const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
5653
5683
  let tm;
5654
5684
  while ((tm = typeBlockRegex.exec(diContent)) !== null) {
5685
+ if (regCount >= 8) break;
5655
5686
  const typeName = tm[1].replace(/\\\\/g, '\\');
5656
5687
  const typeMatches = isFqcn ? typeName === normalizedTarget : typeName.split('\\').pop().toLowerCase() === shortTarget;
5657
5688
  if (!typeMatches) continue;
@@ -5659,22 +5690,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5659
5690
  const pluginRegex = /<plugin\s+([^/>]*)\/?>/g;
5660
5691
  let pm;
5661
5692
  while ((pm = pluginRegex.exec(block)) !== null) {
5693
+ if (regCount >= 8) break;
5662
5694
  const attrs = {};
5663
5695
  const localAttrRe = /(\w+)="([^"]*)"/g;
5664
- let am;
5665
- while ((am = localAttrRe.exec(pm[1])) !== null) attrs[am[1]] = am[2];
5666
- text += `\n- **${attrs.name || '?'}** \`${attrs.type || '?'}\` (${relPath})`;
5667
- if (attrs.type) {
5696
+ let am2;
5697
+ while ((am2 = localAttrRe.exec(pm[1])) !== null) attrs[am2[1]] = am2[2];
5698
+ const disabled = attrs.disabled === 'true' ? ' [DISABLED]' : '';
5699
+ text += `- **${attrs.name || '?'}** → \`${attrs.type || '?'}\`${disabled} (${relPath})\n`;
5700
+ // Only read method bodies for plugins that intercept the targetMethod
5701
+ if (attrs.type && a.targetMethod) {
5668
5702
  const pFile = findClassFile(config.magentoRoot, attrs.type);
5669
5703
  if (pFile) {
5670
5704
  const methods = extractPluginMethods(pFile);
5671
- for (const m of methods) {
5705
+ const relevant = methods.filter(m => m.targetMethod === a.targetMethod);
5706
+ for (const m of relevant) {
5672
5707
  const body = readFullMethodBody(pFile, m.name);
5673
- text += `\n - \`${m.type}\` **${m.targetMethod}** → \`${m.name}()\``;
5674
- if (body) text += '\n ' + '```php\n ' + body.split('\n').join('\n ') + '\n ' + '```';
5708
+ text += ` - \`${m.type}\` **${m.targetMethod}** → \`${m.name}()\`\n`;
5709
+ if (body) text += ' ' + '```php\n ' + body.split('\n').join('\n ') + '\n ' + '```\n';
5675
5710
  }
5676
5711
  }
5677
5712
  }
5713
+ regCount++;
5678
5714
  }
5679
5715
  }
5680
5716
  }
@@ -5740,9 +5776,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5740
5776
  let res = raw.map(normalizeResult).filter(r =>
5741
5777
  r.methodName?.toLowerCase() === ml || r.methods?.some(m => m.toLowerCase() === ml)
5742
5778
  );
5779
+ // Filesystem fallback: grep -rl for method signature
5780
+ if (res.length === 0 && config.magentoRoot) {
5781
+ const methodSig = `function ${a.methodName}(`;
5782
+ const classShort = a.className ? a.className.split('\\').pop() : null;
5783
+ try {
5784
+ let files = [];
5785
+ if (classShort) {
5786
+ files = await glob(`**/${classShort}.php`, { cwd: config.magentoRoot, absolute: false, nodir: true });
5787
+ } else {
5788
+ const grepResult = execFileSync('grep', ['-rl', '--include=*.php', methodSig, '.'],
5789
+ { cwd: config.magentoRoot, encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] });
5790
+ files = grepResult.trim().split('\n').filter(Boolean).map(f => f.replace(/^\.\//, ''));
5791
+ }
5792
+ for (const f of files.slice(0, 10)) {
5793
+ const absP = f.startsWith('/') ? f : path.join(config.magentoRoot, f);
5794
+ let content;
5795
+ try { content = readFileSync(absP, 'utf-8'); } catch { continue; }
5796
+ if (!content.includes(methodSig)) continue;
5797
+ let cn = null;
5798
+ const nsM = content.match(/namespace\s+([\w\\]+)/);
5799
+ const clM = content.match(/class\s+(\w+)/);
5800
+ if (nsM && clM) cn = nsM[1] + '\\' + clM[1];
5801
+ const body = readFullMethodBody(absP, a.methodName);
5802
+ res.push({ path: f, className: cn, methodName: a.methodName, score: 0.5, fullMethodBody: body || undefined });
5803
+ if (res.length >= 5) break;
5804
+ }
5805
+ } catch {}
5806
+ }
5743
5807
  for (const r of res.slice(0, 5)) {
5744
- if (r.path?.endsWith('.php')) {
5745
- const body = readFullMethodBody(r.path, a.methodName);
5808
+ if (r.path?.endsWith('.php') && !r.fullMethodBody) {
5809
+ const body = readFullMethodBody(path.join(config.magentoRoot, r.path), a.methodName);
5746
5810
  if (body) r.fullMethodBody = body;
5747
5811
  }
5748
5812
  }
@@ -5752,13 +5816,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5752
5816
  case 'magento_module_structure': {
5753
5817
  const raw = await rustSearchAsync(a.moduleName, 200);
5754
5818
  const modulePath = a.moduleName.replace('_', '/') + '/';
5755
- const parts = a.moduleName.split('_');
5756
- const vendorPath = parts.length === 2 ? `module-${parts[1].toLowerCase()}/` : '';
5757
- const res = raw.map(normalizeResult).filter(r => {
5819
+ const mParts = a.moduleName.split('_');
5820
+ // Hyphenate camelCase for vendor path: OrderSplit order-split
5821
+ const vendorPath = mParts.length === 2
5822
+ ? `module-${mParts[1].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}/`
5823
+ : '';
5824
+ let res = raw.map(normalizeResult).filter(r => {
5758
5825
  const p = r.path || '';
5759
5826
  const mod = r.module || '';
5760
5827
  return mod === a.moduleName || p.includes(modulePath) || (vendorPath && p.toLowerCase().includes(vendorPath));
5761
5828
  });
5829
+ // Filesystem fallback
5830
+ if (res.length === 0 && config.magentoRoot && vendorPath) {
5831
+ try {
5832
+ const vendorGlob = `**/${vendorPath}**/*.{php,xml,phtml}`;
5833
+ const files = await glob(vendorGlob, { cwd: config.magentoRoot, absolute: false, nodir: true });
5834
+ for (const f of files.slice(0, 100)) {
5835
+ const entry = { path: f, score: 0.5 };
5836
+ if (f.includes('/Controller/')) entry.isController = true;
5837
+ if (f.includes('/Model/')) entry.isModel = true;
5838
+ if (f.includes('/Plugin/')) entry.isPlugin = true;
5839
+ if (f.includes('/Observer/')) entry.isObserver = true;
5840
+ if (f.endsWith('.xml')) entry.type = 'xml';
5841
+ const phpMatch = f.match(/\/([A-Z]\w+)\.php$/);
5842
+ if (phpMatch) entry.className = phpMatch[1];
5843
+ res.push(entry);
5844
+ }
5845
+ } catch {}
5846
+ }
5762
5847
  text = `Module: ${a.moduleName} (${res.length} files)\n`;
5763
5848
  const cats = { controllers: '/Controller/', models: '/Model/', plugins: '/Plugin/', observers: '/Observer/', api: '/Api/' };
5764
5849
  for (const [cat, pattern] of Object.entries(cats)) {