Surgical code grafting — migrate functions between files with zero LLM token waste
Project description
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
Project details
Release history Release notifications | RSS feed
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 surgraft-0.1.0.tar.gz.
File metadata
- Download URL: surgraft-0.1.0.tar.gz
- Upload date:
- Size: 20.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
99e54d6da649723d655850a7433dae68b3b149ef99b006dab283c90f8e1c68f3
|
|
| MD5 |
fb2899d5c5218c53b66d610999f0290e
|
|
| BLAKE2b-256 |
13e49fa0983bdbf68e43df019b413cf0906cdaa4f8bc8d36e08c8a117cd6b974
|
File details
Details for the file surgraft-0.1.0-py3-none-any.whl.
File metadata
- Download URL: surgraft-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01b05742658f21c5e9697ebf01b9e895bb339d3dd708af33e19fa61aaeeee4c6
|
|
| MD5 |
6ec2715c783f2912cdb4aac8af18c79c
|
|
| BLAKE2b-256 |
2942cfc3d7afce307e18990a9d67bc129a8ed80b9dc07eec446dc8ca8ae2a1cd
|