Metadata-Version: 2.5
Name: graphlm
Version: 0.1.3
Summary: Generate codebase graphs (Markdown + JSON + interactive HTML) from any project directory using an OpenAI-compatible LLM
Project-URL: Homepage, https://github.com/ggrace519/graphLM
Project-URL: Repository, https://github.com/ggrace519/graphLM
Project-URL: Changelog, https://github.com/ggrace519/graphLM/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/ggrace519/graphLM/issues
Author-email: Greg Grace <graphlm@519lab.com>
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Keywords: cli,codebase,documentation,graph,llm,static-analysis,tree-sitter
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Documentation
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx
Requires-Dist: pydantic>=2.0
Requires-Dist: python-dotenv
Requires-Dist: tree-sitter-python<0.26,>=0.25.0
Requires-Dist: tree-sitter<0.27,>=0.26.0
Requires-Dist: typer
Description-Content-Type: text/markdown

# 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.

```console
$ 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.json` records when and against which git commit the map was generated, and `GRAPH.md` opens with a refresh directive so a coding agent can tell when the map is stale (see [Self-refreshing graph](#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](#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:

```bash
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](https://github.com/ggrace519/graphLM/releases). Install straight from a release asset:

```bash
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:

```bash
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):

```bash
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](#configuration) for the full list of settings and a `.env` you can drop in a project.

## Usage

### CLI

```bash
# 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

```python
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`).

```python
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:

1. **Pass 1** — The directory tree (no file contents) is sent to the LLM, which identifies the most important files to read.
2. **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:

```bash
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.json` carries a versioned `meta` block — `created_at`
  (UTC), `commit_sha` (the git `HEAD` the graph was generated against, or `null`
  outside a git repo), `graphlm_version`, and `schema_version`. `GRAPH.md` opens
  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 HEAD` to the stamped commit and, if
  they differ, regenerate with `graphlm .`. 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 with `graphlm .` when the
> stamped commit differs from the current `HEAD`, 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_sha` range (a
  `null` side — non-git or an old graph — reads as `unknown`).
- **`--no-ast` safety.** 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 `meta` block 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:

```bash
cp .env.example .env
```

Settings are resolved in this order (first non-empty wins):

1. **Exported shell environment** — `export GRAPHLM_BASE_URL=…` etc.
2. **Project `.env`** — searched from the current working directory upward, so it works whether graphLM is run from a source checkout or `uv tool install`ed.
3. **User-level `.env`** at `~/.config/graphlm/.env` (or `$XDG_CONFIG_HOME/graphlm/.env`) — a global config for an installed graphLM, so you don't need a `.env` in every project.
4. Built-in defaults.

| 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](https://github.com/astral-sh/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](CONTRIBUTING.md) for setup, the test/mypy commands, and the security invariants to preserve. Please also read the [Code of Conduct](CODE_OF_CONDUCT.md).

## Security

Found a vulnerability? **Please don't open a public issue.** Report it privately via GitHub's [Report a vulnerability](https://github.com/ggrace519/graphLM/security/advisories/new) button — see [SECURITY.md](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](LICENSE).
