Current section
Files
Jump to
Current section
Files
README.md
# Adze
[](https://hex.pm/packages/adze)
[](https://hexdocs.pm/adze)
[](LICENSE)
AI coding agents waste context window reading 2000-line files, then fumble
multi-edit refactors across a codebase. Adze fixes both problems.
**Adze parses Elixir source as AST, returns a structural skeleton with exact
line ranges, and lets the agent read only the lines it needs.** When it's time to refactor, the same AST machinery
performs safe mechanical transforms — extract, rename, reorder — that would be
tedious and error-prone as multi-edits.
Built on [Sourceror](https://hex.pm/packages/sourceror) and
[Igniter](https://hex.pm/packages/igniter).
## Requirements
- Elixir `~> 1.19`.
- Adze must be a dependency of the project you're working on. It's a Mix task
that reads your project's formatter config, `elixirc_paths` and compiled
modules (to check `@behaviour` and `use` targets), so there's no global or
standalone install.
- It's a dev-only dependency. Run it as `mix adze ...` from the project root
with `MIX_ENV` unset or `dev`.
## Getting Started
Pick one of two install paths. Both end up in the same place.
### Option 1: Igniter (recommended)
[Igniter](https://hex.pm/packages/igniter) adds the dependency and sets up
agent integration in one step. If your project doesn't already use Igniter,
install its archive first:
```bash
mix archive.install hex igniter_new
```
Then:
```bash
mix igniter.install adze
```
This adds the dependency *and* sets up skill files so your agent knows to reach
for `mix adze` instead of hand-editing:
| Platform | Skill path |
|----------|-----------|
| Claude Code | `.claude/skills/adze/SKILL.md` |
| Codex / Pi / OpenCode | `.agents/skills/adze/SKILL.md` |
Both skills are always installed, since Claude Code only loads skills from
`.claude/skills/` and the other agents from `.agents/skills/`.
It also adds an adze section to `AGENTS.md` (creating it if needed). Codex, Pi
and OpenCode read `AGENTS.md`. Claude Code (v2.1.277+) reads it too, but only
when the project has no `CLAUDE.md`, `.claude/CLAUDE.md` or `CLAUDE.local.md`.
So if your project has a `CLAUDE.md`, the installer appends an `@AGENTS.md`
import to it (and removes the adze section older versions wrote there). It
never creates a `CLAUDE.md` and never edits `CLAUDE.local.md`; if that's your
only one, add the `@AGENTS.md` line yourself.
To upgrade, use `mix igniter.upgrade adze` instead of `mix deps.update adze`.
It updates the dependency and refreshes the skills and the `AGENTS.md`
section. When a release changes how adze is used, it also lists anything in
your own files (scripts, CI, your own agent notes) that needs updating. If you
upgraded with `mix deps.update`, or use adze as a path dependency, run
`mix igniter.install adze` afterwards to refresh the files.
### Option 2: Manual
Add `adze` as a dev dependency:
```elixir
def deps do
[
{:adze, "~> 0.2.0", only: [:dev]}
]
end
```
Then `mix deps.get`.
#### Agent integration
After fetching deps, copy the skill file and add a reference to your
agent config:
**Claude Code:**
```bash
mkdir -p .claude/skills/adze
cp deps/adze/skill.md .claude/skills/adze/SKILL.md
```
**Codex / Pi / OpenCode:**
```bash
mkdir -p .agents/skills/adze
cp deps/adze/skill.md .agents/skills/adze/SKILL.md
```
Then append the contents of
[`agents-snippet.md`](https://github.com/matthewlehner/adze/blob/main/agents-snippet.md) (also at
`deps/adze/agents-snippet.md`) to your `AGENTS.md`, creating it if it
doesn't exist:
```bash
cat deps/adze/agents-snippet.md >> AGENTS.md
```
After upgrading adze, copy `skill.md` again and replace the adze section in
`AGENTS.md`. Or switch to Option 1, which does both for you.
## When to Reach for Adze
| You're about to… | Use this instead |
|------------------|------------------|
| Read an Elixir file > 200 lines | `mix adze ls --file PATH` first, then read only the line range you need |
| Multi-edit a `defmodule` rename across the project | `mix adze rename --from Old --to New` |
| Copy a function + its private helpers into a new module by hand | `mix adze extract --file ... --definition fun/N --module New.Module` |
| Reorder defs inside a module with multi-edit | `mix adze mv --file ... --definition fun/N --before other/M` |
| Grep for callers of `Mod.fun` across the project | `mix adze find-callers --target Mod.fun/N` |
| Convert a `def` to `defp` after eyeballing call sites | `mix adze extract-private --file ... --definition fun/N` |
| Eyeball a module's `alias`/`import`/`use` clauses | `mix adze aliases --file PATH` |
If none of these match, fall back to standard read/edit/grep.
## Quick Start
```bash
# Map a file's structure without reading all of it
mix adze ls --file lib/my_app/accounts.ex
# Find all callers of a function across the project
mix adze find-callers --target MyApp.Accounts.register/1
# Several arities at once (e.g. a function with default arguments)
mix adze find-callers --target MyApp.Accounts.register/1,2
```
### Write, then compile
Write commands write to disk and print a summary: the files they touched with
+/- line counts, and anything you need to act on. Every safety check runs
before anything is written, so a refused command leaves your files alone. Use
`git diff` for the full changes, or add `--dry-run` to see them without
writing:
```
$ mix adze extract --file lib/sample.ex --definition greet/1 --module Sample.Greeter --dry-run
# new file: ./lib/sample/greeter.ex (Sample.Greeter)
defmodule Sample.Greeter do
@spec greet(String.t()) :: String.t()
def greet(name) when is_binary(name) do
"hello, " <> name
end
def greet(_), do: "hello, stranger"
end
# source diff
--- a
+++ b
@@ -13,13 +13,6 @@
defstruct [:id, :name, count: 0]
- @spec greet(String.t()) :: String.t()
- def greet(name) when is_binary(name) do
- "hello, " <> name
- end
-
- def greet(_), do: "hello, stranger"
-
defp internal(x), do: x * 2
...
# dropped directives (use / import / require)
# add back to the new target module if the build needs them.
source:6 use GenServer
source:8 import Enum, only: [map: 2]
source:9 require Logger
```
Drop `--dry-run` to apply it. You get the summary:
```
$ mix adze extract --file lib/sample.ex --definition greet/1 --module Sample.Greeter
# extract → Sample.Greeter
created ./lib/sample/greeter.ex (8 lines)
updated lib/sample.ex (+0 −7)
# dropped directives (use / import / require)
# add back to the new target module if the build needs them.
source:6 use GenServer
source:8 import Enum, only: [map: 2]
source:9 require Logger
```
Then compile:
```bash
mix adze extract --file lib/sample.ex --definition greet/1 --module Sample.Greeter
mix compile --warnings-as-errors
```
## Command Reference
### Read-only (safe, never writes)
| Command | Description |
|---------|-------------|
| `mix adze ls --file PATH` | Structural outline with line ranges |
| `mix adze deps --file PATH` | Intra-module call graph (caller → callees) |
| `mix adze ls-deps --file PATH --definition fun/N` | Transitive dependency tree from one function |
| `mix adze ls-extract --file PATH --definition fun/N` | Private closure that would move with a function |
| `mix adze aliases --file PATH` | All `alias`/`import`/`require`/`use` per module |
| `mix adze find-callers --target Mod.fun/N` | Project-wide callers (resolves aliases, pipes, captures) |
### Write (refactoring)
Each write op writes to disk and prints a summary. Add `--diff` to print the
full diffs too, or `--dry-run` to print them without writing.
| Command | Description |
|---------|-------------|
| `mix adze mv --file PATH --definition fun/N --before other/M` | Reorder a definition within a module |
| `mix adze extract --file PATH --definition fun/N --module New.Mod` | Extract function + closure into a new module |
| `mix adze rename --from Old.Mod --to New.Mod` | Rename a module across the entire project |
| `mix adze extract-private --file PATH --definition fun/N` | Flip `def` → `defp` when no external callers exist |
<details>
<summary>Example output: <code>mix adze ls</code></summary>
```
$ mix adze ls --file lib/sample.ex
# lib/sample.ex
defmodule Sample L1–40
@moduledoc L2–4
use GenServer L6
alias Sample.Inner L7
import Enum L8
require Logger L9
@behaviour GenServer L11
@some_attr L12
defstruct [:id, :name, :count] L14
@spec greet L16
greet/1 when … L17–19
greet/1 L21
p internal/1 L23
m debug/1 L25–29
g is_pos/1 when … L31
d child_call/1 L33
```
Each line is a definition with its line range. A one-character marker column
precedes the name: `p` = `defp`, `m` = `defmacro`, `g` = `defguard`,
`d` = `defdelegate`; public `def`s have a blank marker (hence the deeper
indent). `@spec greet` is the spec for `greet`. Use `--format json` for the
same data as a structured tree.
</details>
## Configuration
### Custom attachable attributes
Some libraries introduce attributes that semantically belong to the next
`def` (e.g. Oban's `@job`, Decorator's `@decorate`, Absinthe's `@desc`).
Tell adze to treat them as part of the definition group:
```elixir
# config/config.exs
config :adze, include_attrs: [:job, :decorate, :desc]
```
### Output format
Every command accepts `--format json` for programmatic consumption.
Text (default) is optimized for humans and LLMs reading inline.
## How It Works
Elixir is homoiconic — source code is data. Sourceror gives us a zipper
over the AST with formatting and comments preserved. Every structural
operation is a tree walk, not string manipulation.
Single-file analysis (outline, deps, aliases) uses raw Sourceror.
Project-wide operations (rename, extract caller rewriting, find-callers)
use Igniter for safe multi-file rewrites with formatting preservation.
## Limitations
`find-callers` (and by extension `extract-private`) resolves qualified calls,
pipe variants, and `&Mod.fun/n` captures — including alias resolution. It
**won't find:**
- Unqualified calls via `import`
- Dynamic dispatch (`apply/3`, `Kernel.apply/3`)
- String-literal module references (`"Elixir.MyApp.Foo"`)
Because callbacks and dynamically dispatched functions have no visible
callers, `extract-private` always refuses `@impl` definitions and callbacks
of a loadable `@behaviour` / `use`d behaviour, and refuses anything in a
module whose `use` / `@behaviour` target it can't load as a behaviour
(e.g. `use MyAppWeb, :controller`) unless you pass `--force`.
Functions reached via `mod.fun()` / `apply/3` from elsewhere are still
invisible to it.
When in doubt, run `mix compile --warnings-as-errors` after any write op.
The compiler catches what adze can't.
## Documentation
Full API documentation is available on [HexDocs](https://hexdocs.pm/adze).
## Prior Art & Acknowledgments
Adze exists because of the tools it builds on and the project that showed
the way:
- **[clj-surgeon](https://github.com/realgenekim/clj-surgeon)** by Gene Kim
([@realgenekim](https://github.com/realgenekim)) — the direct inspiration.
Gene watched Claude Code spend 45 minutes refactoring a 5,000-line Clojure
file and asked: *"What would the ideal tool be to help you manipulate
beautiful homoiconic EDN files?"* The answer was 13 ops in ~1,500 lines of
Clojure, built in one session. Adze is the Elixir port of that idea:
same philosophy (outline-first exploration, AST-aware refactoring, the tool
does bookkeeping while the AI decides what to do), adapted for Elixir's
ecosystem.
- **[Sourceror](https://hex.pm/packages/sourceror)** by Dorgan
([@doorgan](https://github.com/doorgan)) — the foundation. Every single-file
operation in adze is a tree walk on the zipper that Sourceror provides.
Comments and formatting are preserved because Sourceror tracks them in AST
metadata. Without it, adze would need a custom parser.
- **[Igniter](https://hex.pm/packages/igniter)** by Zach Daniel
([@zachdaniel](https://github.com/zachdaniel)) — the project-wide rewrite
engine. Cross-file operations (rename, extract caller rewriting, find-callers)
use Igniter for safe multi-file rewrites with formatter preservation. The
installer task uses Igniter's code generation framework.
The guiding principle, borrowed directly from clj-surgeon:
> **The tool does the mechanical work. The AI decides what to do. The compiler
> catches mistakes. The AI fixes what the compiler reports.**
## License
[MIT](LICENSE)