@smi0001/agent-pintu
v0.4.2
Published
PR call-graph tracer for TypeScript and JavaScript projects — multi-root, extracts a call graph from a PR diff (route-rooted or function-touched mode) and emits Mermaid + YAML artifacts via ts-morph static analysis.
Downloads
954
Maintainers
Readme
@smi0001/agent-pintu
PR call-graph tracer for TypeScript and JavaScript projects. Walks the AST from each route handler (or every touched function) in a PR diff and emits two artifacts you can paste into a PR or hand to another AI:
PR-<n>-<slug>.md— human view: title, combined Mermaid flowchart, per-route Mermaid + table, "touched code not reached" section. Renders as a picture in GitHub / Gitea.PR-<n>-<slug>.yaml— machine view: structuredentry_routes,nodes,edges, timestamps. An AI/tool can read this and skip re-walking the codebase.
Static analysis only — no LLM calls, no token cost per run.
Install
# On demand
npx @smi0001/agent-pintu init
npx @smi0001/agent-pintu trace --pr-num 42 --title "Order Checkout"
# Or globally
npm i -g @smi0001/agent-pintu
agent-pintu --helpRequires Node 18+. Works on any git repo whose server code is TypeScript and whose tsconfig.json compiles cleanly.
Configure
Drop a .pintu.json in your project root:
cd /path/to/your/project
agent-pintu init{
"project": ".",
"serverRoot": "server",
"tsConfig": "server/tsconfig.json",
"appEntry": "server/app.ts",
"baseBranch": "main",
"outDir": "documents/pr-traces",
"routesPattern": "/routes/",
"depth": 4
}Paths in .pintu.json resolve relative to the config file's location, so agent-pintu works no matter what dir you run it from. CLI flags (e.g. --pr-num, --title, --base) override config.
Config search walks up from the current dir to find .pintu.json — handy if you cd into a subdirectory of your project.
Usage
# Trace the current branch vs base (auto-detects main / master / develop)
agent-pintu trace --pr-num 42 --title "Order Checkout"
# Override base branch
agent-pintu trace --base develop --pr-num 42
# Run against a different repo
agent-pintu trace --project /path/to/other/repo --pr-num 7
# All flags
agent-pintu --helpWhat it does
git diff --name-only <base>...HEAD→ changed files.- Loads the TS project with
ts-morph(<server-root>/tsconfig.json). - In every changed
.tsroute file (routesPatternsubstring match), finds:router.route(path).METHOD(handler)chainsrouter.METHOD(path, handler)calls
- Walks the AST from each handler. For each
CallExpression, resolves the called symbol's declaration via the TS type checker. Recurses up todepthhops; cycle-safe via a visited set. - For each in-project symbol: marks
touched_in_prif any line of its body intersects a diff hunk; runsgit log -L <start>,<end>:<file>to extractcreated/updatedISO timestamps. - External calls (node_modules) become terminal nodes. Stdlib noise (
res.json,console.log,Array.push,JSON.stringify, etc.) is filtered. - Detects the mount-prefix chain from
app.ts(app.use("/_", api)+api.use("/orders", router)→/_/orders).
Mermaid color coding:
- 🟧 Orange — function touched by this PR
- 🟦 Blue — route entry handler
- ⬜ Gray — external / library symbol
Output schema (.yaml)
pr: "42"
title: "Order Checkout"
base_branch: "develop"
generated_at: "2026-06-03T..."
tool: agent-pintu v0.1.0
entry_routes:
- method: POST
path: "/_/orders/start"
handler: "createOrder"
route_file: "server/src/routes/ordersRoutes.ts"
nodes:
- id: "server/src/controllers/ordersController.ts:createOrder"
name: createOrder
file: server/src/controllers/ordersController.ts
line: 95
end_line: 232
kind: entry # entry | internal | external
touched_in_pr: true
routes: ["POST /_/orders/start"]
created: "2026-05-17T04:11:07+05:30"
updated: "2026-05-17T04:11:07+05:30"
edges:
- from: "server/src/controllers/ordersController.ts:createOrder"
to: "server/src/controllers/ordersController.ts:requireAuth"
call_sites:
- line: 138
args: ["payload"]Trace modes (v0.3)
Pintu now has two ways to pick entry points, selected via --mode or the interactive prompt:
| Mode | Entry points | Best for |
|---|---|---|
| routes (default for backend servers) | Express route handlers detected in changed route files (router.METHOD(path, handler) or router.route(path).METHOD(handler)) | Backend Node services with HTTP routes — answers "what URLs does this PR affect?" |
| touched | Every top-level function (and class method) whose body intersects a diff hunk | Any project — frontend SPAs, libraries, CLIs, monorepos without classic Express routes. Answers "what functions changed and what do they call?" |
If you don't pass --mode and don't set mode in .pintu.json, pintu prompts you interactively (or defaults to routes in non-TTY contexts like CI).
agent-pintu trace --pr-num 42 --title "..." # interactive prompt
agent-pintu trace --mode touched --pr-num 42 --title "..." # explicit, no promptFor CI, pin the mode in .pintu.json or pass --mode so the run is deterministic.
Scope (v0.3)
- TypeScript and JavaScript. Walks
.ts,.tsx,.js,.jsx,.mjs,.cjs. WhentsConfigis set (ortsconfig.jsonexists at the server or project root), pintu loads the project from it. For JS-only projects without a tsconfig, pintu synthesizes a ts-morph project withallowJs: true— just leavetsConfigunset in your.pintu.json(or omit the file). - Route handlers — named or inline (routes mode). Both
router.get(path, namedHandler)androuter.get(path, async (req, res) => {...})are detected. Inline handlers get a synthetic label like<inline get@L22>. - Top-level functions and class methods (touched mode). Nested arrow functions inside other functions are skipped to avoid graph explosion.
- Doesn't trace client-side framework routers (Durandal, Knockout, React Router, Vue Router, etc.) in routes mode. For frontend projects, use
--mode touched. - Arg labels are source text, not type-resolved. Edge labels show the literal source of the first 1–2 arguments (capped at 60 chars).
- Mount-prefix detection (routes mode) uses suffix-matching on import paths in
appEntryand only resolves plain string / no-substitution template prefixes. Template literals with variable substitutions (e.g.app.use(\${BASE_PATH}/api`, router)`) currently fall back to router-local paths — roadmap item.
Composition with agent-binod
The pair works well together: run agent-pintu first to generate the .yaml, then point agent-binod at the PR — binod's review gets free structural context ("you touched 6 routes; here's the call graph") at near-zero token cost vs re-parsing the source.
Multi-root projects (v0.4)
When a PR spans multiple source trees — server + client + admin dashboard, or multiple bundles in a monorepo — declare each root in .pintu.json:
{
"project": ".",
"roots": [
{
"label": "server",
"serverRoot": "server",
"tsConfig": "server/tsconfig.json",
"appEntry": "server/app.ts",
"mode": "routes"
},
{
"label": "client",
"serverRoot": "client/src",
"tsConfig": "client/tsconfig.json",
"mode": "touched"
},
{
"label": "admin",
"serverRoot": "adminDashboard/src",
"tsConfig": "adminDashboard/tsconfig.json",
"mode": "touched"
}
],
"baseBranch": "upstream/main",
"outDir": "documents/pr-traces",
"depth": 4
}Each root is processed independently — its own tsconfig, mode, and entry-point detector — and the resulting call graphs are merged into a single Mermaid diagram and YAML file. Each node in the YAML carries a roots array tagging which root(s) walked into it, so a shared utility called by both client and admin shows roots: ["client", "admin"].
Per-root fields:
| Field | Required | Notes |
|---|---|---|
| label | optional | Display name (defaults to basename of serverRoot) |
| serverRoot | yes | Directory to walk, relative to config file's location |
| tsConfig | optional | If omitted, pintu auto-finds <serverRoot>/tsconfig.json then synthesizes |
| appEntry | optional | For routes mode mount-prefix detection |
| mode | optional | routes or touched. Falls back to top-level mode, then interactive prompt |
| routesPattern | optional | Defaults to /routes/ |
Single-root configs continue to work unchanged — the top-level serverRoot/tsConfig/appEntry/mode/routesPattern fields are still supported, and the output shape for single-root is identical to v0.3.
Cross-root edges aren't drawn. If a function in client/src imports from adminDashboard/src, each root only resolves symbols within its own tsconfig. The call to the cross-root function will appear as a terminal external node in the calling root. Single-tsconfig projects don't have this problem — both halves are loaded into the same ts-morph project there.
Base-branch freshness check
When baseBranch is a remote-tracking ref (e.g. upstream/release), pintu does a one-off git ls-remote to compare your local ref with the remote tip before running. If they differ, you get a warning:
[pintu] ⚠ Your local "upstream/release" is OUT OF SYNC with upstream.
[pintu] local: 1d559d441475
[pintu] remote: d6fc897d2df4
[pintu] Run `git fetch upstream release` to refresh, then re-run.
[pintu] (Pass --skip-base-check to suppress this warning.)It's non-blocking — pintu proceeds with the local copy. Skipped automatically when:
baseBranchis a local branch or SHA (no remote-tracking concept)- You pass
--skip-base-check(useful for offline / air-gapped / CI runs with pre-fetched refs) - The remote is unreachable (a soft notice is printed, run continues)
Known limitations / roadmap
- Mount prefixes from template literals with variables — e.g.
app.use(\${BASE_PATH}/api`, router)` currently falls back to router-local paths. - Cross-stack stitching — pick up
axios.get('/_/...')/fetch('/_/...')calls in client code and join them into matching route entries. - Test coverage overlay — flag which nodes have a corresponding
*.test.{ts,js}reference. <out-dir>/index.yaml— one-line entry per generated trace so future agents can grep instead of scanning the directory.- Type-resolved arg labels.
- Pre-push hook / Gitea webhook — auto-generate on PR open, commit the pair back to the branch.
- Python / Go / other languages — different ASTs entirely; would ship as sibling packages with a shared core, only when there's a concrete project to point each one at.
License
MIT © Shammi Hans
