@kage-core/kage-graph-mcp 1.1.27 → 1.1.29

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
@@ -37,7 +37,7 @@ Restart your agent once after setup so MCP tools reload.
37
37
  - repo-local memory for decisions, runbooks, bug fixes, gotchas, conventions,
38
38
  and code explanations
39
39
  - a code graph for files, symbols, imports, calls, routes, tests, and packages,
40
- including generic call/test signals for non-TypeScript repos
40
+ including generic call/test signals and mixed-language framework routes
41
41
  - memory-code links so project knowledge points at the code it affects
42
42
  - decision intelligence for why-memory coverage, stale/weak packets, and
43
43
  important files that still lack linked repo knowledge
package/dist/kernel.js CHANGED
@@ -2830,11 +2830,62 @@ function extractGenericCalls(path, text, symbols, symbolByName) {
2830
2830
  }
2831
2831
  return calls.sort((a, b) => a.line - b.line || a.to_symbol.localeCompare(b.to_symbol));
2832
2832
  }
2833
+ function offsetForLine(text, oneBasedLine) {
2834
+ if (oneBasedLine <= 1)
2835
+ return 0;
2836
+ const lines = text.split(/\r?\n/).slice(0, oneBasedLine - 1);
2837
+ return lines.join("\n").length + (lines.length ? 1 : 0);
2838
+ }
2839
+ function normalizeWebRoutePath(routePath) {
2840
+ let cleaned = routePath
2841
+ .trim()
2842
+ .replace(/^r(["'`])|(["'`])$/g, "")
2843
+ .replace(/^['"`]|['"`]$/g, "")
2844
+ .replace(/\\/g, "")
2845
+ .replace(/^\^/, "")
2846
+ .replace(/\$$/, "")
2847
+ .replace(/\{([A-Za-z_][\w]*)\}/g, ":$1")
2848
+ .replace(/<(?:(?:int|str|slug|uuid|path):)?([A-Za-z_][\w]*)>/g, ":$1")
2849
+ .replace(/\/+/g, "/");
2850
+ if (!cleaned.startsWith("/"))
2851
+ cleaned = `/${cleaned}`;
2852
+ if (cleaned.length > 1)
2853
+ cleaned = cleaned.replace(/\/$/, "");
2854
+ return cleaned || "/";
2855
+ }
2856
+ function pythonRouteFramework(text) {
2857
+ return /\bfrom\s+flask\s+import\b|\bimport\s+flask\b|\bFlask\s*\(/.test(text) ? "flask" : "fastapi";
2858
+ }
2859
+ function parsePythonMethodList(value) {
2860
+ if (!value)
2861
+ return ["GET"];
2862
+ const methods = [...value.matchAll(/["']([A-Za-z]+)["']/g)].map((match) => match[1].toUpperCase());
2863
+ return methods.length ? unique(methods) : ["GET"];
2864
+ }
2865
+ const SPRING_ROUTE_METHODS = {
2866
+ GetMapping: "GET",
2867
+ PostMapping: "POST",
2868
+ PutMapping: "PUT",
2869
+ PatchMapping: "PATCH",
2870
+ DeleteMapping: "DELETE",
2871
+ RequestMapping: "ANY",
2872
+ };
2873
+ const ASPNET_ROUTE_METHODS = {
2874
+ Get: "GET",
2875
+ Post: "POST",
2876
+ Put: "PUT",
2877
+ Patch: "PATCH",
2878
+ Delete: "DELETE",
2879
+ };
2880
+ function routeHandlerNearLine(lines, startIndex, pattern) {
2881
+ const handlerLine = lines.slice(startIndex + 1, Math.min(lines.length, startIndex + 8)).find((candidate) => pattern.test(candidate));
2882
+ return handlerLine?.match(pattern)?.[1] ?? null;
2883
+ }
2833
2884
  function extractRoutes(path, text, symbols) {
2834
2885
  const routes = [];
2835
2886
  const addRoute = (method, routePath, offset, framework, handler = null) => {
2836
2887
  const line = lineForOffset(text, offset);
2837
- const cleanRoutePath = routePath.replace(/\\/g, "");
2888
+ const cleanRoutePath = normalizeWebRoutePath(routePath);
2838
2889
  const containing = handler ? symbols.find((symbol) => symbol.path === path && symbol.name === handler) : symbolAtLine(symbols, path, line);
2839
2890
  routes.push({
2840
2891
  id: routeId(path, method, cleanRoutePath, line),
@@ -2856,6 +2907,92 @@ function extractRoutes(path, text, symbols) {
2856
2907
  const routeMatch = text.match(new RegExp(`const\\s+${match[2]}Match\\s*=\\s*url\\.pathname\\.match\\(\\s*/\\^\\\\/([^/]+)[^/]*`));
2857
2908
  addRoute(match[1], routeMatch ? `/${routeMatch[1]}/:id` : "/:dynamic", match.index ?? 0, "node-http");
2858
2909
  }
2910
+ if (extensionOf(path) === ".py") {
2911
+ const lines = text.split(/\r?\n/);
2912
+ const framework = pythonRouteFramework(text);
2913
+ for (let index = 0; index < lines.length; index += 1) {
2914
+ const line = lines[index];
2915
+ const decorator = line.match(/^\s*@(?:\w+\.)?(get|post|put|patch|delete|options|head)\s*\(\s*["']([^"']+)["']/i);
2916
+ if (decorator) {
2917
+ const handlerLine = lines.slice(index + 1, Math.min(lines.length, index + 7)).find((candidate) => /^\s*(?:async\s+)?def\s+[A-Za-z_][\w]*\s*\(/.test(candidate));
2918
+ const handler = handlerLine?.match(/^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)\s*\(/)?.[1] ?? null;
2919
+ addRoute(decorator[1].toUpperCase(), decorator[2], offsetForLine(text, index + 1), framework, handler);
2920
+ continue;
2921
+ }
2922
+ const flaskRoute = line.match(/^\s*@(?:\w+\.)?route\s*\(\s*["']([^"']+)["']/i);
2923
+ if (flaskRoute) {
2924
+ const handlerLine = lines.slice(index + 1, Math.min(lines.length, index + 7)).find((candidate) => /^\s*(?:async\s+)?def\s+[A-Za-z_][\w]*\s*\(/.test(candidate));
2925
+ const handler = handlerLine?.match(/^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)\s*\(/)?.[1] ?? null;
2926
+ const methods = line.match(/methods\s*=\s*\[([^\]]+)\]/i)?.[1];
2927
+ for (const method of parsePythonMethodList(methods))
2928
+ addRoute(method, flaskRoute[1], offsetForLine(text, index + 1), "flask", handler);
2929
+ continue;
2930
+ }
2931
+ const djangoPath = line.match(/\b(?:path|re_path)\s*\(\s*r?["']([^"']+)["']\s*,\s*([A-Za-z_][\w.]+)/);
2932
+ if (djangoPath) {
2933
+ const handler = djangoPath[2].split(".").pop() ?? null;
2934
+ addRoute("ANY", djangoPath[1], offsetForLine(text, index + 1), "django", handler);
2935
+ }
2936
+ }
2937
+ }
2938
+ if (extensionOf(path) === ".rb") {
2939
+ for (const match of text.matchAll(/\b(get|post|put|patch|delete)\s+["']([^"']+)["']/gi)) {
2940
+ addRoute(match[1].toUpperCase(), match[2], match.index ?? 0, "rails");
2941
+ }
2942
+ }
2943
+ if (extensionOf(path) === ".php") {
2944
+ for (const match of text.matchAll(/\bRoute::(get|post|put|patch|delete|options|any)\s*\(\s*["']([^"']+)["']/gi)) {
2945
+ addRoute(match[1].toUpperCase(), match[2], match.index ?? 0, "laravel");
2946
+ }
2947
+ }
2948
+ if ([".java", ".kt"].includes(extensionOf(path))) {
2949
+ const lines = text.split(/\r?\n/);
2950
+ for (let index = 0; index < lines.length; index += 1) {
2951
+ const line = lines[index];
2952
+ const mapping = line.match(/@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*(?:\(\s*(?:value\s*=\s*)?["']([^"']+)["'])?/);
2953
+ if (!mapping || !mapping[2])
2954
+ continue;
2955
+ let method = SPRING_ROUTE_METHODS[mapping[1]] ?? "ANY";
2956
+ if (mapping[1] === "RequestMapping") {
2957
+ const explicit = line.match(/RequestMethod\.(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)/);
2958
+ if (explicit)
2959
+ method = explicit[1];
2960
+ }
2961
+ const handler = routeHandlerNearLine(lines, index, /^\s*(?:public|private|protected)?\s*[\w<>\[\], ?]+\s+([A-Za-z_][\w]*)\s*\(/);
2962
+ addRoute(method, mapping[2], offsetForLine(text, index + 1), "spring", handler);
2963
+ }
2964
+ }
2965
+ if (extensionOf(path) === ".go") {
2966
+ for (const match of text.matchAll(/\b[A-Za-z_][\w]*\.(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*["`]([^"`]+)["`]\s*,\s*([A-Za-z_][\w.]*)?/g)) {
2967
+ addRoute(match[1], match[2], match.index ?? 0, "go-router", match[3]?.split(".").pop() ?? null);
2968
+ }
2969
+ }
2970
+ if (extensionOf(path) === ".rs") {
2971
+ for (const match of text.matchAll(/\.route\s*\(\s*["']([^"']+)["']\s*,\s*(get|post|put|patch|delete|options|head)\s*\(\s*([A-Za-z_][\w:]*)?/gi)) {
2972
+ addRoute(match[2].toUpperCase(), match[1], match.index ?? 0, "rust-router", match[3]?.split("::").pop() ?? null);
2973
+ }
2974
+ const lines = text.split(/\r?\n/);
2975
+ for (let index = 0; index < lines.length; index += 1) {
2976
+ const attr = lines[index].match(/#\[(get|post|put|patch|delete|options|head)\(\s*["']([^"']+)["']\s*\)\]/i);
2977
+ if (!attr)
2978
+ continue;
2979
+ const handler = routeHandlerNearLine(lines, index, /^\s*(?:pub\s+)?(?:async\s+)?fn\s+([A-Za-z_][\w]*)\s*\(/);
2980
+ addRoute(attr[1].toUpperCase(), attr[2], offsetForLine(text, index + 1), "rust-router", handler);
2981
+ }
2982
+ }
2983
+ if (extensionOf(path) === ".cs") {
2984
+ for (const match of text.matchAll(/\bMap(Get|Post|Put|Patch|Delete)\s*\(\s*["']([^"']+)["']\s*,\s*([A-Za-z_][\w.]*)?/g)) {
2985
+ addRoute(ASPNET_ROUTE_METHODS[match[1]] ?? match[1].toUpperCase(), match[2], match.index ?? 0, "aspnet", match[3]?.split(".").pop() ?? null);
2986
+ }
2987
+ const lines = text.split(/\r?\n/);
2988
+ for (let index = 0; index < lines.length; index += 1) {
2989
+ const attr = lines[index].match(/\[\s*Http(Get|Post|Put|Patch|Delete)?\s*\(\s*["']([^"']+)["']\s*\)\s*\]/);
2990
+ if (!attr)
2991
+ continue;
2992
+ const handler = routeHandlerNearLine(lines, index, /^\s*(?:public|private|protected|internal)?\s*(?:async\s+)?[\w<>\[\], ?]+\s+([A-Za-z_][\w]*)\s*\(/);
2993
+ addRoute(attr[1] ? ASPNET_ROUTE_METHODS[attr[1]] ?? attr[1].toUpperCase() : "ANY", attr[2], offsetForLine(text, index + 1), "aspnet", handler);
2994
+ }
2995
+ }
2859
2996
  if (/app\/api\//.test(path)) {
2860
2997
  for (const symbol of symbols.filter((symbol) => symbol.path === path && symbol.export && ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(symbol.name))) {
2861
2998
  const apiPath = `/${path.replace(/^.*app\/api\//, "").replace(/\/route\.[cm]?[jt]sx?$/, "").replace(/\[([^\]]+)\]/g, ":$1")}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kage-core/kage-graph-mcp",
3
- "version": "1.1.27",
3
+ "version": "1.1.29",
4
4
  "description": "Local-first repo memory, code graph, and recall MCP server for coding agents",
5
5
  "main": "dist/index.js",
6
6
  "files": [