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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,35 @@ codebase project build --wait "build a launch waitlist page"

OAuth uses Codebase Auto by default (`codebase/d4f`, in-house DeepSeek V4 Flash). Swap models live with `/model`. Set reasoning depth with `/effort`. `project build` hands a prompt to the web builder and prints the session, status, event stream, and preview URL when you pass `--wait`.

## Use Codebase from an ACP client

Codebase speaks the [Agent Client Protocol](https://agentclientprotocol.com/) over stdio:

```sh
codebase acp
```

For a Buzz custom harness, use:

```text
Name: Codebase
Command: codebase
Arguments: acp
```

Buzz can then discover the models available to your Codebase account. Each ACP
session gets its own working directory and conversation, accepts client-supplied
MCP servers, streams assistant and tool progress, supports cancellation, and
routes write/shell approvals through the client's permission UI.

## What makes it good

- **🏁 Tournaments.** `/tournament <task>` races several agents on the same change in isolated worktrees, a judge ranks them, you merge the winner. `--models opus,sonnet,haiku` pits models head-to-head on *your* code.
- **Receipts.** `codebase auto --reliable` turns a one-shot task into an audited run: task lifecycle, per-task evidence, verification, tool calls, usage, and checkpoints are saved locally and inspectable with `codebase receipt`.
- **↺ Rewind anything.** `/rewind` rolls the conversation *and* the files back to before any earlier prompt — a bad turn fully un-happens. Every edit is checkpointed.
- **🧠 Remembers across sessions.** Pulls durable facts (your prefs, project decisions, the rules you set) out of a session, then recalls matching notes with file/source/session/last-used/staleness labels. `#note` to add one by hand; `/memory list|show|forget` to inspect or clean them up.
- **🔌 MCP.** Connect external tool servers (filesystem, Postgres, git, fetch, …) over stdio or remote HTTP, OAuth and all. Their tools splice straight into the agent.
- **ACP.** Run `codebase acp` to use Codebase from Buzz and other ACP clients, with model discovery, streamed tool activity, cancellation, permissions, and client-supplied MCP servers.
- **🤖 Subagents.** Fan out read-only researchers or write-capable workers that keep their tool-noise out of your main context — each can run in its own git worktree, on its own model and reasoning level.
- **🪝 Hooks.** Shell commands on lifecycle events (pre/post tool, edit, prompt, session start/end) — run a formatter on save, block secrets, commit on exit.
- **🌐 SSH.** Run commands on enrolled remote hosts by name, behind the same safety validator as the local shell.
Expand Down
14 changes: 12 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codebase-cli",
"version": "2.0.0-pre.80",
"version": "2.0.0-pre.81",
"description": "Codebase CLI — a TypeScript coding agent on the pi-mono runtime. OAuth-aware, any LLM provider, single install.",
"keywords": [
"ai",
Expand Down Expand Up @@ -63,6 +63,7 @@
"prepack": "npm run build"
},
"dependencies": {
"@agentclientprotocol/sdk": "^1.3.0",
"@earendil-works/pi-agent-core": "0.74.0",
"@earendil-works/pi-ai": "0.74.0",
"@earendil-works/pi-tui": "^0.74.0",
Expand Down
138 changes: 138 additions & 0 deletions src/acp/server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { PassThrough, Readable, Writable } from "node:stream";
import { fileURLToPath } from "node:url";
import * as acp from "@agentclientprotocol/sdk";
import type { Model } from "@earendil-works/pi-ai";
import { fauxAssistantMessage, fauxToolCall, registerFauxProvider } from "@earendil-works/pi-ai";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { runAcpServer } from "./server.js";

const MOCK_MCP = fileURLToPath(new URL("../mcp/__test__/mock-server.mjs", import.meta.url));

describe("runAcpServer", () => {
let faux: ReturnType<typeof registerFauxProvider>;
let model: Model<string>;
let home: string;
let cwd: string;
let previousHome: string | undefined;

beforeEach(() => {
previousHome = process.env.HOME;
home = mkdtempSync(join(tmpdir(), "acp-home-"));
cwd = mkdtempSync(join(tmpdir(), "acp-cwd-"));
process.env.HOME = home;
process.env.CODEBASE_NO_AUTO_MEMORY = "1";
faux = registerFauxProvider({
models: [
{
id: "test-model",
name: "Test Model",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 100_000,
maxTokens: 4096,
},
],
tokenSize: { min: 1, max: 2 },
});
model = faux.models[0] as Model<string>;
});

afterEach(() => {
faux.unregister();
rmSync(home, { recursive: true, force: true });
rmSync(cwd, { recursive: true, force: true });
if (previousHome === undefined) delete process.env.HOME;
else process.env.HOME = previousHome;
delete process.env.CODEBASE_NO_AUTO_MEMORY;
});

it("runs a Buzz-compatible ACP turn with injected MCP tools and streamed updates", async () => {
faux.setResponses([
fauxAssistantMessage([fauxToolCall("mcp__buzz__echo", { text: "from Buzz" })], {
stopReason: "toolUse",
}),
fauxAssistantMessage("posted through Buzz"),
]);

const toAgent = new PassThrough();
const fromAgent = new PassThrough();
const serverDone = runAcpServer({
stdin: toAgent,
stdout: fromAgent,
configOverride: { model, apiKey: "faux-key", source: "byok" },
});
const updates: acp.SessionNotification[] = [];
const permissionRequests: acp.RequestPermissionRequest[] = [];
const stream = acp.ndJsonStream(Writable.toWeb(toAgent), Readable.toWeb(fromAgent) as ReadableStream<Uint8Array>);

const result = await acp
.client({ name: "buzz-test" })
.onNotification(acp.methods.client.session.update, (ctx) => {
updates.push(ctx.params);
})
.onRequest(acp.methods.client.session.requestPermission, (ctx) => {
permissionRequests.push(ctx.params);
const allow = ctx.params.options.find((option) => option.kind === "allow_once");
if (!allow) return { outcome: { outcome: "cancelled" as const } };
return { outcome: { outcome: "selected" as const, optionId: allow.optionId } };
})
.connectWith(stream, async (client) => {
const initialized = await client.request(acp.methods.agent.initialize, {
protocolVersion: acp.PROTOCOL_VERSION,
clientInfo: { name: "buzz-acp", version: "test" },
});
expect(initialized.protocolVersion).toBe(acp.PROTOCOL_VERSION);
expect(initialized.agentInfo?.name).toBe("Codebase");

const session = await client.request(acp.methods.agent.session.new, {
cwd,
mcpServers: [
{
name: "buzz",
command: process.execPath,
args: [MOCK_MCP],
env: [],
},
],
});
expect(session.configOptions?.[0]).toMatchObject({
id: "model",
category: "model",
currentValue: "test-model",
});
await client.request(acp.methods.agent.session.setConfigOption, {
sessionId: session.sessionId,
configId: "model",
value: "test-model",
});
return client.request(acp.methods.agent.session.prompt, {
sessionId: session.sessionId,
prompt: [{ type: "text", text: "Reply through the Buzz tool." }],
});
});

expect(result.stopReason).toBe("end_turn");
expect(permissionRequests).toHaveLength(1);
expect(permissionRequests[0]?.toolCall.name).toBe("mcp__buzz__echo");
expect(updates.some((event) => event.update.sessionUpdate === "tool_call")).toBe(true);
expect(updates.some((event) => event.update.sessionUpdate === "tool_call_update")).toBe(true);
const text = updates
.filter(
(
event,
): event is acp.SessionNotification & {
update: { sessionUpdate: "agent_message_chunk"; content: { type: "text"; text: string } };
} => event.update.sessionUpdate === "agent_message_chunk" && event.update.content.type === "text",
)
.map((event) => event.update.content.text)
.join("");
expect(text).toContain("posted through Buzz");

toAgent.end();
await serverDone;
});
});
Loading
Loading