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

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,8 @@ 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 Python framework routes for
41
+ non-TypeScript repos
41
42
  - memory-code links so project knowledge points at the code it affects
42
43
  - decision intelligence for why-memory coverage, stale/weak packets, and
43
44
  important files that still lack linked repo knowledge
package/dist/kernel.js CHANGED
@@ -2830,11 +2830,43 @@ 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
+ }
2833
2865
  function extractRoutes(path, text, symbols) {
2834
2866
  const routes = [];
2835
2867
  const addRoute = (method, routePath, offset, framework, handler = null) => {
2836
2868
  const line = lineForOffset(text, offset);
2837
- const cleanRoutePath = routePath.replace(/\\/g, "");
2869
+ const cleanRoutePath = normalizeWebRoutePath(routePath);
2838
2870
  const containing = handler ? symbols.find((symbol) => symbol.path === path && symbol.name === handler) : symbolAtLine(symbols, path, line);
2839
2871
  routes.push({
2840
2872
  id: routeId(path, method, cleanRoutePath, line),
@@ -2856,6 +2888,34 @@ function extractRoutes(path, text, symbols) {
2856
2888
  const routeMatch = text.match(new RegExp(`const\\s+${match[2]}Match\\s*=\\s*url\\.pathname\\.match\\(\\s*/\\^\\\\/([^/]+)[^/]*`));
2857
2889
  addRoute(match[1], routeMatch ? `/${routeMatch[1]}/:id` : "/:dynamic", match.index ?? 0, "node-http");
2858
2890
  }
2891
+ if (extensionOf(path) === ".py") {
2892
+ const lines = text.split(/\r?\n/);
2893
+ const framework = pythonRouteFramework(text);
2894
+ for (let index = 0; index < lines.length; index += 1) {
2895
+ const line = lines[index];
2896
+ const decorator = line.match(/^\s*@(?:\w+\.)?(get|post|put|patch|delete|options|head)\s*\(\s*["']([^"']+)["']/i);
2897
+ if (decorator) {
2898
+ 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));
2899
+ const handler = handlerLine?.match(/^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)\s*\(/)?.[1] ?? null;
2900
+ addRoute(decorator[1].toUpperCase(), decorator[2], offsetForLine(text, index + 1), framework, handler);
2901
+ continue;
2902
+ }
2903
+ const flaskRoute = line.match(/^\s*@(?:\w+\.)?route\s*\(\s*["']([^"']+)["']/i);
2904
+ if (flaskRoute) {
2905
+ 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));
2906
+ const handler = handlerLine?.match(/^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)\s*\(/)?.[1] ?? null;
2907
+ const methods = line.match(/methods\s*=\s*\[([^\]]+)\]/i)?.[1];
2908
+ for (const method of parsePythonMethodList(methods))
2909
+ addRoute(method, flaskRoute[1], offsetForLine(text, index + 1), "flask", handler);
2910
+ continue;
2911
+ }
2912
+ const djangoPath = line.match(/\b(?:path|re_path)\s*\(\s*r?["']([^"']+)["']\s*,\s*([A-Za-z_][\w.]+)/);
2913
+ if (djangoPath) {
2914
+ const handler = djangoPath[2].split(".").pop() ?? null;
2915
+ addRoute("ANY", djangoPath[1], offsetForLine(text, index + 1), "django", handler);
2916
+ }
2917
+ }
2918
+ }
2859
2919
  if (/app\/api\//.test(path)) {
2860
2920
  for (const symbol of symbols.filter((symbol) => symbol.path === path && symbol.export && ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(symbol.name))) {
2861
2921
  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.28",
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": [