Skip to main content

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:

  1. Locate — find function boundaries using AST parsing (zero tokens)
  2. Copy — shell-level byte copy into the new file (zero tokens)
  3. 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)

Source distribution for surgraft 0.1.0
File Size Uploaded
surgraft-0.1.0.tar.gz 20.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for surgraft 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page