universal-ast-mapper 1.5.0 → 1.7.0
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 +15 -0
- package/dist/cli.js +23 -0
- package/dist/explorer.js +69 -0
- package/dist/index.js +66 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,6 +98,7 @@ ast-map complexity <path> [alias: cx] [--min N]
|
|
|
98
98
|
ast-map unused-params <path> [alias: unused]
|
|
99
99
|
ast-map trace-type <type> [dir] [alias: flow]
|
|
100
100
|
ast-map workspace [dir] [alias: ws]
|
|
101
|
+
ast-map explore [dir] [-o out.html]
|
|
101
102
|
ast-map search <pattern> [dir] [-m contains|exact|regex] [-k kind] [-e]
|
|
102
103
|
ast-map deps <file> [--scan <dir>]
|
|
103
104
|
ast-map top <dir> [-n 10]
|
|
@@ -562,6 +563,18 @@ src/
|
|
|
562
563
|
|
|
563
564
|
---
|
|
564
565
|
|
|
566
|
+
## MCP resources
|
|
567
|
+
|
|
568
|
+
Beyond tools, the server exposes the codebase as **browseable MCP resources**, so an agent (or MCP client UI) can list and read structure directly:
|
|
569
|
+
|
|
570
|
+
| URI | What |
|
|
571
|
+
|-----|------|
|
|
572
|
+
| `ast://languages` | supported languages + extensions |
|
|
573
|
+
| `ast://skeleton/{path}` | the skeleton for one source file (templated; `resources/list` enumerates every file) |
|
|
574
|
+
| `ast://graph` | the whole-root symbol dependency graph (guarded by file count) |
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
565
578
|
## GitHub Action — architecture gate in CI
|
|
566
579
|
|
|
567
580
|
Use AST-MCP as a CI check with the bundled composite action (`action.yml`):
|
|
@@ -605,6 +618,8 @@ Not part of the public API: the internal `src/` module layout and the generated
|
|
|
605
618
|
|
|
606
619
|
| Version | What changed |
|
|
607
620
|
|---------|--------------|
|
|
621
|
+
| **1.7.0** | **Web UI graph explorer** — `ast-map explore [dir]` writes a self-contained, dependency-free interactive HTML: a force-directed file dependency graph (drag, zoom, click-to-highlight neighbours, filter by name). No build step, no external scripts — just open it in a browser. |
|
|
622
|
+
| **1.6.0** | **MCP resource endpoints** — the server now exposes browseable resources: `ast://languages`, `ast://skeleton/{path}` (templated, one per source file via `resources/list`), and `ast://graph`. Agents can list and read codebase structure as resources, not just call tools. |
|
|
608
623
|
| **1.5.0** | **`.d.ts` / ambient declarations** — `declare function/const/class`, `declare module "x"`, and `declare namespace` (and plain `namespace`) are now extracted (previously a `.d.ts` yielded 0 symbols). Adds a `namespace` symbol kind; declared APIs surface as exported, nested under their module/namespace. |
|
|
609
624
|
| **1.4.0** | **Dynamic import tracking** — dynamic `import("...")` and CommonJS `require("...")` calls (anywhere in a file) are now captured as imports with an `isDynamic` flag. Relative dynamic imports resolve and draw graph edges like static ones, so lazy-loaded routes/modules show up in the dependency graph. |
|
|
610
625
|
| **1.3.0** | **TS/JS decorators** — class and method symbols now carry a `decorators` field (`@Component({...})`, `@Injectable()`, `@Get("/x")`), in skeletons and `get_call_graph`. Extends the Python decorator support (v0.8.7) to TypeScript/JavaScript — traces Angular/NestJS-style framework wiring to its class/handler. |
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { computeFileComplexity } from "./complexity.js";
|
|
|
14
14
|
import { findUnusedParams } from "./unused-params.js";
|
|
15
15
|
import { traceTypeInFile } from "./typeflow.js";
|
|
16
16
|
import { discoverWorkspace, findPackageCycles } from "./workspace.js";
|
|
17
|
+
import { buildExplorerHtml } from "./explorer.js";
|
|
17
18
|
import { buildCallGraph } from "./callgraph.js";
|
|
18
19
|
import { searchSymbols } from "./search.js";
|
|
19
20
|
const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
|
|
@@ -370,6 +371,28 @@ program
|
|
|
370
371
|
}
|
|
371
372
|
console.log();
|
|
372
373
|
});
|
|
374
|
+
// ─── Command: explore ─────────────────────────────────────────────────────────
|
|
375
|
+
program
|
|
376
|
+
.command("explore [dir]")
|
|
377
|
+
.description("Generate an interactive HTML dependency-graph explorer")
|
|
378
|
+
.option("-o, --out <file>", "Output HTML path")
|
|
379
|
+
.action(async (dir, opts) => {
|
|
380
|
+
const { abs, rel } = resolveArg(dir ?? ".");
|
|
381
|
+
if (!fs.statSync(abs).isDirectory())
|
|
382
|
+
die(`"${rel}" is not a directory`);
|
|
383
|
+
const skeletons = await gatherSkeletons(abs);
|
|
384
|
+
const graph = buildSymbolGraph(skeletons, ROOT);
|
|
385
|
+
const html = buildExplorerHtml(graph, abs);
|
|
386
|
+
const outPath = opts.out
|
|
387
|
+
? path.resolve(process.cwd(), opts.out)
|
|
388
|
+
: path.join(abs, "ast-explorer.html");
|
|
389
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
390
|
+
fs.writeFileSync(outPath, html, "utf8");
|
|
391
|
+
header(`Graph Explorer — ${rel}/ ${dim(`(${skeletons.length} files)`)}`);
|
|
392
|
+
console.log(indent(green("✓ wrote " + path.relative(process.cwd(), outPath))));
|
|
393
|
+
console.log(indent(dim("open it in a browser — drag nodes, scroll to zoom, click to highlight, filter by name")));
|
|
394
|
+
console.log();
|
|
395
|
+
});
|
|
373
396
|
// ─── Command: workspace ───────────────────────────────────────────────────────
|
|
374
397
|
program
|
|
375
398
|
.command("workspace [dir]")
|
package/dist/explorer.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Derive a file-level dependency graph (nodes = files, edges = imports). */
|
|
2
|
+
function deriveFileGraph(graph) {
|
|
3
|
+
const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
4
|
+
const nodes = [];
|
|
5
|
+
for (const n of graph.nodes) {
|
|
6
|
+
if (n.nodeType !== "file")
|
|
7
|
+
continue;
|
|
8
|
+
const f = n;
|
|
9
|
+
const parts = f.id.split("/");
|
|
10
|
+
nodes.push({ id: f.id, symbols: f.symbolCount, group: parts.length > 1 ? parts[0] : "(root)", lang: f.language });
|
|
11
|
+
}
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const links = [];
|
|
14
|
+
for (const e of graph.edges) {
|
|
15
|
+
if (e.edgeType !== "imports")
|
|
16
|
+
continue;
|
|
17
|
+
const to = nodeMap.get(e.to);
|
|
18
|
+
const toFile = to ? (to.nodeType === "file" ? to.id : to.file) : null;
|
|
19
|
+
if (!toFile || e.from === toFile)
|
|
20
|
+
continue;
|
|
21
|
+
const key = e.from + "|" + toFile;
|
|
22
|
+
if (seen.has(key))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(key);
|
|
25
|
+
links.push({ source: e.from, target: toFile });
|
|
26
|
+
}
|
|
27
|
+
return { nodes, links };
|
|
28
|
+
}
|
|
29
|
+
const STYLE = "body{margin:0;font-family:system-ui,sans-serif;color:#222;background:#fafafa}" +
|
|
30
|
+
"#bar{position:fixed;top:0;left:0;right:0;height:48px;display:flex;align-items:center;gap:12px;padding:0 14px;background:#fff;border-bottom:1px solid #e5e5e5;z-index:2;box-sizing:border-box}" +
|
|
31
|
+
"#bar h1{font-size:14px;margin:0;font-weight:600}#bar .muted{color:#888;font-size:12px}" +
|
|
32
|
+
"#q{flex:0 0 220px;padding:6px 10px;border:1px solid #ddd;border-radius:6px;font-size:13px}" +
|
|
33
|
+
"#cv{position:fixed;top:48px;left:0;right:0;bottom:0;display:block;cursor:grab}" +
|
|
34
|
+
"#tip{position:fixed;pointer-events:none;background:#222;color:#fff;font-size:12px;padding:4px 8px;border-radius:5px;display:none;z-index:3}" +
|
|
35
|
+
"@media(prefers-color-scheme:dark){body{color:#ddd;background:#161616}#bar{background:#1e1e1e;border-color:#333}#q{background:#2a2a2a;border-color:#444;color:#ddd}}";
|
|
36
|
+
const CLIENT = "var c=document.getElementById('cv'),ctx=c.getContext('2d'),tip=document.getElementById('tip');" +
|
|
37
|
+
"var W,H;function resize(){var r=devicePixelRatio||1;W=c.clientWidth;H=c.clientHeight;c.width=W*r;c.height=H*r;ctx.setTransform(r,0,0,r,0,0);}addEventListener('resize',resize);resize();" +
|
|
38
|
+
"var nodes=DATA.nodes,links=DATA.links,byId={};nodes.forEach(function(n){n.x=Math.random()*W;n.y=Math.random()*H;n.vx=0;n.vy=0;byId[n.id]=n;});" +
|
|
39
|
+
"var groups={},gi=0;function color(g){if(groups[g]==null)groups[g]=gi++;return 'hsl('+((groups[g]*67)%360)+',58%,55%)';}" +
|
|
40
|
+
"var adj={};links.forEach(function(l){(adj[l.source]=adj[l.source]||[]).push(l.target);(adj[l.target]=adj[l.target]||[]).push(l.source);});" +
|
|
41
|
+
"var view={x:0,y:0,k:1},sel=null,hover=null,drag=null,pan=null,q='';" +
|
|
42
|
+
"function radius(n){return 4+Math.sqrt(n.symbols||0)*1.7;}" +
|
|
43
|
+
"function tick(){var k=0.0007;for(var i=0;i<nodes.length;i++){var a=nodes[i];a.vx+=(W/2-a.x)*k;a.vy+=(H/2-a.y)*k;for(var j=i+1;j<nodes.length;j++){var b=nodes[j];var dx=a.x-b.x,dy=a.y-b.y,d2=dx*dx+dy*dy+0.01,d=Math.sqrt(d2),f=1400/d2,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;}}" +
|
|
44
|
+
"links.forEach(function(l){var a=byId[l.source],b=byId[l.target];if(!a||!b)return;var dx=b.x-a.x,dy=b.y-a.y,d=Math.sqrt(dx*dx+dy*dy)+0.01,f=(d-95)*0.012,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;});" +
|
|
45
|
+
"for(var i=0;i<nodes.length;i++){var n=nodes[i];if(n===drag)continue;n.vx*=0.86;n.vy*=0.86;n.x+=n.vx;n.y+=n.vy;}}" +
|
|
46
|
+
"function draw(){ctx.clearRect(0,0,W,H);ctx.save();ctx.translate(view.x,view.y);ctx.scale(view.k,view.k);ctx.lineWidth=0.7;" +
|
|
47
|
+
"links.forEach(function(l){var a=byId[l.source],b=byId[l.target];if(!a||!b)return;var on=sel&&(l.source===sel.id||l.target===sel.id);ctx.strokeStyle=on?'rgba(110,110,240,0.85)':'rgba(140,140,140,0.16)';ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();});" +
|
|
48
|
+
"nodes.forEach(function(n){var dim=(sel&&n!==sel&&(adj[sel.id]||[]).indexOf(n.id)<0)||(q&&n.id.toLowerCase().indexOf(q)<0);ctx.globalAlpha=dim?0.16:1;ctx.beginPath();ctx.arc(n.x,n.y,radius(n),0,6.2832);ctx.fillStyle=color(n.group);ctx.fill();if(n===sel||n===hover){ctx.lineWidth=2;ctx.strokeStyle='#111';ctx.stroke();ctx.lineWidth=0.7;}});" +
|
|
49
|
+
"ctx.globalAlpha=1;ctx.fillStyle=getComputedStyle(document.body).color;ctx.font='11px system-ui';nodes.forEach(function(n){if(n===sel||n===hover||n.symbols>=14){ctx.fillText(n.id.split('/').pop(),n.x+radius(n)+3,n.y+3);}});ctx.restore();}" +
|
|
50
|
+
"function loop(){tick();tick();draw();requestAnimationFrame(loop);}" +
|
|
51
|
+
"function world(e){return{x:(e.clientX-view.x)/view.k,y:(e.clientY-48-view.y)/view.k};}" +
|
|
52
|
+
"function pick(p){for(var i=nodes.length-1;i>=0;i--){var n=nodes[i];if((p.x-n.x)*(p.x-n.x)+(p.y-n.y)*(p.y-n.y)<=radius(n)*radius(n)+12)return n;}return null;}" +
|
|
53
|
+
"c.addEventListener('mousedown',function(e){var n=pick(world(e));if(n){drag=n;sel=n;}else{pan={x:e.clientX-view.x,y:e.clientY-view.y};sel=null;}});" +
|
|
54
|
+
"addEventListener('mousemove',function(e){var p=world(e);if(drag){drag.x=p.x;drag.y=p.y;drag.vx=0;drag.vy=0;}else if(pan){view.x=e.clientX-pan.x;view.y=e.clientY-pan.y;}else{hover=pick(p);if(hover){tip.style.display='block';tip.style.left=(e.clientX+12)+'px';tip.style.top=(e.clientY+12)+'px';tip.textContent=hover.id+' · '+hover.symbols+' symbols · '+hover.lang;}else tip.style.display='none';}});" +
|
|
55
|
+
"addEventListener('mouseup',function(){drag=null;pan=null;});" +
|
|
56
|
+
"c.addEventListener('wheel',function(e){e.preventDefault();var s=e.deltaY<0?1.1:0.9;var mx=e.clientX,my=e.clientY-48;view.x=mx-(mx-view.x)*s;view.y=my-(my-view.y)*s;view.k*=s;},{passive:false});" +
|
|
57
|
+
"document.getElementById('q').addEventListener('input',function(e){q=e.target.value.toLowerCase();});loop();";
|
|
58
|
+
/** Build a self-contained, dependency-free HTML graph explorer. */
|
|
59
|
+
export function buildExplorerHtml(graph, root) {
|
|
60
|
+
const data = deriveFileGraph(graph);
|
|
61
|
+
const dataJson = JSON.stringify(data);
|
|
62
|
+
const title = root.split(/[\\/]/).filter(Boolean).pop() || "project";
|
|
63
|
+
return ("<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">" +
|
|
64
|
+
"<title>AST-MCP — " + title + " graph</title><style>" + STYLE + "</style></head><body>" +
|
|
65
|
+
"<div id=\"bar\"><h1>AST-MCP graph</h1><span class=\"muted\">" + data.nodes.length + " files · " + data.links.length + " edges · drag / scroll / click</span>" +
|
|
66
|
+
"<input id=\"q\" placeholder=\"filter files…\" /></div>" +
|
|
67
|
+
"<canvas id=\"cv\"></canvas><div id=\"tip\"></div>" +
|
|
68
|
+
"<script>var DATA=" + dataJson + ";</script><script>" + CLIENT + "</script></body></html>");
|
|
69
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -983,6 +983,71 @@ function describeError(err) {
|
|
|
983
983
|
}
|
|
984
984
|
return err instanceof Error ? err.message : String(err);
|
|
985
985
|
}
|
|
986
|
+
/* ─────────────────── MCP resources (browseable structure) ──────────────── */
|
|
987
|
+
server.registerResource("languages", "ast://languages", {
|
|
988
|
+
title: "Supported languages",
|
|
989
|
+
description: "Languages and file extensions this server can map.",
|
|
990
|
+
mimeType: "application/json",
|
|
991
|
+
}, async (uri) => ({
|
|
992
|
+
contents: [{
|
|
993
|
+
uri: uri.href,
|
|
994
|
+
mimeType: "application/json",
|
|
995
|
+
text: JSON.stringify({ root: ROOT, languages: supportedLanguages() }, null, 2),
|
|
996
|
+
}],
|
|
997
|
+
}));
|
|
998
|
+
server.registerResource("skeleton", new ResourceTemplate("ast://skeleton/{+path}", {
|
|
999
|
+
list: async () => {
|
|
1000
|
+
const opts = resolveOptions({ detail: "outline", emitHtml: false });
|
|
1001
|
+
const files = collectSourceFiles(ROOT, opts);
|
|
1002
|
+
return {
|
|
1003
|
+
resources: files.map((f) => {
|
|
1004
|
+
const rel = path.relative(ROOT, f).split(path.sep).join("/");
|
|
1005
|
+
return { uri: `ast://skeleton/${rel}`, name: rel, mimeType: "application/json" };
|
|
1006
|
+
}),
|
|
1007
|
+
};
|
|
1008
|
+
},
|
|
1009
|
+
}), {
|
|
1010
|
+
title: "File skeleton",
|
|
1011
|
+
description: "Normalized code skeleton (symbols, imports, ranges) for one source file.",
|
|
1012
|
+
mimeType: "application/json",
|
|
1013
|
+
}, async (uri, variables) => {
|
|
1014
|
+
const rel = decodeURIComponent(String(variables.path)).split(path.sep).join("/");
|
|
1015
|
+
const { abs, rel: safeRel } = resolveInRoot(rel);
|
|
1016
|
+
const opts = resolveOptions({ detail: "outline", emitHtml: false });
|
|
1017
|
+
const skel = await buildSkeleton(abs, safeRel.split(path.sep).join("/"), opts);
|
|
1018
|
+
return {
|
|
1019
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(skel, null, 2) }],
|
|
1020
|
+
};
|
|
1021
|
+
});
|
|
1022
|
+
server.registerResource("graph", "ast://graph", {
|
|
1023
|
+
title: "Symbol dependency graph",
|
|
1024
|
+
description: "Symbol-level dependency graph for the whole root (guarded by node count).",
|
|
1025
|
+
mimeType: "application/json",
|
|
1026
|
+
}, async (uri) => {
|
|
1027
|
+
const opts = resolveOptions({ detail: "outline", emitHtml: false });
|
|
1028
|
+
const files = collectSourceFiles(ROOT, opts);
|
|
1029
|
+
if (files.length > 1500) {
|
|
1030
|
+
return {
|
|
1031
|
+
contents: [{
|
|
1032
|
+
uri: uri.href,
|
|
1033
|
+
mimeType: "application/json",
|
|
1034
|
+
text: JSON.stringify({ note: `Too large to inline (${files.length} files). Use build_symbol_graph on a subdirectory.`, files: files.length }, null, 2),
|
|
1035
|
+
}],
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
const skels = [];
|
|
1039
|
+
for (const file of files) {
|
|
1040
|
+
const fileRel = path.relative(ROOT, file).split(path.sep).join("/");
|
|
1041
|
+
try {
|
|
1042
|
+
skels.push(await buildSkeleton(file, fileRel, opts));
|
|
1043
|
+
}
|
|
1044
|
+
catch { /* skip */ }
|
|
1045
|
+
}
|
|
1046
|
+
const graph = buildSymbolGraph(skels, ROOT);
|
|
1047
|
+
return {
|
|
1048
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(graph, null, 2) }],
|
|
1049
|
+
};
|
|
1050
|
+
});
|
|
986
1051
|
async function main() {
|
|
987
1052
|
const transport = new StdioServerTransport();
|
|
988
1053
|
await server.connect(transport);
|
package/package.json
CHANGED