graphLM
Point it at a codebase. Get back a map.
You've cloned an unfamiliar repo and you're staring at 400 files wondering where anything is. graphLM reads the project the way you would — but faster — and hands you a map: what the modules are, how they depend on each other, where data flows, which imports form nasty little cycles, and "where do I find X?" answers. It comes out as Markdown to read, JSON to script against, and an interactive HTML graph to click around in.
It's built for the age of coding agents, too: the map stamps itself with the git commit it was generated against, so an agent (or you) can tell at a glance when it's gone stale and regenerate. Under the hood it pairs an OpenAI-compatible LLM with deterministic Tree-sitter parsing — the AST is ground truth the model isn't allowed to contradict, so the dependency edges are real, not hallucinated.
$ graphlm ~/code/some-project
Scanning ~/code/some-project...
Wrote .graphlm/GRAPH.md, .graphlm/GRAPH.json, .graphlm/GRAPH.html
By default the map is written into a .graphlm/ folder inside the project (so it stays out of your way); point it elsewhere with -o.
What it produces
- Directory tree — annotated tree of the project
- Import edges — dependency relationships between files
- Modules — named components and what they do
- Data flow — how data moves through the system
- Database schema — tables and columns (if applicable)
- Test organization — test files mapped to what they cover
- Architecture notes — key decisions and patterns
- Quick reference — "where do I find X?" lookups
- Import cycles — strongly-connected components with SLOC-based risk scores
- Interactive HTML — D3 force graph (
GRAPH.html) with zoom/pan, search, and theme toggle - Provenance stamp —
GRAPH.jsonrecords when and against which git commit the map was generated, andGRAPH.mdopens with a refresh directive so a coding agent can tell when the map is stale (see Self-refreshing graph) - Graph-vs-graph diff — every run also writes
GRAPH_DIFF.md/GRAPH_DIFF.json: what changed in the map (modules, edges, cycles, data flows, entry points, file summaries added and removed) since the prior run, so you see a new entry point or a broken import cycle at a glance without re-reading the whole graph (see Graph diff)
Install
graphLM is a Python 3.11+ CLI. The friendliest way to get it on your PATH is a tool installer that keeps it in its own isolated environment:
uv tool install graphlm # via uv (https://github.com/astral-sh/uv)
# or
pipx install graphlm # via pipx
Either one gives you a global graphlm command. Prefer plain pip? pip install graphlm works too — just mind your virtualenvs.
No PyPI, no problem. Every release also ships the wheel and sdist on its GitHub Release. Install straight from a release asset:
pipx install https://github.com/ggrace519/graphLM/releases/download/v0.1.0/graphlm-0.1.0-py3-none-any.whl
Hacking on graphLM itself? Clone it and let uv sync the dev deps:
git clone https://github.com/ggrace519/graphLM && cd graphLM
uv sync --group dev
uv run graphlm --version
Quick start
graphLM needs an OpenAI-compatible LLM endpoint to do its thing. Point it at one with three environment variables (or the matching -b / -k / -m flags):
export GRAPHLM_BASE_URL="https://your-endpoint/v1"
export GRAPHLM_API_KEY="sk-..."
export GRAPHLM_MODEL="your-model-name"
graphlm ~/code/some-project # writes the map into ~/code/some-project/.graphlm/
Want to see what it would send the model without spending a token? Add --dry-run — it scans, parses the AST, and prints the context stats, no network call. See Configuration for the full list of settings and a .env you can drop in a project.
Usage
CLI
# Analyze a project; writes GRAPH.md, GRAPH.json, GRAPH.html into <project>/.graphlm/
graphlm /path/to/project
# Write to a different directory
graphlm /path/to/project -o ./output
# Dry run — see context stats without calling the LLM
graphlm /path/to/project --dry-run
# Override LLM settings from the command line
graphlm /path/to/project -b https://api.example.com/v1 -k sk-xxx -m my-model
# Exclude test files and custom patterns
graphlm /path/to/project --no-tests --exclude __pycache__ --exclude .git
# Skip Tree-sitter AST import edges
graphlm /path/to/project -o ./output --no-ast
# Skip writing GRAPH.html
graphlm /path/to/project -o ./output --no-html
# Skip writing the GRAPH_DIFF.* graph-vs-graph diff
graphlm /path/to/project --no-diff
Library API
from graphlm import generate_graph
from pathlib import Path
result = generate_graph("/path/to/project")
written = result.write(Path("./output"))
md_path, json_path, html_path = written
# html_path is Path | None (None if include_html=False)
diff_md = written.diff_md # Path | None (None if include_diff=False)
diff_json = written.diff_json # Path | None
GraphResult.write accepts str | Path and returns a WriteResult — the (Markdown, JSON, HTML) path tuple you can unpack three ways, with .diff_md / .diff_json attributes for the graph diff (None when include_diff=False).
result = generate_graph(
"/path/to/project",
base_url="https://api.example.com/v1",
api_key="sk-xxx",
model="Qwen3.6-35B",
output_dir="./output",
ast=True, # Tree-sitter import edges + SLOC cycle scores (default)
include_html=True, # skip GRAPH.html when False (default: write it)
include_diff=True, # skip GRAPH_DIFF.* when False (default: write it)
show_cycles=True, # skip the cycle section when False
cycle_threshold=0.0, # min cycle risk score
)
print(len(result.graph.modules), "modules found")
AST parsing is on by default: graphLM runs Tree-sitter, attaches graph.deterministic_edges, passes those edges into the pass-2 prompt as ground truth, and runs cycle detection on the AST edges with SLOC-based risk scores. Pass ast=False or --no-ast to skip. include_html=False skips writing GRAPH.html when output_dir is set (HTML is on by default).
How it works
graphLM uses a two-pass LLM strategy to stay within context windows while still producing comprehensive graphs:
- Pass 1 — The directory tree (no file contents) is sent to the LLM, which identifies the most important files to read.
- Pass 2 — The tree + those key files are sent to the LLM, which produces the final structured graph.
This keeps the first pass lightweight (~tree tokens) and ensures the second pass only includes files that matter.
A Tree-sitter pass (Python imports) runs by default. It does not replace the LLM: the two-pass analysis still runs, and AST edges are extra ground truth plus cycle detection. Pass --no-ast to skip.
Teach your coding agent to use it
The map is most useful when your coding agent reads it automatically before it starts spelunking through a codebase. One command sets that up:
graphlm --install-skill claude # writes ~/.claude/skills/graphlm/SKILL.md
graphlm --install-skill codex # writes ~/.codex/graphlm.md + a snippet to paste into AGENTS.md
It drops a short guide telling the agent to look for .graphlm/GRAPH.md when it opens a repo, follow the map's refresh directive, and regenerate with graphlm . when the map is missing or stale. Installs user-global by default (so every repo benefits); add --skill-local to write into the current project instead, and --skill-force to overwrite an existing guide.
graphLM only ever creates its own files — it will never edit your existing CLAUDE.md or AGENTS.md. For Codex (whose config is a user-owned AGENTS.md), it writes a standalone guide and prints the one line for you to paste in yourself.
Self-refreshing graph
A generated graph goes stale the moment the code moves on. graphLM makes the
output self-refreshing without any hook or flag: it stamps its own provenance
and rides the refresh nudge along in the loop an agent already uses to read
GRAPH.md.
- The stamp.
GRAPH.jsoncarries a versionedmetablock —created_at(UTC),commit_sha(the gitHEADthe graph was generated against, ornulloutside a git repo),graphlm_version, andschema_version.GRAPH.mdopens with a short refresh directive rendered from that stamp. - The agent is the scheduler. graphLM has no staleness logic — invoked, it
always regenerates and re-stamps. The directive tells a reading agent to
compare the repo's current
git rev-parse HEADto the stamped commit and, if they differ, regenerate withgraphlm .. Staleness = SHA mismatch. - It's advisory. The agent may ignore the directive; the map is best-effort, not guaranteed current. Non-git projects have no SHA, so the directive falls back to "regenerate when you believe the code has changed."
- Honest wording. The stamp says "generated against commit X", not "reflects X": the graph is built from files on disk, which may include uncommitted changes, so a graph can be SHA-fresh yet not match the working tree.
Adoption — one line for an AGENTS.md / rules file (or just run graphlm --install-skill claude / --install-skill codex, below):
A codebase map lives at
.graphlm/GRAPH.md— read it before exploring the code, and follow its refresh directive (regenerate withgraphlm .when the stamped commit differs from the currentHEAD, or when the map is missing).
Graph diff
Once the map is self-stamped and regenerated as the code moves, the natural next
question is "what changed in the map since last time?" Every real run answers
it by also writing GRAPH_DIFF.md and GRAPH_DIFF.json — a
graph-vs-graph diff, not a code diff (git already does code diffs better).
- What it reports. Per dimension — modules, import edges (LLM and AST),
import cycles, data flows, entry points, file summaries — the entities
added and removed since the prior
GRAPH.json. So a new entry point, a dropped module, or a newly broken/resolved import cycle is visible at a glance without re-reading the whole graph. - Added/removed only. Identity is structural (a module's
path, an edge's(from, to, kind), a cycle's node set), so a pure prose rewrite — a description or summary the LLM regenerates every run — is intentionally invisible. It would otherwise drown the structural signal in nondeterministic churn. Renames show as remove + add (no rename-matching). - Three baseline states, never conflated. First run ("initial graph — no
prior version to compare"), uncomparable (the prior file is corrupt or an
unrecognized
schema_version— this is not silently treated as a first run), and normal. An agent can always tell "nothing changed" from "never compared." - Commit range. The diff header shows the old→new
commit_sharange (anullside — non-git or an old graph — reads asunknown). --no-astsafety. Toggling AST parsing off between runs reports the AST edge dimension as "not compared" rather than fabricating a mass deletion.- Reads graphlm's own prior output. This is why the
metablock is a versioned input contract (above): a future format change is detected, not misparsed.
On by default. Pass --no-diff (or include_diff=False) to skip it. --dry-run
writes no diff — it makes no LLM call and produces no authoritative graph. The
diff is pure local computation over the two graphs: no extra network or LLM call.
Opting out only skips writing — like --no-html, it does not delete a
GRAPH_DIFF.* left by a previous run, so a stale diff can linger on disk;
regenerate (or remove it) if that matters.
Committing vs. gitignoring the graph. The refresh check is stamped_sha != HEAD, so if you commit GRAPH.*, the stamp is invalidated by the very commit
that ships it — HEAD moves to that commit, so the map immediately reads as
one commit stale, and stays perpetually one commit behind. Two sane options:
- Gitignore
.graphlm/(this repo's own choice) and regenerate on demand. The stamp then always reflects a real, current SHA. (One line —echo '.graphlm/' >> .gitignore— covers the whole output folder.) - Commit it and regenerate as the final step of the same commit so the map ships fresh — but expect it to show one-commit staleness until the next regen, and treat that as normal.
Committing a graph that goes stale on every push (with no regeneration step) is the one workflow to avoid — it reintroduces exactly the per-session refresh tax this design set out to remove.
Note on -o: the default (.graphlm/ inside the scanned project) keeps the
map in the repo it describes, so the staleness check works. If you redirect
output elsewhere (-o <elsewhere>), an agent reading that GRAPH.md and running
git rev-parse HEAD in its own directory will compare against the wrong repo —
keep the graph in the project it describes for the staleness check to work.
Configuration
graphLM reads its LLM settings from environment variables. Copy .env.example to .env and fill in your values:
cp .env.example .env
| Variable | Description | Default |
|---|---|---|
GRAPHLM_BASE_URL |
OpenAI-compatible API endpoint | https://openrouter.ai/api/v1 |
GRAPHLM_API_KEY |
API key for authentication | (required) |
GRAPHLM_MODEL |
Model name to use | openai/gpt-4o |
Settings can also be passed directly via CLI flags (-b, -k, -m) or library arguments.
Options
| Flag | Description | Default |
|---|---|---|
-o, --output-dir |
Output directory for GRAPH.md, GRAPH.json, and GRAPH.html |
<project>/.graphlm/ |
-b, --base-url |
LLM API base URL | GRAPHLM_BASE_URL env var |
-k, --api-key |
LLM API key | GRAPHLM_API_KEY env var |
-m, --model |
Model name | GRAPHLM_MODEL env var |
--max-files |
Maximum files to scan initially | 200 |
--max-file-chars |
Maximum characters per file | 4000 |
--max-pass2-files |
Max files in pass 2 context | 80 |
--max-context |
Token budget for pass-2 context | GRAPHLM_MAX_CONTEXT env var, else 120000 |
--no-tests |
Exclude test files | Tests included by default |
--exclude |
Exclude pattern (repeatable) | — |
--no-redact |
Skip secret redaction | Redaction on |
--dry-run |
Show stats without calling LLM | Disabled |
--no-ast |
Skip Tree-sitter AST import edges | AST on |
--no-html |
Do not write GRAPH.html |
HTML on |
--no-show-cycles |
Skip the cycle section | Cycles on |
--cycle-threshold |
Minimum cycle risk score | 0.0 |
--install-skill <harness> |
Install an agent guide (claude / codex) and exit |
— |
--skill-local |
With --install-skill: write into the project, not user-global |
User-global |
--skill-force |
With --install-skill: overwrite an existing guide |
Skip if exists |
-V, --version |
Print the version and exit | — |
Project structure
graphlm/
├── __init__.py # Library API — generate_graph()
├── _html_template.html # D3 visualization template
├── cli.py # CLI entry point — Typer
├── config.py # Settings from environment variables
├── context.py # Two-pass prompt assembly
├── cycles.py # Import cycle detection (Tarjan + SLOC risk)
├── html_render.py # Interactive D3 HTML visualization
├── llm.py # LLM client with retry and JSON recovery
├── models.py # Pydantic v2 data models
├── parser.py # Tree-sitter AST import parser
├── prompts.py # System prompt (injection guard)
├── provenance.py # Git SHA / timestamp / version capture for the stamp
├── render.py # Markdown + JSON + HTML output rendering
├── scanner.py # Project directory scanner
└── skills.py # --install-skill: agent-guide installer
tests/
├── conftest.py
├── test_cli.py
├── test_config.py
├── test_context.py
├── test_cycles.py
├── test_html_render.py
├── test_integration.py
├── test_llm.py
├── test_models.py
├── test_parser.py
├── test_prompts.py
├── test_provenance.py
├── test_render.py
├── test_scanner.py
└── fixtures/ # Small, medium, large test projects
Requirements
- Python 3.11, 3.12, or 3.13
- An OpenAI-compatible LLM endpoint (base URL + API key + model name)
- uv — recommended for installing (
uv tool install) and required for development
Contributing
Contributions are welcome — bug reports, fixes, docs, and new language support especially. See CONTRIBUTING.md for setup, the test/mypy commands, and the security invariants to preserve. Please also read the Code of Conduct.
Security
Found a vulnerability? Please don't open a public issue. Report it privately via GitHub's Report a vulnerability button — see SECURITY.md for scope and details. graphLM reads code it didn't write, so its sensitive-file, redaction, symlink, and prompt-injection guards are the surface that matters most.
License
GPLv3 — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file graphlm-0.1.2.tar.gz.
File metadata
- Download URL: graphlm-0.1.2.tar.gz
- Upload date:
- Size: 212.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ceadeb82c874652e0aa8d9b9b0acede6f0e6849c4080d5f9729f95ea0b9ba927
|
|
| MD5 |
bb930b278c2644c2586e06455d4ebb0f
|
|
| BLAKE2b-256 |
898b4302c2d07332673bbef745d6289a38a838f71699171c82e13dc5988d785e
|
Provenance
The following attestation bundles were made for graphlm-0.1.2.tar.gz:
Publisher:
release.yml on ggrace519/graphLM
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
graphlm-0.1.2.tar.gz -
Subject digest:
ceadeb82c874652e0aa8d9b9b0acede6f0e6849c4080d5f9729f95ea0b9ba927 - Sigstore transparency entry: 2657303713
- Sigstore integration time:
-
Permalink:
ggrace519/graphLM@240a7c1c88b49c5b772be9ee34408a84e2c98359 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/ggrace519
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@240a7c1c88b49c5b772be9ee34408a84e2c98359 -
Trigger Event:
push
-
Statement type:
File details
Details for the file graphlm-0.1.2-py3-none-any.whl.
File metadata
- Download URL: graphlm-0.1.2-py3-none-any.whl
- Upload date:
- Size: 83.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4782caebc82b1c63cc94045b5aa41162c3bf2769ddfd0d9760de5fce1994679c
|
|
| MD5 |
68f9f835e0434efe3261fabf46747aa1
|
|
| BLAKE2b-256 |
4984726d4dec80e6ce816282597dbca712d19cd13a681ee5299c8b295b696c8c
|
Provenance
The following attestation bundles were made for graphlm-0.1.2-py3-none-any.whl:
Publisher:
release.yml on ggrace519/graphLM
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
graphlm-0.1.2-py3-none-any.whl -
Subject digest:
4782caebc82b1c63cc94045b5aa41162c3bf2769ddfd0d9760de5fce1994679c - Sigstore transparency entry: 2657303765
- Sigstore integration time:
-
Permalink:
ggrace519/graphLM@240a7c1c88b49c5b772be9ee34408a84e2c98359 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/ggrace519
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@240a7c1c88b49c5b772be9ee34408a84e2c98359 -
Trigger Event:
push
-
Statement type: