Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions .claude/commands/ship.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,12 @@ The subject must be conventional (`.githooks/commit-msg` rejects anything
else): `<type>(<scope>): <imperative summary>`, lowercase, no trailing period.
Scopes in use: `tools`, `runtime`, `provider`, `tests`, `ci`, `replay`, `docs`.

The body says why, not what — the diff already says what. End it with:
The body says why, not what — the diff already says what.

```
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
```
**No attribution trailers.** No `Co-Authored-By`, no tool or model name, no
"generated with" line — not in commits, PR bodies, issues or review comments.
The commit history is the maintainer's, and a trailer naming a tool says nothing
about why the change exists.

Write the message to a temp file and use `git commit -F <file>`, so backticks
and newlines survive.
Expand Down Expand Up @@ -140,11 +141,9 @@ Either way:
- **Verified means verified.** List the checks that actually ran and what they
actually printed. If something was skipped or could not be tested, say that
plainly rather than writing a sentence that implies a green run.
- End with:

```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
```
- End on the verification. No attribution footer, no tool or model name, no
"generated with" line — see the rule under **Commit**, which applies to PR
bodies, issues and review comments too.

## 7. Report

Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ jobs:
# The approval classifier and the config loader are built on path
# semantics, and macOS differs from Linux on case sensitivity and on
# where a temporary directory lives. Both are cheap to run.
#
# windows-latest is deliberately absent, and its absence is the honest
# signal rather than an oversight: `runCommand` spawns `sh -c`, which
# Windows does not have, so `run_terminal` and `run_tests` cannot run
# there at all. The classifier compounds it — it knows `rm -rf`, `sudo`
# and `chmod`, not `del /f /s /q` or `reg delete`, so every Windows
# command falls through to the fail-closed default and asks. Adding the
# runner would produce a permanently red job that documents nothing the
# docs do not now say outright. The platform is a port, not a matrix
# entry, and the docs claim macOS and Linux only.
os: [ubuntu-latest, macos-latest]

steps:
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,27 @@
[![TypeScript](https://img.shields.io/badge/language-TypeScript-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
[![License: MIT](https://img.shields.io/badge/license-MIT-22c55e.svg)](LICENSE)

[Documentation](https://woop-code.vercel.app) · [Install](https://woop-code.vercel.app/docs/getting-started/install) · [How a turn works](https://woop-code.vercel.app/docs/introduction/how-a-turn-works)

**A terminal-native coding agent that understands your repository, shows its work, and keeps you in control of code changes.**

Woopcode runs where you work: in the terminal and inside the current repository. Ask it to investigate, explain, implement, review, or test a change; it streams progress, uses focused tools, and presents edits as a readable diff before it writes to an existing file.

> **Status:** an early-stage project. Google Gemini, OpenAI, and Anthropic are all implemented and usable. Gemini is the most heavily exercised path — it is what the benchmark suite runs against — so treat it as the best-tested option rather than the only one.
Google Gemini, OpenAI, and Anthropic are all implemented. Gemini is the most heavily exercised path — it is what the benchmark suite runs against — so treat it as the best-tested option rather than the only one. Runs on macOS and Linux; on Windows, use WSL.

## Demo

![Woopcode demo](public/terminal1.gif)

## Why Woopcode

| | |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Repository-aware** | Starts with your package metadata, README, and top-level project structure, then discovers deeper context only when needed. Repository context is budgeted per request rather than dumped, since the agent can read any file on demand. |
| **Terminal-first** | A focused React Ink interface with a pinned header, scrollable conversation, keyboard navigation, and no browser tab required. |
| **Visible execution** | Streams assistant output and tool activity so you can follow the work instead of waiting behind an opaque progress screen. |
| **Review before overwrite** | Existing-file edits and overwrites pause on a unified diff for approval. |
| **Practical guardrails** | Detects duplicate tool calls, limits tool iterations, supports cancellation, and returns recoverable tool errors to the agent. |
| | |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Nothing is overwritten unreviewed** | Every edit to an existing file stops on a unified diff and waits. A tool that writes raises an approval request rather than touching the disk itself, so there is no path around the review. |
| **Shell commands fail closed** | Commands are classified by risk before they run, and a command the classifier does not recognise is treated as destructive. A line is judged by its riskiest part, so `git status && rm -rf build` asks — and so does anything hidden inside `$(...)`. |
| **Plan mode is enforced twice** | The provider is not offered the writing tools, *and* the loop refuses a write that arrives anyway. Both are load-bearing: `run_terminal` has to stay available for inspection, so `sed -i` and `cat > file` reach the disk through a tool the first gate must keep. |
| **Provider reasoning is replayed correctly** | Anthropic and OpenAI both require the reasoning that preceded a tool call to be sent back with that call's result, and both fail silently without it — the request succeeds and the model simply reasons from less. Each client handles its own rules; the agent loop stays neutral. |
| **Tested against a real filesystem** | Tools are exercised on real files in temporary directories rather than behind mocks. Only the provider and the approval prompt are faked, because one would make network calls and the other needs a human. |

## Quick start

Expand Down Expand Up @@ -84,7 +86,6 @@ Prompt → repository context → streaming agent → focused tools → review d
The conversation, provider configuration, and local state are stored in:

- macOS and Linux: `~/.config/woopcode/`
- Windows: `%LOCALAPPDATA%\\woopcode\\`

### Session history

Expand Down
11 changes: 11 additions & 0 deletions config/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ import { existsSync, mkdirSync } from "fs";
* Location:
* - macOS/Linux: ~/.config/woopcode/
* - Windows: %LOCALAPPDATA%\woopcode\
*
* The Windows branch is correct and unreachable in practice, and the
* documentation no longer advertises the platform. `runCommand` in
* tools/command.ts spawns `sh -c`, which Windows does not have, so
* `run_terminal` and `run_tests` cannot run there — and the approval classifier
* knows only POSIX command names, so every Windows command falls to its
* fail-closed default and asks. Supporting the platform is a port, not a fix.
*
* Kept rather than deleted because it costs nothing, it is what WSL and any
* later port would want, and removing it would make the gap harder to find
* than this comment does.
*/
export function getConfigDir(): string {
const home = homedir();
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/connect-a-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ mode in one line.
## Where the key goes

Into `providers.json` in your config directory — `~/.config/woopcode/` on macOS
and Linux, `%LOCALAPPDATA%\woopcode\` on Windows.
and Linux.

:::warning
The key is stored in plain text. There is no encryption and no keychain
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ woopcode --version
| | |
| --- | --- |
| Runtime | Bun 1.0+ |
| Platforms | macOS, Linux, Windows |
| Platforms | macOS, Linux — on Windows, use WSL |
| Network | Outbound HTTPS to the provider |
| Account | A Google Gemini API key — [free to create](https://aistudio.google.com/apikey) |

Expand Down
1 change: 0 additions & 1 deletion docs/guides/sessions-and-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ context. If something matters, restate it.
| Platform | Path |
| --- | --- |
| macOS, Linux | `~/.config/woopcode/conversation.json` |
| Windows | `%LOCALAPPDATA%\woopcode\conversation.json` |

:::warning
There is one history file, not one per repository. Starting Woopcode in a
Expand Down
2 changes: 0 additions & 2 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ since: 0.6.0
| Platform | Directory |
| --- | --- |
| macOS, Linux | `$XDG_CONFIG_HOME/woopcode/`, or `~/.config/woopcode/` |
| Windows | `%LOCALAPPDATA%\woopcode\` |

The directory is created on first run.

Expand Down Expand Up @@ -99,7 +98,6 @@ something you may want to remove yourself.
| Variable | Effect |
| --- | --- |
| `XDG_CONFIG_HOME` | Moves the config directory on macOS and Linux |
| `LOCALAPPDATA` | Moves the config directory on Windows |

### Credentials

Expand Down
7 changes: 6 additions & 1 deletion onboarding/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,14 @@ export async function ensureProviderConfigured(): Promise<ProviderCredentials> {
// never arrive, so an unconfigured non-interactive run must fail with a
// message that names the fix rather than hanging until the caller's timeout.
if (!canPromptInteractively()) {
// Every supported vendor variable is named, not just Gemini's. This
// message is the only instruction a headless caller gets, and pointing a
// user who holds an OpenAI key at GEMINI_API_KEY sends them to buy a
// credential they already have the equivalent of.
throw new Error(
"No provider is configured and there is no terminal to run setup in.\n" +
"Set GEMINI_API_KEY (or WOOPCODE_API_KEY with WOOPCODE_PROVIDER) in the environment.",
"Set one of GEMINI_API_KEY, OPENAI_API_KEY or ANTHROPIC_API_KEY in the\n" +
"environment, or WOOPCODE_API_KEY together with WOOPCODE_PROVIDER.",
);
}

Expand Down
81 changes: 46 additions & 35 deletions onboarding/test-reset.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,66 @@
#!/usr/bin/env bun
/**
* Utility script to reset provider configuration for testing onboarding.
*
* Usage:
* bun onboarding/test-reset.ts # Clear all API keys
* bun onboarding/test-reset.ts restore # Restore from backup
* Puts the machine back into the state a new user starts from, so the setup
* wizard can be walked without uninstalling anything.
*
* bun onboarding/test-reset.ts # move the config directory aside
* bun onboarding/test-reset.ts restore # put it back
*
* The whole directory moves, rather than the keys being blanked in place.
* `ensureProviderConfigured` treats a keyless provider and a missing config the
* same way, but the rest of a first run does not: conversation.json and
* execution-log.json are what separate "no key yet" from "never run before",
* and blanking a key leaves both behind.
*
* This previously pointed at ./config/providers.json — a path inside the
* repository, where no configuration has ever lived. It raised ENOENT on every
* invocation, which is why it never reset anything. `getConfigDir` is imported
* rather than rebuilt so the two cannot drift apart again.
*/

import { existsSync } from "fs";
import { existsSync, renameSync, rmSync } from "node:fs";
import { dirname, join } from "node:path";
import { getConfigDir } from "../config/paths";

const CONFIG_PATH = "./config/providers.json";
const BACKUP_PATH = "./config/providers.json.backup";
// getConfigDir creates the directory as a side effect, which is harmless here:
// an empty directory moved aside is the same cold start as a missing one.
const CONFIG_DIR = getConfigDir();
const BACKUP_DIR = join(dirname(CONFIG_DIR), "woopcode.pre-onboarding-test");

const command = process.argv[2];

async function clearKeys() {
// Backup current config
if (existsSync(CONFIG_PATH)) {
const current = await Bun.file(CONFIG_PATH).text();
await Bun.write(BACKUP_PATH, current);
console.log("✓ Backed up current configuration");
function reset() {
if (existsSync(BACKUP_DIR)) {
console.error(`✖ A backup is already sitting at ${BACKUP_DIR}`);
console.error(" Restore it first, or delete it if you no longer want it.");
process.exit(1);
}

// Clear all API keys
const config = JSON.parse(await Bun.file(CONFIG_PATH).text());

for (const provider in config.providers) {
config.providers[provider].apiKey = "";
}
renameSync(CONFIG_DIR, BACKUP_DIR);

await Bun.write(CONFIG_PATH, JSON.stringify(config, null, 2));
console.log("✓ Cleared all API keys");
console.log("\nYou can now test the onboarding flow:");
console.log(`✓ Moved ${CONFIG_DIR}`);
console.log(` to ${BACKUP_DIR}`);
console.log("\nWalk the setup wizard:");
console.log(" bun cli.ts");
console.log("\nTo restore your keys:");
console.log("\nThen put your real configuration back:");
console.log(" bun onboarding/test-reset.ts restore");
}

async function restore() {
if (!existsSync(BACKUP_PATH)) {
console.error("✖ No backup found");
function restore() {
if (!existsSync(BACKUP_DIR)) {
console.error(`✖ No backup at ${BACKUP_DIR}`);
process.exit(1);
}

const backup = await Bun.file(BACKUP_PATH).text();
await Bun.write(CONFIG_PATH, backup);
console.log("✓ Restored configuration from backup");
// Whatever the wizard just wrote is test data, and the backup is the real
// configuration. Removing it first keeps rename from failing on a directory
// that already exists.
rmSync(CONFIG_DIR, { recursive: true, force: true });
renameSync(BACKUP_DIR, CONFIG_DIR);

console.log(`✓ Restored ${CONFIG_DIR}`);
}

if (command === "restore") {
await restore();
if (process.argv[2] === "restore") {
restore();
} else {
await clearKeys();
reset();
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "woopcode",
"version": "0.8.2",
"description": "An autonomous AI coding assistant built for the terminal",
"version": "0.9.0",
"description": "A terminal coding agent that shows every edit as a diff before it writes, and asks before running a shell command it cannot vouch for",
"license": "MIT",
"author": {
"name": "Manas Raghuwanshi",
Expand All @@ -11,7 +11,7 @@
"type": "git",
"url": "git+https://github.com/mangit955/woop-code.git"
},
"homepage": "https://github.com/mangit955/woop-code#readme",
"homepage": "https://woop-code.vercel.app",
"bugs": {
"url": "https://github.com/mangit955/woop-code/issues"
},
Expand Down
Loading