universal-ast-mapper 1.7.0 → 1.10.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/CHANGELOG.md +127 -0
- package/README.md +10 -0
- package/dist/cli.js +75 -0
- package/dist/explorer.js +60 -23
- package/dist/index.js +22 -0
- package/dist/sourcemap.js +60 -0
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **universal-ast-mapper** (AST-MCP). Format based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/); this project follows semver and,
|
|
5
|
+
since 1.0.0, guarantees a stable MCP tool / CLI surface across the 1.x line.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [1.10.0] — 2026-06-01 · Source maps
|
|
10
|
+
- **`read_source_map`** MCP tool + **`ast-map sourcemap <file>`** CLI: trace a
|
|
11
|
+
compiled JS/CSS file (inline `data:` or external `.map`) back to its original
|
|
12
|
+
sources; honors `sourceRoot` and reports embedded `sourcesContent`.
|
|
13
|
+
- Ruby re-investigated and confirmed blocked (external-scanner grammar needs
|
|
14
|
+
web-tree-sitter ≥0.22; engine upgrade would risk the 12 working languages).
|
|
15
|
+
|
|
16
|
+
## [1.9.0] — 2026-06-01 · Watch mode
|
|
17
|
+
- **`ast-map watch [dir]`** — debounced, coalesced rebuild of the dependency
|
|
18
|
+
analysis (files · dead exports · cycles) on every source change; `-o file.html`
|
|
19
|
+
regenerates the live explorer too.
|
|
20
|
+
- Explorer debug readout hidden by default (toggle with `d`).
|
|
21
|
+
|
|
22
|
+
## [1.8.0] — 2026-06-01 · Explorer detail sidebar
|
|
23
|
+
- Click a file in `ast-map explore` for a side panel: language, symbol count,
|
|
24
|
+
symbols, **Imports** and **Imported by** (each clickable to navigate the graph).
|
|
25
|
+
- **1.8.1–1.8.3 (fixes):** explorer now reliably centers/fills the viewport —
|
|
26
|
+
separated orphan files into a tidy grid, clamped the force layout to stop nodes
|
|
27
|
+
being flung to huge coordinates, and sized the canvas from `innerWidth/innerHeight`.
|
|
28
|
+
|
|
29
|
+
## [1.7.0] — 2026-06-01 · Web UI graph explorer
|
|
30
|
+
- **`ast-map explore [dir]`** writes a self-contained, dependency-free interactive
|
|
31
|
+
HTML: a force-directed file dependency graph (drag / zoom / pan / click-to-
|
|
32
|
+
highlight / name filter). Opens in any browser, no build step.
|
|
33
|
+
- **1.7.1–1.7.3 (fixes):** auto-fit and layout tuning.
|
|
34
|
+
|
|
35
|
+
## [1.6.0] — 2026-06-01 · MCP resource endpoints
|
|
36
|
+
- Browseable resources: **`ast://languages`**, **`ast://skeleton/{path}`**
|
|
37
|
+
(templated, one per file via `resources/list`), **`ast://graph`**. Agents can
|
|
38
|
+
list/read codebase structure as resources, not just call tools.
|
|
39
|
+
|
|
40
|
+
## [1.5.0] — 2026-06-01 · `.d.ts` / ambient declarations
|
|
41
|
+
- Extract `declare function/const/class`, `declare module "x"`, and
|
|
42
|
+
`declare namespace` (plus plain `namespace`); a `.d.ts` used to yield 0 symbols.
|
|
43
|
+
- New `namespace` symbol kind.
|
|
44
|
+
|
|
45
|
+
## [1.4.0] — 2026-06-01 · Dynamic import tracking
|
|
46
|
+
- Capture dynamic `import("...")` and CommonJS `require("...")` with an
|
|
47
|
+
`isDynamic` flag; relative ones resolve and draw graph edges like static imports.
|
|
48
|
+
|
|
49
|
+
## [1.3.0] — 2026-06-01 · TS/JS decorators
|
|
50
|
+
- Class and method symbols carry a `decorators` field (`@Component`, `@Get(...)`),
|
|
51
|
+
in skeletons and `get_call_graph`. Extends the Python decorator support to TS/JS.
|
|
52
|
+
|
|
53
|
+
## [1.2.0] — 2026-06-01 · File-level cross-package resolution
|
|
54
|
+
- In a monorepo, bare imports of a workspace package (`@org/utils`, `@org/utils/sub`)
|
|
55
|
+
resolve to the real source file (prefers `src/` over `dist/`), so `resolve_imports`
|
|
56
|
+
marks them in-project and `build_symbol_graph` draws cross-package edges.
|
|
57
|
+
|
|
58
|
+
## [1.1.0] — 2026-06-01 · Monorepo support
|
|
59
|
+
- **`analyze_workspace`** tool + **`ast-map workspace`** CLI: discover packages
|
|
60
|
+
(npm/yarn `workspaces`, `pnpm-workspace.yaml`, `lerna.json`), map internal
|
|
61
|
+
package dependencies, and detect circular package deps.
|
|
62
|
+
|
|
63
|
+
## [1.0.0] — 2026-06-01 · Stable release 🎉
|
|
64
|
+
- Locked public API (MCP tool names + schemas, CLI surface) for the 1.x line.
|
|
65
|
+
- Bundled **GitHub Action** (`action.yml`) running `ast-map validate` as a CI gate,
|
|
66
|
+
plus a project CI workflow.
|
|
67
|
+
- 12 languages · 18 MCP tools / 17 CLI commands at release.
|
|
68
|
+
|
|
69
|
+
## [0.9.0] — 2026-05-31 · Scoped type-flow tracing
|
|
70
|
+
- **`trace_type`** tool + **`ast-map trace-type`** CLI: follow a named type through
|
|
71
|
+
function params, return types, typed variables, and class fields. Completes the
|
|
72
|
+
deeper-analysis suite.
|
|
73
|
+
|
|
74
|
+
## [0.8.7] — 2026-05-31 · Python decorators
|
|
75
|
+
- `decorators` field on Python symbols + `get_call_graph`; traces
|
|
76
|
+
`@router.get(...)` → handler and stacked decorators.
|
|
77
|
+
|
|
78
|
+
## [0.8.6] — 2026-05-31 · Unused parameter detection
|
|
79
|
+
- **`find_unused_params`** tool + **`ast-map unused-params`** CLI: named functions
|
|
80
|
+
whose params are never referenced (low false-positive; counts object shorthand).
|
|
81
|
+
|
|
82
|
+
## [0.8.5] — 2026-05-31 · Cyclomatic complexity
|
|
83
|
+
- **`get_complexity`** tool + **`ast-map complexity`** CLI: per-function score with
|
|
84
|
+
low/moderate/high/very-high ratings and directory hotspots.
|
|
85
|
+
|
|
86
|
+
## [0.8.4] — 2026-05-31 · Duplicate symbol detection
|
|
87
|
+
- **`find_duplicate_symbols`** tool + **`ast-map duplicates`** CLI: exported names
|
|
88
|
+
declared in 2+ files.
|
|
89
|
+
|
|
90
|
+
## [0.8.3] — 2026-05-31 · TSX/React component props
|
|
91
|
+
- Component symbols carry `propsType` + `props[]`; detects `React.FC<P>` and
|
|
92
|
+
JSX-returning PascalCase functions. MCP server version now read from package.json.
|
|
93
|
+
|
|
94
|
+
## [0.8.2] — 2026-05-30 · Swift cross-file wiring
|
|
95
|
+
- `import <Module>` → that module's files (`Sources/<Module>/`). Completes
|
|
96
|
+
cross-file graph/resolver support for all four v0.8.0 languages.
|
|
97
|
+
|
|
98
|
+
## [0.8.1] — 2026-05-30 · Kotlin + C/C++ cross-file wiring
|
|
99
|
+
- Kotlin FQCN/package index; C/C++ `#include` resolution with header↔impl pairing.
|
|
100
|
+
- Fixes: parse-cache rel-path leak; Kotlin call-graph extraction.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Earlier (pre-session history)
|
|
105
|
+
|
|
106
|
+
- **0.8.0** — +4 languages: C · C++ · Kotlin · Swift (symbol extraction + imports).
|
|
107
|
+
- **0.7.0** — Go full module resolution; C# reverse `calledBy`; 4-suite test harness.
|
|
108
|
+
- **0.6.0** — +3 languages: Rust · Java · C#; cross-language resolver.
|
|
109
|
+
- **0.5.x** — `/ast-map` skill auto-install; iterative DFS; barrel re-exports; parse cache; call-graph aliases; `.ast-map.config.json`.
|
|
110
|
+
- **0.4.0** — `search_symbol`, `get_file_deps`, `get_top_symbols`, dead-code tiers.
|
|
111
|
+
- **0.3.0** — CLI; `find_dead_code`, `find_circular_deps`, `get_change_impact`, `get_call_graph`.
|
|
112
|
+
- **0.2.0** — import extraction; `resolve_imports`; `build_symbol_graph`.
|
|
113
|
+
- **0.1.0** — `get_skeleton_json`, `generate_skeleton`, `get_symbol_context`, `validate_architecture`.
|
|
114
|
+
|
|
115
|
+
[1.10.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.10.0
|
|
116
|
+
[1.9.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.9.0
|
|
117
|
+
[1.8.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.8.0
|
|
118
|
+
[1.7.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.7.0
|
|
119
|
+
[1.6.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.6.0
|
|
120
|
+
[1.5.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.5.0
|
|
121
|
+
[1.4.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.4.0
|
|
122
|
+
[1.3.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.3.0
|
|
123
|
+
[1.2.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.2.0
|
|
124
|
+
[1.1.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.1.0
|
|
125
|
+
[1.0.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v1.0.0
|
|
126
|
+
[0.9.0]: https://github.com/6ixthxense/AST-MCP/releases/tag/v0.9.0
|
|
127
|
+
[0.8.1]: https://github.com/6ixthxense/AST-MCP/releases/tag/v0.8.1
|
package/README.md
CHANGED
|
@@ -99,6 +99,8 @@ 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
101
|
ast-map explore [dir] [-o out.html]
|
|
102
|
+
ast-map watch [dir] [-o out.html]
|
|
103
|
+
ast-map sourcemap <file>
|
|
102
104
|
ast-map search <pattern> [dir] [-m contains|exact|regex] [-k kind] [-e]
|
|
103
105
|
ast-map deps <file> [--scan <dir>]
|
|
104
106
|
ast-map top <dir> [-n 10]
|
|
@@ -618,6 +620,14 @@ Not part of the public API: the internal `src/` module layout and the generated
|
|
|
618
620
|
|
|
619
621
|
| Version | What changed |
|
|
620
622
|
|---------|--------------|
|
|
623
|
+
| **1.10.0** | **Source-map support** — new `read_source_map` MCP tool + `ast-map sourcemap <file>` CLI: given a compiled JS/CSS file with an inline (`data:`) or external `sourceMappingURL`, returns the original source files it maps back to (honors `sourceRoot`). Traces `dist/` output back to source. |
|
|
624
|
+
| **1.9.0** | **Watch mode** — `ast-map watch [dir]` recomputes the dependency analysis (file count · dead exports · cycles) on every source-file change, debounced; `-o file.html` also regenerates the live explorer each time. Plus: the explorer debug readout is now hidden (toggle with `d`). |
|
|
625
|
+
| **1.8.2** | **Explorer stability fix** — clamp the force layout (distance floor + velocity cap) so nodes that initialize close together can't be flung to huge coordinates, which was blowing up the bounding box and shrinking the whole graph into a corner. Now reliably centers and fills. |
|
|
626
|
+
| **1.8.1** | **Explorer self-heal sizing** — the explorer now re-checks canvas size every frame and re-fits, so it always centers/fills even if the canvas reports zero size at load (and survives container resizes). |
|
|
627
|
+
| **1.8.0** | **Explorer detail sidebar** — click a file in `ast-map explore` to open a side panel: language, symbol count, its symbols, the files it imports, and the files that import it — each clickable to jump to that file. |
|
|
628
|
+
| **1.7.3** | **Explorer layout overhaul** — only connected files are force-laid (centered + filling the viewport); orphan files with no in-scope deps are parked in a tidy grid below instead of sprawling. Verified centered at any window size. |
|
|
629
|
+
| **1.7.2** | **Explorer fit, really this time** — continuous auto-fit until you interact, robust canvas sizing, and centered node init, so the graph fills the viewport instead of bunching in a corner. |
|
|
630
|
+
| **1.7.1** | **Explorer fit fix** — the `ast-map explore` graph now auto-fits the viewport (spreads to fill the screen instead of clustering in the centre); double-click re-fits. |
|
|
621
631
|
| **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
632
|
| **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. |
|
|
623
633
|
| **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. |
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { findUnusedParams } from "./unused-params.js";
|
|
|
15
15
|
import { traceTypeInFile } from "./typeflow.js";
|
|
16
16
|
import { discoverWorkspace, findPackageCycles } from "./workspace.js";
|
|
17
17
|
import { buildExplorerHtml } from "./explorer.js";
|
|
18
|
+
import { readSourceMap } from "./sourcemap.js";
|
|
18
19
|
import { buildCallGraph } from "./callgraph.js";
|
|
19
20
|
import { searchSymbols } from "./search.js";
|
|
20
21
|
const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
|
|
@@ -371,6 +372,80 @@ program
|
|
|
371
372
|
}
|
|
372
373
|
console.log();
|
|
373
374
|
});
|
|
375
|
+
// ─── Command: watch ───────────────────────────────────────────────────────────
|
|
376
|
+
program
|
|
377
|
+
.command("watch [dir]")
|
|
378
|
+
.description("Rebuild analysis (and optionally the explorer) when files change")
|
|
379
|
+
.option("-o, --out <file>", "Also regenerate the explorer HTML on each change")
|
|
380
|
+
.action(async (dir, opts) => {
|
|
381
|
+
const { abs, rel } = resolveArg(dir ?? ".");
|
|
382
|
+
if (!fs.statSync(abs).isDirectory())
|
|
383
|
+
die(`"${rel}" is not a directory`);
|
|
384
|
+
let building = false;
|
|
385
|
+
let queued = false;
|
|
386
|
+
async function rebuild(reason) {
|
|
387
|
+
if (building) {
|
|
388
|
+
queued = true;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
building = true;
|
|
392
|
+
try {
|
|
393
|
+
const skels = await gatherSkeletons(abs);
|
|
394
|
+
const graph = buildSymbolGraph(skels, ROOT);
|
|
395
|
+
const dead = findDeadExports(graph).filter((d) => d.confidence === "high").length;
|
|
396
|
+
const cycles = findCircularDeps(graph).length;
|
|
397
|
+
let line = `${dim(new Date().toLocaleTimeString())} ${bold(String(skels.length))} files · ${dead} dead · ${cycles} cycle(s)`;
|
|
398
|
+
if (opts.out) {
|
|
399
|
+
fs.writeFileSync(path.resolve(process.cwd(), opts.out), buildExplorerHtml(graph, abs), "utf8");
|
|
400
|
+
line += ` · ${green("explorer updated")}`;
|
|
401
|
+
}
|
|
402
|
+
line += ` ${dim(reason)}`;
|
|
403
|
+
console.log(line);
|
|
404
|
+
}
|
|
405
|
+
finally {
|
|
406
|
+
building = false;
|
|
407
|
+
if (queued) {
|
|
408
|
+
queued = false;
|
|
409
|
+
rebuild("(coalesced)");
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
header(`Watching ${rel}/ ${dim("(Ctrl+C to stop)")}`);
|
|
414
|
+
await rebuild("initial");
|
|
415
|
+
let timer = null;
|
|
416
|
+
const exts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".cs", ".c", ".cpp", ".h", ".hpp", ".kt", ".swift"]);
|
|
417
|
+
fs.watch(abs, { recursive: true }, (_evt, file) => {
|
|
418
|
+
if (!file)
|
|
419
|
+
return;
|
|
420
|
+
const f = String(file).split(path.sep).join("/");
|
|
421
|
+
if (/(^|\/)(node_modules|\.git|dist|\.ast-map)(\/|$)/.test(f))
|
|
422
|
+
return;
|
|
423
|
+
if (!exts.has(path.extname(f).toLowerCase()))
|
|
424
|
+
return;
|
|
425
|
+
if (timer)
|
|
426
|
+
clearTimeout(timer);
|
|
427
|
+
timer = setTimeout(() => rebuild(`(${f.split("/").pop()} changed)`), 300);
|
|
428
|
+
});
|
|
429
|
+
await new Promise(() => { }); // keep the process alive
|
|
430
|
+
});
|
|
431
|
+
// ─── Command: sourcemap ───────────────────────────────────────────────────────
|
|
432
|
+
program
|
|
433
|
+
.command("sourcemap <file>")
|
|
434
|
+
.description("Show the original sources a compiled file maps back to")
|
|
435
|
+
.option("--json", "Output as JSON")
|
|
436
|
+
.action(async (inputPath, opts) => {
|
|
437
|
+
const { abs, rel } = resolveArg(inputPath);
|
|
438
|
+
const info = readSourceMap(abs, rel);
|
|
439
|
+
if (!info)
|
|
440
|
+
die(`No source map found for "${rel}"`);
|
|
441
|
+
if (opts.json)
|
|
442
|
+
return jsonOut(info);
|
|
443
|
+
header(`Source Map — ${rel} ${dim("(" + info.mapKind + ")")}`);
|
|
444
|
+
for (const sourceFile of info.sources)
|
|
445
|
+
console.log(indent(green("←") + " " + sourceFile));
|
|
446
|
+
console.log(`\n ${info.sources.length} original source(s)` + (info.hasContent ? dim(" · embeds sourcesContent") : ""));
|
|
447
|
+
console.log();
|
|
448
|
+
});
|
|
374
449
|
// ─── Command: explore ─────────────────────────────────────────────────────────
|
|
375
450
|
program
|
|
376
451
|
.command("explore [dir]")
|
package/dist/explorer.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
/** Derive a file-level dependency graph (nodes = files, edges = imports). */
|
|
2
2
|
function deriveFileGraph(graph) {
|
|
3
3
|
const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
4
|
+
// top-level symbol names per file (for the detail panel).
|
|
5
|
+
const fileSyms = new Map();
|
|
6
|
+
for (const n of graph.nodes) {
|
|
7
|
+
if (n.nodeType !== "symbol")
|
|
8
|
+
continue;
|
|
9
|
+
const s = n;
|
|
10
|
+
if (s.id.indexOf("::") !== s.id.lastIndexOf("::"))
|
|
11
|
+
continue; // skip nested (one :: only)
|
|
12
|
+
const arr = fileSyms.get(s.file) ?? [];
|
|
13
|
+
if (arr.length < 60)
|
|
14
|
+
arr.push(s.kind + " " + s.symbol);
|
|
15
|
+
fileSyms.set(s.file, arr);
|
|
16
|
+
}
|
|
4
17
|
const nodes = [];
|
|
5
18
|
for (const n of graph.nodes) {
|
|
6
19
|
if (n.nodeType !== "file")
|
|
7
20
|
continue;
|
|
8
21
|
const f = n;
|
|
9
22
|
const parts = f.id.split("/");
|
|
10
|
-
nodes.push({ id: f.id, symbols: f.symbolCount, group: parts.length > 1 ? parts[0] : "(root)", lang: f.language });
|
|
23
|
+
nodes.push({ id: f.id, symbols: f.symbolCount, group: parts.length > 1 ? parts[0] : "(root)", lang: f.language, syms: fileSyms.get(f.id) ?? [] });
|
|
11
24
|
}
|
|
12
25
|
const seen = new Set();
|
|
13
26
|
const links = [];
|
|
@@ -27,34 +40,58 @@ function deriveFileGraph(graph) {
|
|
|
27
40
|
return { nodes, links };
|
|
28
41
|
}
|
|
29
42
|
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:
|
|
43
|
+
"#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:4;box-sizing:border-box}" +
|
|
31
44
|
"#bar h1{font-size:14px;margin:0;font-weight:600}#bar .muted{color:#888;font-size:12px}" +
|
|
32
|
-
"#q{flex:0 0
|
|
45
|
+
"#q{flex:0 0 200px;padding:6px 10px;border:1px solid #ddd;border-radius:6px;font-size:13px}" +
|
|
33
46
|
"#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:
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
47
|
+
"#tip{position:fixed;pointer-events:none;background:#222;color:#fff;font-size:12px;padding:4px 8px;border-radius:5px;display:none;z-index:5}" +
|
|
48
|
+
"#panel{position:fixed;top:48px;right:0;bottom:0;width:300px;background:#fff;border-left:1px solid #e5e5e5;z-index:3;overflow-y:auto;padding:14px 16px;box-sizing:border-box;display:none;font-size:13px}" +
|
|
49
|
+
"#panel h2{font-size:14px;margin:0 0 2px;word-break:break-all}#panel .path{color:#888;font-size:11px;margin-bottom:10px;word-break:break-all}" +
|
|
50
|
+
"#panel .meta{color:#555;margin-bottom:12px}#panel h3{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:#999;margin:14px 0 6px}" +
|
|
51
|
+
"#panel .row{padding:3px 6px;border-radius:5px;cursor:pointer;word-break:break-all;line-height:1.5}#panel .row:hover{background:#f0f0f0}" +
|
|
52
|
+
"#panel .sym{color:#444;padding:2px 6px;word-break:break-all}#panel .k{color:#999;font-size:11px}" +
|
|
53
|
+
"#close{position:absolute;top:10px;right:12px;cursor:pointer;color:#999;font-size:18px;line-height:1;border:none;background:none}" +
|
|
54
|
+
"@media(prefers-color-scheme:dark){body{color:#ddd;background:#161616}#bar,#panel{background:#1e1e1e;border-color:#333}#q{background:#2a2a2a;border-color:#444;color:#ddd}#panel .row:hover{background:#2a2a2a}#panel .sym{color:#bbb}}";
|
|
55
|
+
const CLIENT = "var c=document.getElementById('cv'),ctx=c.getContext('2d'),tip=document.getElementById('tip'),panel=document.getElementById('panel');" +
|
|
56
|
+
"var PANELW=300,panelOpen=false;" +
|
|
57
|
+
"var W,H;function resize(){var r=devicePixelRatio||1;W=innerWidth||c.clientWidth||800;H=(innerHeight-48)||c.clientHeight||600;c.width=W*r;c.height=H*r;ctx.setTransform(r,0,0,r,0,0);}addEventListener('resize',function(){resize();});resize();" +
|
|
58
|
+
"function availW(){return W-(panelOpen?PANELW:0);}" +
|
|
59
|
+
"var nodes=DATA.nodes,links=DATA.links,byId={};nodes.forEach(function(n){byId[n.id]=n;n.vx=0;n.vy=0;});" +
|
|
60
|
+
"var deg={},out={},inn={};links.forEach(function(l){deg[l.source]=(deg[l.source]||0)+1;deg[l.target]=(deg[l.target]||0)+1;(out[l.source]=out[l.source]||[]).push(l.target);(inn[l.target]=inn[l.target]||[]).push(l.source);});" +
|
|
61
|
+
"var sim=nodes.filter(function(n){return deg[n.id];}),orphans=nodes.filter(function(n){return !deg[n.id];});" +
|
|
62
|
+
"sim.forEach(function(n){n.x=W/2+(Math.random()-0.5)*240;n.y=H/2+(Math.random()-0.5)*240;});" +
|
|
39
63
|
"var groups={},gi=0;function color(g){if(groups[g]==null)groups[g]=gi++;return 'hsl('+((groups[g]*67)%360)+',58%,55%)';}" +
|
|
40
64
|
"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='';" +
|
|
65
|
+
"var view={x:0,y:0,k:1},sel=null,hover=null,drag=null,pan=null,q='',autofit=true;" +
|
|
42
66
|
"function radius(n){return 4+Math.sqrt(n.symbols||0)*1.7;}" +
|
|
43
|
-
"function tick(){var k=0.
|
|
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-
|
|
45
|
-
"for(var i=0;i<
|
|
46
|
-
"function
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"function
|
|
67
|
+
"function tick(){if(!sim.length)return;var k=0.0016;for(var i=0;i<sim.length;i++){var a=sim[i];a.vx+=(W/2-a.x)*k;a.vy+=(H/2-a.y)*k;for(var j=i+1;j<sim.length;j++){var b=sim[j];var dx=a.x-b.x,dy=a.y-b.y,d2=dx*dx+dy*dy;if(d2<100)d2=100;var d=Math.sqrt(d2),f=2200/d2,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;}}" +
|
|
68
|
+
"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-90)*0.02,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;});" +
|
|
69
|
+
"for(var i=0;i<sim.length;i++){var n=sim[i];if(n===drag)continue;n.vx*=0.85;n.vy*=0.85;if(n.vx>30)n.vx=30;if(n.vx<-30)n.vx=-30;if(n.vy>30)n.vy=30;if(n.vy<-30)n.vy=-30;n.x+=n.vx;n.y+=n.vy;}}" +
|
|
70
|
+
"function bb4(arr){var a=1e9,b=1e9,c2=-1e9,d2=-1e9;for(var i=0;i<arr.length;i++){var n=arr[i];if(n.x<a)a=n.x;if(n.y<b)b=n.y;if(n.x>c2)c2=n.x;if(n.y>d2)d2=n.y;}return[a,b,c2,d2];}" +
|
|
71
|
+
"function layoutOrphans(){if(!orphans.length)return;var bb=sim.length?bb4(sim):[W*0.3,H*0.3,W*0.7,H*0.5];var left=bb[0],bottom=bb[3]+46,wide=Math.max(bb[2]-bb[0],260);var cols=Math.max(1,Math.ceil(Math.sqrt(orphans.length*1.8)));var gap=Math.max(22,wide/cols);for(var i=0;i<orphans.length;i++){orphans[i].x=left+(i%cols)*gap;orphans[i].y=bottom+Math.floor(i/cols)*22;}}" +
|
|
72
|
+
"function fitView(){var bb=bb4(nodes);var aw=availW();var bw=Math.max(bb[2]-bb[0],1),bh=Math.max(bb[3]-bb[1],1),k=Math.min(aw/(bw+70),H/(bh+70));k=Math.max(0.12,Math.min(k,2.2));view.k=k;view.x=aw/2-((bb[0]+bb[2])/2)*k;view.y=H/2-((bb[1]+bb[3])/2)*k;}" +
|
|
73
|
+
"function center(n){view.k=Math.max(view.k,0.7);view.x=availW()/2-n.x*view.k;view.y=H/2-n.y*view.k;}" +
|
|
74
|
+
"function esc(t){return String(t).replace(/&/g,'&').replace(/</g,'<');}" +
|
|
75
|
+
"function rowList(ids){if(!ids||!ids.length)return '<div class=\"sym\" style=\"color:#aaa\">none</div>';return ids.slice().sort().map(function(id){return '<div class=\"row\" data-id=\"'+esc(id)+'\">'+esc(id)+'</div>';}).join('');}" +
|
|
76
|
+
"function showPanel(n){sel=n;panelOpen=true;var imp=out[n.id]||[],impBy=inn[n.id]||[];var syms=(n.syms||[]).map(function(s){var i=s.indexOf(' ');return '<div class=\"sym\"><span class=\"k\">'+esc(s.slice(0,i))+'</span> '+esc(s.slice(i+1))+'</div>';}).join('')||'<div class=\"sym\" style=\"color:#aaa\">none</div>';" +
|
|
77
|
+
"panel.innerHTML='<button id=\"close\">×</button>'+'<h2>'+esc(n.id.split('/').pop())+'</h2><div class=\"path\">'+esc(n.id)+'</div>'+'<div class=\"meta\">'+esc(n.lang)+' · '+(n.symbols||0)+' symbols'+(deg[n.id]?'':' · no in-scope deps')+'</div>'+'<h3>Imports ('+imp.length+')</h3>'+rowList(imp)+'<h3>Imported by ('+impBy.length+')</h3>'+rowList(impBy)+'<h3>Symbols</h3>'+syms;" +
|
|
78
|
+
"panel.style.display='block';}" +
|
|
79
|
+
"panel.addEventListener('click',function(e){if(e.target.id==='close'){panelOpen=false;sel=null;panel.style.display='none';autofit=true;return;}var id=e.target.getAttribute('data-id');if(id&&byId[id]){showPanel(byId[id]);center(byId[id]);}});" +
|
|
80
|
+
"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.8;" +
|
|
81
|
+
"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.9)':'rgba(150,150,150,0.18)';ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();});" +
|
|
82
|
+
"function dot(n,orphan){var dim=(sel&&n!==sel&&(adj[sel.id]||[]).indexOf(n.id)<0)||(q&&n.id.toLowerCase().indexOf(q)<0);ctx.globalAlpha=dim?0.14:(orphan?0.55:1);ctx.beginPath();ctx.arc(n.x,n.y,orphan?3.2:radius(n),0,6.2832);ctx.fillStyle=color(n.group);ctx.fill();if(n===sel||n===hover){ctx.lineWidth=2;ctx.strokeStyle='#fff';ctx.stroke();ctx.lineWidth=0.8;}}" +
|
|
83
|
+
"orphans.forEach(function(n){dot(n,true);});sim.forEach(function(n){dot(n,false);});" +
|
|
84
|
+
"ctx.globalAlpha=1;ctx.fillStyle=getComputedStyle(document.body).color;ctx.font='11px system-ui';sim.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();}" +
|
|
85
|
+
"function loop(){var w=innerWidth,hh=innerHeight-48;if(w&&hh&&(w!==W||hh!==H))resize();tick();tick();layoutOrphans();if(autofit)fitView();draw();var bx=bb4(nodes);document.getElementById(\"dbg\").textContent=\"W=\"+W+\" H=\"+H+\" iw=\"+innerWidth+\"x\"+innerHeight+\" dpr=\"+(devicePixelRatio||1)+\" k=\"+view.k.toFixed(2)+\" vx=\"+Math.round(view.x)+\" vy=\"+Math.round(view.y)+\" fit=\"+autofit+\" sim=\"+sim.length+\" orph=\"+orphans.length+\" worldBox=\"+Math.round(bx[0])+\",\"+Math.round(bx[1])+\"..\"+Math.round(bx[2])+\",\"+Math.round(bx[3]);requestAnimationFrame(loop);}" +
|
|
51
86
|
"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=
|
|
53
|
-
"c.addEventListener('mousedown',function(e){var n=pick(world(e));if(n){drag=n;
|
|
54
|
-
"addEventListener('
|
|
87
|
+
"function pick(p){var all=sim.concat(orphans);for(var i=all.length-1;i>=0;i--){var n=all[i];var r=(deg[n.id]?radius(n):3.2)+5;if((p.x-n.x)*(p.x-n.x)+(p.y-n.y)*(p.y-n.y)<=r*r)return n;}return null;}" +
|
|
88
|
+
"c.addEventListener('mousedown',function(e){autofit=false;var n=pick(world(e));if(n){drag=n;showPanel(n);}else{pan={x:e.clientX-view.x,y:e.clientY-view.y};}});" +
|
|
89
|
+
"c.addEventListener('dblclick',function(){panelOpen=false;sel=null;panel.style.display='none';autofit=true;});" +
|
|
90
|
+
"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||0)+' symbols · '+hover.lang;}else tip.style.display='none';}});" +
|
|
55
91
|
"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();});
|
|
92
|
+
"c.addEventListener('wheel',function(e){e.preventDefault();autofit=false;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});" +
|
|
93
|
+
"document.getElementById('q').addEventListener('input',function(e){q=e.target.value.toLowerCase();});" +
|
|
94
|
+
"addEventListener('keydown',function(e){if(e.key==='d'&&e.target.tagName!=='INPUT'){var x=document.getElementById('dbg');x.style.display=x.style.display==='none'?'block':'none';}});loop();";
|
|
58
95
|
/** Build a self-contained, dependency-free HTML graph explorer. */
|
|
59
96
|
export function buildExplorerHtml(graph, root) {
|
|
60
97
|
const data = deriveFileGraph(graph);
|
|
@@ -64,6 +101,6 @@ export function buildExplorerHtml(graph, root) {
|
|
|
64
101
|
"<title>AST-MCP — " + title + " graph</title><style>" + STYLE + "</style></head><body>" +
|
|
65
102
|
"<div id=\"bar\"><h1>AST-MCP graph</h1><span class=\"muted\">" + data.nodes.length + " files · " + data.links.length + " edges · drag / scroll / click</span>" +
|
|
66
103
|
"<input id=\"q\" placeholder=\"filter files…\" /></div>" +
|
|
67
|
-
"<canvas id=\"cv\"></canvas><div id=\"tip\"></div>" +
|
|
104
|
+
"<canvas id=\"cv\"></canvas><div id=\"tip\"></div><div id=\"panel\"></div><div id=\"dbg\" style=\"position:fixed;left:8px;bottom:8px;font:11px monospace;color:#e07;z-index:6;pointer-events:none;white-space:pre;display:none\"></div>" +
|
|
68
105
|
"<script>var DATA=" + dataJson + ";</script><script>" + CLIENT + "</script></body></html>");
|
|
69
106
|
}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ import { computeFileComplexity } from "./complexity.js";
|
|
|
19
19
|
import { findUnusedParams } from "./unused-params.js";
|
|
20
20
|
import { traceTypeInFile } from "./typeflow.js";
|
|
21
21
|
import { discoverWorkspace, findPackageCycles } from "./workspace.js";
|
|
22
|
+
import { readSourceMap } from "./sourcemap.js";
|
|
22
23
|
/** Files may only be read inside this root (override with AST_MAP_ROOT). */
|
|
23
24
|
const ROOT = path.resolve(process.env.AST_MAP_ROOT ?? process.cwd());
|
|
24
25
|
function resolveInRoot(input) {
|
|
@@ -745,6 +746,27 @@ server.registerTool("analyze_workspace", {
|
|
|
745
746
|
return errorText(describeError(err));
|
|
746
747
|
}
|
|
747
748
|
});
|
|
749
|
+
/* ─────────────────── tool: read_source_map ─────────────────────────────── */
|
|
750
|
+
server.registerTool("read_source_map", {
|
|
751
|
+
title: "Read a compiled file's source map",
|
|
752
|
+
description: "Given a compiled JS/CSS file with an inline (`data:`) or external `sourceMappingURL`, " +
|
|
753
|
+
"return the original source paths it maps back to. Useful for tracing built output in " +
|
|
754
|
+
"dist/ back to the real source files.",
|
|
755
|
+
inputSchema: {
|
|
756
|
+
path: z.string().describe("Compiled file path, relative to project root or absolute within it."),
|
|
757
|
+
},
|
|
758
|
+
}, async ({ path: input }) => {
|
|
759
|
+
try {
|
|
760
|
+
const { abs, rel } = resolveInRoot(input);
|
|
761
|
+
const info = readSourceMap(abs, rel.split(path.sep).join("/"));
|
|
762
|
+
if (!info)
|
|
763
|
+
return errorText(`No source map found for "${input}".`);
|
|
764
|
+
return jsonText(info);
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
return errorText(describeError(err));
|
|
768
|
+
}
|
|
769
|
+
});
|
|
748
770
|
/* ─────────────────── tool: get_change_impact ───────────────────────────── */
|
|
749
771
|
server.registerTool("get_change_impact", {
|
|
750
772
|
title: "Get change impact (blast radius)",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function decodeInline(url) {
|
|
4
|
+
const b64 = url.match(/base64,(.+)$/);
|
|
5
|
+
try {
|
|
6
|
+
if (b64)
|
|
7
|
+
return JSON.parse(Buffer.from(b64[1], "base64").toString("utf8"));
|
|
8
|
+
const comma = url.indexOf(",");
|
|
9
|
+
if (comma >= 0)
|
|
10
|
+
return JSON.parse(decodeURIComponent(url.slice(comma + 1)));
|
|
11
|
+
}
|
|
12
|
+
catch { /* malformed */ }
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Read the source map for a compiled JS/CSS file: handles an inline
|
|
17
|
+
* `//# sourceMappingURL=data:...` comment or an external `.map` file, and
|
|
18
|
+
* returns the original source paths it maps back to.
|
|
19
|
+
*/
|
|
20
|
+
export function readSourceMap(absPath, relPath) {
|
|
21
|
+
let src;
|
|
22
|
+
try {
|
|
23
|
+
src = fs.readFileSync(absPath, "utf8");
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const matches = [...src.matchAll(/[#@]\s*sourceMappingURL=([^\s'"]+)/g)];
|
|
29
|
+
if (matches.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
const url = matches[matches.length - 1][1];
|
|
32
|
+
let map = null;
|
|
33
|
+
let mapKind;
|
|
34
|
+
let mapFile;
|
|
35
|
+
if (url.startsWith("data:")) {
|
|
36
|
+
mapKind = "inline";
|
|
37
|
+
map = decodeInline(url);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
mapKind = "external";
|
|
41
|
+
mapFile = url;
|
|
42
|
+
try {
|
|
43
|
+
map = JSON.parse(fs.readFileSync(path.resolve(path.dirname(absPath), url), "utf8"));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
map = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!map || !Array.isArray(map.sources))
|
|
50
|
+
return null;
|
|
51
|
+
const root = typeof map.sourceRoot === "string" ? map.sourceRoot.replace(/\/$/, "") : "";
|
|
52
|
+
const sources = map.sources.map((s) => root && !s.startsWith("/") ? root + "/" + s : s);
|
|
53
|
+
return {
|
|
54
|
+
file: relPath,
|
|
55
|
+
mapKind,
|
|
56
|
+
...(mapFile ? { mapFile } : {}),
|
|
57
|
+
sources,
|
|
58
|
+
hasContent: Array.isArray(map.sourcesContent) && map.sourcesContent.length > 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-ast-mapper",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "MCP server that maps source files into a normalized code skeleton (JSON + HTML) using tree-sitter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"dist",
|
|
13
13
|
"scripts",
|
|
14
14
|
"README.md",
|
|
15
|
+
"CHANGELOG.md",
|
|
15
16
|
"BLUEPRINT.md"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|