magector 2.16.12 → 2.16.14

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 +175 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.16.12",
3
+ "version": "2.16.14",
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.16.12",
37
- "@magector/cli-linux-x64": "2.16.12",
38
- "@magector/cli-linux-arm64": "2.16.12",
39
- "@magector/cli-win32-x64": "2.16.12"
36
+ "@magector/cli-darwin-arm64": "2.16.14",
37
+ "@magector/cli-linux-x64": "2.16.14",
38
+ "@magector/cli-linux-arm64": "2.16.14",
39
+ "@magector/cli-win32-x64": "2.16.14"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -1687,10 +1687,184 @@ async function traceApi(entryPoint, depth) {
1687
1687
  return trace;
1688
1688
  }
1689
1689
 
1690
+ /**
1691
+ * Parse .graphqls schema files structurally to find a GraphQL operation
1692
+ * (query/mutation/subscription) by name and extract its @resolver directive.
1693
+ *
1694
+ * This is the structural-first counterpart to semantic search in traceGraphql().
1695
+ * Handles both escaped ("\\\\Magento\\\\…") and bare ("Magento\\…") resolver paths
1696
+ * with or without a leading backslash, and tolerates intermediate directives
1697
+ * (e.g. @doc(…)) between the operation signature and @resolver(…).
1698
+ *
1699
+ * @param {string} entryPoint — GraphQL operation name (e.g. "addProductsToCart")
1700
+ * @returns {Promise<Array<{path, line, resolverClass, snippet}>>}
1701
+ */
1702
+ async function parseGraphqlSchema(entryPoint) {
1703
+ const root = config.magentoRoot;
1704
+ const schemas = [];
1705
+
1706
+ let graphqlsFiles;
1707
+ try {
1708
+ graphqlsFiles = await glob('**/etc/**/*.graphqls', {
1709
+ cwd: root,
1710
+ absolute: true,
1711
+ nodir: true,
1712
+ ignore: ['**/test/**', '**/tests/**', '**/Test/**', '**/Tests/**', '**/node_modules/**']
1713
+ });
1714
+ } catch {
1715
+ return schemas;
1716
+ }
1717
+
1718
+ const escapedName = entryPoint.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1719
+ const lineRe = new RegExp(`^\\s*${escapedName}\\s*(?:\\(|:)`);
1720
+
1721
+ for (const file of graphqlsFiles) {
1722
+ let content;
1723
+ try { content = readFileSync(file, 'utf-8'); } catch { continue; }
1724
+ if (!content.includes(entryPoint)) continue;
1725
+
1726
+ const lines = content.split('\n');
1727
+ for (let i = 0; i < lines.length; i++) {
1728
+ if (!lineRe.test(lines[i])) continue;
1729
+
1730
+ // Accumulate up to 10 lines (covers multi-line operation signatures) until
1731
+ // we find the @resolver directive or exit the current definition.
1732
+ let block = '';
1733
+ for (let j = i; j < Math.min(i + 10, lines.length); j++) {
1734
+ block += lines[j] + '\n';
1735
+ if (/@resolver\s*\(/.test(block)) break;
1736
+ }
1737
+
1738
+ const resolverMatch = block.match(/@resolver\s*\(\s*class\s*:\s*"([^"]+)"\s*\)/);
1739
+ if (!resolverMatch) continue;
1740
+
1741
+ // Normalize: unescape doubled backslashes, strip any leading backslashes
1742
+ const resolverClass = resolverMatch[1].replace(/\\\\/g, '\\').replace(/^\\+/, '');
1743
+ schemas.push({
1744
+ path: file.replace(root + '/', ''),
1745
+ line: i + 1,
1746
+ resolverClass,
1747
+ snippet: block.trim().slice(0, 400)
1748
+ });
1749
+ }
1750
+ }
1751
+
1752
+ return schemas;
1753
+ }
1754
+
1755
+ /**
1756
+ * Resolve a PHP class FQCN to a filesystem path.
1757
+ * Tries standard Magento locations first (app/code, vendor/), then globs by
1758
+ * short name and verifies the namespace matches. Returns null if not found.
1759
+ */
1760
+ async function resolveClassFileFromRoot(className) {
1761
+ const root = config.magentoRoot;
1762
+ if (!className) return null;
1763
+ const nsPath = className.replace(/\\/g, '/') + '.php';
1764
+ const candidates = [
1765
+ path.join(root, 'app', 'code', nsPath),
1766
+ path.join(root, 'vendor', nsPath)
1767
+ ];
1768
+ for (const c of candidates) {
1769
+ if (existsSync(c)) return c;
1770
+ }
1771
+
1772
+ const shortName = className.split('\\').pop();
1773
+ let matches;
1774
+ try {
1775
+ matches = await glob(`**/${shortName}.php`, {
1776
+ cwd: root, absolute: true, nodir: true,
1777
+ ignore: ['**/test/**', '**/tests/**', '**/Test/**', '**/Tests/**', '**/node_modules/**']
1778
+ });
1779
+ } catch { return null; }
1780
+
1781
+ for (const match of matches) {
1782
+ let content;
1783
+ try { content = readFileSync(match, 'utf-8'); } catch { continue; }
1784
+ const nsMatch = content.match(/namespace\s+([\w\\]+)/);
1785
+ const classMatch = content.match(/(?:class|abstract\s+class|final\s+class|interface|trait)\s+(\w+)/);
1786
+ if (classMatch && classMatch[1] === shortName) {
1787
+ const fqcn = nsMatch ? `${nsMatch[1]}\\${classMatch[1]}` : classMatch[1];
1788
+ if (fqcn === className) return match;
1789
+ }
1790
+ }
1791
+ return null;
1792
+ }
1793
+
1690
1794
  async function traceGraphql(entryPoint, depth) {
1691
1795
  const trace = {};
1692
1796
 
1693
- // Schema + resolver (independent, run in parallel)
1797
+ // Step 1 STRUCTURAL: parse .graphqls files for the operation definition.
1798
+ // Short GraphQL operation names (addProductsToCart, placeOrder) don't have
1799
+ // enough context for embedding-based search to reliably locate them, so we
1800
+ // parse the schema files directly. Semantic search remains as a fallback.
1801
+ const schemaMatches = await parseGraphqlSchema(entryPoint);
1802
+
1803
+ if (schemaMatches.length > 0) {
1804
+ trace.schema = schemaMatches.slice(0, 5).map(s => ({
1805
+ path: s.path,
1806
+ line: s.line,
1807
+ resolverClass: s.resolverClass,
1808
+ snippet: s.snippet
1809
+ }));
1810
+
1811
+ // A single operation name may resolve to multiple classes when a schema is
1812
+ // extended/overridden across modules; deduplicate while preserving order.
1813
+ const uniqueResolverClasses = [...new Set(schemaMatches.map(s => s.resolverClass))];
1814
+ const resolverEntries = [];
1815
+ for (const cls of uniqueResolverClasses) {
1816
+ const filePath = await resolveClassFileFromRoot(cls);
1817
+ const entry = { className: cls };
1818
+ if (filePath) {
1819
+ entry.path = filePath.replace(config.magentoRoot + '/', '');
1820
+ // GraphQL resolvers implement ResolverInterface::resolve()
1821
+ const snippet = readMethodSnippet(filePath, 'resolve', 30);
1822
+ if (snippet) entry.codeSnippet = snippet;
1823
+ } else {
1824
+ entry.path = null;
1825
+ entry.note = 'Resolver class referenced in schema but file not located on disk';
1826
+ }
1827
+ resolverEntries.push(entry);
1828
+ }
1829
+ // Primary resolver = first match (backwards-compatible with existing shape).
1830
+ // When multiple distinct resolvers exist, expose extras under additionalResolvers.
1831
+ if (resolverEntries.length > 0) {
1832
+ trace.resolver = resolverEntries[0];
1833
+ if (resolverEntries.length > 1) {
1834
+ trace.additionalResolvers = resolverEntries.slice(1);
1835
+ }
1836
+ }
1837
+
1838
+ // Step 2 — deep mode: walk DI graph for plugins/preferences on every
1839
+ // resolver class we found.
1840
+ if (depth === 'deep') {
1841
+ const pluginEntries = [];
1842
+ const preferenceEntries = [];
1843
+ for (const cls of uniqueResolverClasses) {
1844
+ let di;
1845
+ try { di = await findDiWiring(cls); } catch { continue; }
1846
+ if (!di) continue;
1847
+ for (const p of di.plugins || []) {
1848
+ pluginEntries.push({
1849
+ target: p.target,
1850
+ pluginName: p.pluginName,
1851
+ pluginClass: p.pluginClass,
1852
+ file: p.file
1853
+ });
1854
+ }
1855
+ for (const pref of di.preferences || []) {
1856
+ preferenceEntries.push({ for: pref.for, type: pref.type, file: pref.file });
1857
+ }
1858
+ }
1859
+ if (pluginEntries.length > 0) trace.plugins = pluginEntries.slice(0, 15);
1860
+ if (preferenceEntries.length > 0) trace.preferences = preferenceEntries.slice(0, 10);
1861
+ }
1862
+
1863
+ return trace;
1864
+ }
1865
+
1866
+ // Step 3 — FALLBACK: semantic search (retains the prior behaviour so that
1867
+ // indexed but atypically-named operations aren't completely invisible).
1694
1868
  const [schemaRaw, resolverRaw] = await Promise.all([
1695
1869
  safeSearch(`graphql ${entryPoint} mutation query`, 20),
1696
1870
  safeSearch(`${entryPoint} resolver`, 20)