surgraft
Surgical code grafting. Spend tokens only on what changed.
When refactoring a large file, most LLM tools read a function and then rewrite it into the new file — burning tokens on code that didn't change. surgraft separates the two concerns:
- Locate — find function boundaries using AST parsing (zero tokens)
- Copy — shell-level byte copy into the new file (zero tokens)
- Edit — LLM sees only the extracted snippet (tokens ∝ delta, not file size)
5,000-line file, 20-line function, naive LLM rewrite: ~5,000 tokens
surgraft copy-only: 0 tokens
surgraft + edit pass: ~25 tokens
Install
pip install surgraft # copy-only (no API key needed)
pip install "surgraft[llm]" # + LLM edit pass via Anthropic
Usage
List symbols in a file
surgraft list old_service.py
old_service.py (4,821 lines)
SYMBOL KIND LINES
────────────────────────────────────────────────────────────
AuthService class 42–310 (269L)
AuthService.validate_token function 58–89 (32L)
AuthService.refresh_session function 91–134 (44L)
parse_jwt function 312–341 (30L)
hash_password function 343–367 (25L)
...
23 symbols
Graft symbols — zero tokens
Copy functions into a new file without touching the LLM at all:
surgraft graft old_service.py new_auth.py \
--symbols "validate_token,refresh_session,parse_jwt"
→ Source old_service.py
→ Dest new_auth.py
✓ Grafted validate_token lines 58–89 → new_auth.py
✓ Grafted refresh_session lines 91–134 → new_auth.py
✓ Grafted parse_jwt lines 312–341 → new_auth.py
Tokens spent on copy: 0 (naive rewrite ≈ 19,284 tokens)
Graft + edit — tokens only on the delta
Copy and transform in one step. The LLM sees only the extracted snippet:
surgraft graft old_service.py new_auth.py \
--symbols "validate_token,refresh_session" \
--edit "convert to async/await, replace self.db with db: AsyncSession parameter"
→ Source old_service.py
→ Dest new_auth.py
→ Edit 'convert to async/await...'
→ Editing validate_token (~142 tokens)
✓ Grafted (edited) validate_token → new_auth.py
→ Editing refresh_session (~176 tokens)
✓ Grafted (edited) refresh_session → new_auth.py
Token usage: 318 tokens used (naive rewrite ≈ 19,284 tokens, saved ~98%)
Graft all symbols
surgraft graft old_service.py new_service.py --symbols ALL
Check migration progress
See what's been moved and what's still outstanding:
surgraft diff old_service.py new_service.py
Migration gap: old_service.py → new_service.py
NOT YET GRAFTED
✗ parse_jwt 30L
✗ hash_password 25L
✗ AuthService.rotate_key 18L
ALREADY PRESENT
✓ validate_token
✓ refresh_session
3 remaining / 2 done / 5 total
Dry run
Preview what would be copied without writing anything:
surgraft graft old.py new.py --symbols parse_jwt --dry-run
How it works
┌─────────────────────────────────────────────────────────┐
│ surgraft graft │
└────────────────────┬────────────────────────────────────┘
│
┌───────────▼───────────┐
│ AST / regex parser │ ← zero tokens
│ finds line boundaries │
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ Shell byte copy │ ← zero tokens
│ sed -n 'N,Mp' → dest │
└───────────┬───────────┘
│
--edit provided?
│
YES ───────▼──────── NO → done
┌───────────────────┐
│ LLM edit pass │ ← tokens ∝ snippet, not file
│ sees: snippet │
│ + instruction │
│ outputs: snippet │
└───────────────────┘
Supported languages
| Language | Method | Accuracy |
|---|---|---|
| Python | ast module |
Exact |
| JavaScript / TypeScript | Regex heuristic | Good |
| JSX / TSX | Regex heuristic | Good |
API
Use surgraft as a library:
from surgraft.extractor import extract_symbols, find_symbol, graft_lines, read_lines
from surgraft.llm import edit_snippet
# find all symbols
symbols = extract_symbols("old_service.py")
# locate one
sym = find_symbol("old_service.py", "validate_token")
print(sym.start_line, sym.end_line) # 58 89
# copy — zero tokens
graft_lines("old_service.py", sym.start_line, sym.end_line, "new_auth.py")
# edit — tokens ∝ snippet only
snippet = read_lines("old_service.py", sym.start_line, sym.end_line)
edited = edit_snippet(snippet, "make this async")
with open("new_auth.py", "a") as f:
f.write(edited)
Configuration
Set your API key for the edit pass:
export ANTHROPIC_API_KEY=sk-ant-...
Or pass it inline:
surgraft graft old.py new.py --symbols fn --edit "..." --api-key sk-ant-...
Why not just use an LLM agent?
LLM agents do solve refactoring — but they solve it by reading and rewriting. For a 5,000-line file with 50 functions, that's 50 × ~5,000 = 250,000 tokens spent transcribing unchanged code. surgraft treats the LLM as a transformer, not a copier. The bytes that didn't change never touch the model.
Contributing
git clone https://github.com/your-name/surgraft
cd surgraft
pip install -e ".[dev]"
pytest
License
MIT
Metadata
Release files for surgraft 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| surgraft-0.1.0.tar.gz | 20.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| surgraft-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.8 kB
Release files / surgraft-0.1.0.tar.gz
| Download URL | surgraft-0.1.0.tar.gz |
|---|---|
| Size | 20.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
99e54d6da649723d655850a7433dae68b3b149ef99b006dab283c90f8e1c68f3
|
|
BLAKE2b-256 checksum How to use checksums |
13e49fa0983bdbf68e43df019b413cf0906cdaa4f8bc8d36e08c8a117cd6b974
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|
Release files / surgraft-0.1.0-py3-none-any.whl
| Download URL | surgraft-0.1.0-py3-none-any.whl |
|---|---|
| Size | 13.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
01b05742658f21c5e9697ebf01b9e895bb339d3dd708af33e19fa61aaeeee4c6
|
|
BLAKE2b-256 checksum How to use checksums |
2942cfc3d7afce307e18990a9d67bc129a8ed80b9dc07eec446dc8ca8ae2a1cd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|