dripbird is a TypeScript/JavaScript automated refactoring tool. It reads a unified diff from stdin, identifies changed lines, applies a set of automated refactors only to the changed regions, and writes modified files back in place.
It uses recast for format-preserving AST transformations and optional LLM integration (via Moonshot AI) for intelligent function naming.
The wise dripbird sits perched among the highest branches of the Abstract Syntax Trees in the forest of code. Looking snatched in a suit that high key slays, the dripbird is ready to lock in and sharpen your change set before it becomes canon—for real.
git diff --cached HEAD~1 | dripbird
dripbird operates only on the lines you actually changed — not the whole file. Each refactor receives the diff's line ranges and skips code outside those ranges. This makes it safe to run on any in-progress change without disturbing surrounding code.
If dripbird modifies a file, it exits with code 1 (signaling a pre-commit hook to abort so you can re-stage). If no changes are needed, it exits 0.
Requires Deno 2.0+ and Lefthook.
git clone https://github.com/Voidious/dripbird
cd dripbird
deno task installPipe any unified diff to dripbird:
# Refactor uncommitted changes
git diff | dripbird
# Refactor staged changes
git diff --cached | dripbird
# Refactor changes since a specific commit
git diff HEAD~1 | dripbird
# Refactor a specific file as if it were entirely new
git diff /dev/null somefile.ts | dripbirddripbird prints a summary of every change it applies, modifies files in place, and exits 1 if any file was changed.
dripbird reads optional YAML config files from your project root:
dripbird.yml— committed, shared project defaults.dripbird.yml— local overrides (git-ignored, for personal preferences)
Local overrides take precedence over committed settings.
| Option | Default | Description |
|---|---|---|
max_function_lines |
75 |
Line count threshold above which the function splitter will consider splitting a function |
function_splitter_retries |
2 |
Number of LLM retry attempts when naming a helper function |
function_matcher_retries |
2 |
Number of LLM retry attempts when a function matcher edit fails verification |
provider |
"moonshot" |
LLM provider (currently only "moonshot") |
model |
"kimi-k2.5" |
LLM model name to use |
enabled_refactors |
[] |
If non-empty, only these refactors will run |
disabled_refactors |
[] |
These refactors will be skipped |
verbose |
false |
Print detailed log output for each refactor |
max_function_lines: 50
function_splitter_retries: 3
disabled_refactors:
- function_splitterThe function splitter and function matcher refactors require a Moonshot AI API key.
Set the MOONSHOT_API_KEY environment variable:
export MOONSHOT_API_KEY="your-api-key-here"If the API key is not set, both are automatically disabled. All other refactors (e.g., flip negated if/else) work without LLM access.
Flips if (!condition) { ... } else { ... } to eliminate the negation.
When an if has a negated condition (!) and an else clause that is not an
else if, dripbird removes the ! and swaps the two branches. This eliminates a
layer of logical indirection and makes intent clearer.
Before:
if (!validInput(frequency, duration)) {
doErrorThing();
} else {
doMainThing();
}After:
if (validInput(frequency, duration)) {
doMainThing();
} else {
doErrorThing();
}Skipped when:
- There is no
elseclause - The
elseis anelse ifchain (which would change semantics) - The condition is not a top-level
!expression
Splits long functions into smaller, well-named helper functions.
When a function exceeds max_function_lines and falls within the diff, dripbird
identifies a good split point, computes the free variables the tail needs, extracts
the tail into a new helper function, and replaces the original tail with a call to
it. The helper function's name is suggested by an LLM to be semantically meaningful.
Works on both standalone function declarations and class methods. For class methods,
it automatically determines whether the helper should be a static method (if this
is not used) or an instance method.
Before:
function processOrder(order: Order, user: User) {
validateOrder(order);
const total = calculateTotal(order.items);
const discount = applyDiscount(user, total);
const finalAmount = total - discount;
chargePayment(finalAmount, user.paymentMethod);
sendConfirmation(user.email, order.id);
updateInventory(order.items);
logTransaction(order.id, finalAmount);
}After:
function processOrder(order: Order, user: User) {
validateOrder(order);
const total = calculateTotal(order.items);
const discount = applyDiscount(user, total);
const finalAmount = total - discount;
chargePayment(finalAmount, user.paymentMethod);
return finalizeOrder(user, order, finalAmount);
}
function finalizeOrder(user: User, order: Order, finalAmount: number) {
sendConfirmation(user.email, order.id);
updateInventory(order.items);
logTransaction(order.id, finalAmount);
}Skipped when:
- The function is under
max_function_lines - The function is
asyncor a generator - The function contains nested function declarations
- No LLM API key is configured (
MOONSHOT_API_KEY)
Replaces duplicate code with calls to existing functions.
When a code block within the diff is semantically identical to an existing function body (ignoring variable names), dripbird replaces it with a call to that function. It uses fingerprint-based matching to find candidates and LLM verification to confirm the match is semantically correct.
Works on standalone function declarations and static class methods. It matches both full statement sequences and single return expressions against existing function bodies.
Before:
function sendWelcomeEmail(recipient: string) {
const subject = "Welcome!";
const body = `Hello ${recipient}, thanks for signing up.`;
smtp.send(recipient, subject, body);
}
function registerUser(username: string, email: string) {
db.insert("users", { username, email });
const subject = "Welcome!";
const body = `Hello ${email}, thanks for signing up.`;
smtp.send(email, subject, body);
}After:
function sendWelcomeEmail(recipient: string) {
const subject = "Welcome!";
const body = `Hello ${recipient}, thanks for signing up.`;
smtp.send(recipient, subject, body);
}
function registerUser(username: string, email: string) {
db.insert("users", { username, email });
sendWelcomeEmail(email);
}Skipped when:
- The matching code is inside the same function it would call
- The LLM rejects the match as not semantically equivalent
- No LLM API key is configured (
MOONSHOT_API_KEY)
stdin (unified diff)
│
▼
src/cli.ts Entry point: reads stdin, calls run()
│
├── src/diff.ts parseDiff() → DiffHunk[], groupByFile()
│
└── src/main.ts run() / runInDir(): reads files, runs engine, writes back
│
├── src/config.ts loadConfig(): reads dripbird.yml + .dripbird.yml
│
├── src/llm.ts createLLMClient(): Moonshot AI integration
│
├── src/type_checker.ts TypeCheckerImpl: TypeScript type checking
│
└── src/engine.ts runRefactors(): chains refactors sequentially
│
└── src/refactors/
├── if_not_else.ts Flip negated if/else
├── function_splitter.ts Split long functions (LLM-assisted)
└── function_matcher.ts Replace duplicate code with function calls (LLM-assisted)
- Create
src/refactors/my_refactor.tsimplementing theRefactortype fromengine.ts. - The function receives
(source: string, ranges: ChangedRange[], context?: RefactorContext)and returns{ changed, source, description }(sync or async). - Check
inRange(node.loc.start.line, node.loc.end.line, ranges)to only touch changed regions. - Register it as a
NamedRefactorinsrc/main.tswith a unique name (used byenabled_refactors/disabled_refactors). - Add tests in
tests/refactors/my_refactor_test.ts— 100% branch and line coverage is enforced.
deno task fmt # format code
deno task fmt:check # check formatting
deno task lint # lint
deno task test # run tests
deno task test:coverage # run tests with 100% coverage enforcement
deno task install # install the dripbird CLI globallyPre-commit hooks (via Lefthook) run deno fmt --check, deno lint, and the 100%
coverage test suite automatically.