@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 +2 -1
- package/dist/kernel.js +61 -1
- package/package.json +1 -1
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
|
|
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
|
|
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")}`;
|