Skip to content
Open
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
28 changes: 28 additions & 0 deletions apps/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ All notable changes to the `@roo-code/cli` package will be documented in this fi
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.46] - 2026-01-12

### Added

- **Text User Interface (TUI)**: Major new interactive terminal UI with React/Ink for enhanced user experience ([#10480](https://github.com/RooCodeInc/Roo-Code/pull/10480))
- Interactive mode and model pickers for easy selection
- Improved task management and navigation
- CLI release script now supports local installation for testing ([#10597](https://github.com/RooCodeInc/Roo-Code/pull/10597))

### Changed

- Default model changed to `anthropic/claude-opus-4.5` ([#10544](https://github.com/RooCodeInc/Roo-Code/pull/10544))
- File organization improvements for better maintainability ([#10599](https://github.com/RooCodeInc/Roo-Code/pull/10599))
- Cleanup in ExtensionHost for better code organization ([#10600](https://github.com/RooCodeInc/Roo-Code/pull/10600))
- Updated README documentation
- Logging cleanup and improvements

### Fixed

- Model switching issues (model ID mismatch)
- ACP task cancellation handling
- Command output streaming
- Use `DEFAULT_FLAGS.model` as single source of truth for default model ID

### Tests

- Updated tests for model changes

## [0.0.45] - 2026-01-08

### Changed
Expand Down
52 changes: 51 additions & 1 deletion apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when yo
| `-y, --yes` | Non-interactive mode: auto-approve all actions | `false` |
| `-k, --api-key <key>` | API key for the LLM provider | From env var |
| `-p, --provider <provider>` | API provider (anthropic, openai, openrouter, etc.) | `openrouter` |
| `-m, --model <model>` | Model to use | `anthropic/claude-sonnet-4.5` |
| `-m, --model <model>` | Model to use | `anthropic/claude-opus-4.5` |
| `-M, --mode <mode>` | Mode to start in (code, architect, ask, debug, etc.) | `code` |
| `-r, --reasoning-effort <effort>` | Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh) | `medium` |
| `--ephemeral` | Run without persisting state (uses temporary storage) | `false` |
Expand All @@ -171,6 +171,56 @@ Tokens are valid for 90 days. The CLI will prompt you to re-authenticate when yo
| `roo auth logout` | Clear stored authentication token |
| `roo auth status` | Show current authentication status |

## ACP (Agent Client Protocol) Integration

The CLI supports the [Agent Client Protocol (ACP)](https://agentclientprotocol.com), allowing ACP-compatible editors like [Zed](https://zed.dev) to use Roo Code as their AI coding assistant.

### Running ACP Server Mode

Start the CLI in ACP server mode:

```bash
roo acp [options]
```

**ACP Options:**

| Option | Description | Default |
| --------------------------- | -------------------------------------------- | ----------------------------- |
| `-e, --extension <path>` | Path to the extension bundle directory | Auto-detected |
| `-p, --provider <provider>` | API provider (anthropic, openai, openrouter) | `openrouter` |
| `-m, --model <model>` | Model to use | `anthropic/claude-opus-4.5` |
| `-M, --mode <mode>` | Initial mode (code, architect, ask, debug) | `code` |
| `-k, --api-key <key>` | API key for the LLM provider | From env var |

### Configuring Zed

Add the following to your Zed settings (`settings.json`):

```json
{
"agent_servers": {
"Roo Code": {
"command": "roo",
"args": ["acp"]
}
}
}
```

If you need to specify options:

```json
{
"agent_servers": {
"Roo Code": {
"command": "roo",
"args": ["acp", "-e", "/path/to/extension", "-m", "anthropic/claude-sonnet-4.5"]
}
}
}
```

## Environment Variables

The CLI will look for API keys in environment variables if not provided via `--api-key`:
Expand Down
4 changes: 3 additions & 1 deletion apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@roo-code/cli",
"version": "0.0.45",
"version": "0.0.46",
"description": "Roo Code CLI - Run the Roo Code agent from the command line",
"private": true,
"type": "module",
Expand All @@ -21,6 +21,7 @@
"clean": "rimraf dist .turbo"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@inkjs/ui": "^2.0.0",
"@roo-code/core": "workspace:^",
"@roo-code/types": "workspace:^",
Expand All @@ -33,6 +34,7 @@
"p-wait-for": "^5.0.2",
"react": "^19.1.0",
"superjson": "^2.2.6",
"zod": "^4.3.5",
"zustand": "^5.0.0"
},
"devDependencies": {
Expand Down
247 changes: 247 additions & 0 deletions apps/cli/src/acp/__tests__/agent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import type * as acp from "@agentclientprotocol/sdk"

import { RooCodeAgent } from "../agent.js"
import type { AcpSessionOptions } from "../session.js"

vi.mock("@/commands/auth/index.js", () => ({
login: vi.fn().mockResolvedValue({ success: true }),
logout: vi.fn().mockResolvedValue({ success: true }),
status: vi.fn().mockResolvedValue({ authenticated: false }),
}))

vi.mock("../session.js", () => ({
AcpSession: {
create: vi.fn().mockResolvedValue({
prompt: vi.fn().mockResolvedValue({ stopReason: "end_turn" }),
cancel: vi.fn(),
setMode: vi.fn(),
dispose: vi.fn().mockResolvedValue(undefined),
getSessionId: vi.fn().mockReturnValue("test-session-id"),
}),
},
}))

describe("RooCodeAgent", () => {
let agent: RooCodeAgent
let mockConnection: acp.AgentSideConnection

const defaultOptions: AcpSessionOptions = {
extensionPath: "/test/extension",
provider: "openrouter",
apiKey: "test-key",
model: "test-model",
mode: "code",
}

beforeEach(() => {
mockConnection = {
sessionUpdate: vi.fn().mockResolvedValue(undefined),
requestPermission: vi.fn().mockResolvedValue({
outcome: { outcome: "selected", optionId: "allow" },
}),
readTextFile: vi.fn().mockResolvedValue({ content: "test content" }),
writeTextFile: vi.fn().mockResolvedValue({}),
createTerminal: vi.fn(),
extMethod: vi.fn(),
extNotification: vi.fn(),
signal: new AbortController().signal,
closed: Promise.resolve(),
} as unknown as acp.AgentSideConnection

agent = new RooCodeAgent(defaultOptions, mockConnection)
})

afterEach(() => {
vi.clearAllMocks()
})

describe("initialize", () => {
it("should return protocol version and capabilities", async () => {
const result = await agent.initialize({
protocolVersion: 1,
})

expect(result.protocolVersion).toBeDefined()
expect(result.agentCapabilities).toBeDefined()
expect(result.agentCapabilities?.loadSession).toBe(false)
expect(result.agentCapabilities?.promptCapabilities?.image).toBe(true)
})

it("should return auth methods", async () => {
const result = await agent.initialize({
protocolVersion: 1,
})

expect(result.authMethods).toBeDefined()
expect(result.authMethods).toHaveLength(1)

const methods = result.authMethods!
expect(methods[0]!.id).toBe("roo")
})

it("should store client capabilities", async () => {
const clientCapabilities: acp.ClientCapabilities = {
fs: {
readTextFile: true,
writeTextFile: true,
},
}

await agent.initialize({
protocolVersion: 1,
clientCapabilities,
})

// Capabilities should be stored for use in newSession
// This is tested indirectly through the session creation
})
})

describe("authenticate", () => {
it("should throw for invalid auth method", async () => {
await expect(
agent.authenticate({
methodId: "invalid-method",
}),
).rejects.toThrow()
})
})

describe("newSession", () => {
it("should create a new session", async () => {
const result = await agent.newSession({
cwd: "/test/workspace",
mcpServers: [],
})

expect(result.sessionId).toBeDefined()
expect(typeof result.sessionId).toBe("string")
})

it("should throw auth error when not authenticated and no API key", async () => {
// Create agent without API key
const agentWithoutKey = new RooCodeAgent({ ...defaultOptions, apiKey: undefined }, mockConnection)

// Mock environment to not have API key
const originalEnv = process.env.OPENROUTER_API_KEY
delete process.env.OPENROUTER_API_KEY

try {
await expect(
agentWithoutKey.newSession({
cwd: "/test/workspace",
mcpServers: [],
}),
).rejects.toThrow()
} finally {
if (originalEnv) {
process.env.OPENROUTER_API_KEY = originalEnv
}
}
})
})

describe("prompt", () => {
it("should forward prompt to session", async () => {
// Setup
const { sessionId } = await agent.newSession({
cwd: "/test/workspace",
mcpServers: [],
})

// Execute
const result = await agent.prompt({
sessionId,
prompt: [{ type: "text", text: "Hello, world!" }],
})

// Verify
expect(result.stopReason).toBe("end_turn")
})

it("should throw for invalid session ID", async () => {
await expect(
agent.prompt({
sessionId: "invalid-session",
prompt: [{ type: "text", text: "Hello" }],
}),
).rejects.toThrow("Session not found")
})
})

describe("cancel", () => {
it("should cancel session prompt", async () => {
// Setup
const { sessionId } = await agent.newSession({
cwd: "/test/workspace",
mcpServers: [],
})

// Execute - should not throw
await agent.cancel({ sessionId })
})

it("should handle cancel for non-existent session gracefully", async () => {
// Should not throw for invalid session
await agent.cancel({ sessionId: "non-existent" })
})
})

describe("setSessionMode", () => {
it("should set session mode", async () => {
// Setup
const { sessionId } = await agent.newSession({
cwd: "/test/workspace",
mcpServers: [],
})

// Execute
const result = await agent.setSessionMode({
sessionId,
modeId: "architect",
})

// Verify
expect(result).toEqual({})
})

it("should throw for invalid mode", async () => {
// Setup
const { sessionId } = await agent.newSession({
cwd: "/test/workspace",
mcpServers: [],
})

// Execute
await expect(
agent.setSessionMode({
sessionId,
modeId: "invalid-mode",
}),
).rejects.toThrow("Unknown mode")
})

it("should throw for invalid session", async () => {
await expect(
agent.setSessionMode({
sessionId: "invalid-session",
modeId: "code",
}),
).rejects.toThrow("Session not found")
})
})

describe("dispose", () => {
it("should dispose all sessions", async () => {
// Setup
await agent.newSession({ cwd: "/test/workspace1", mcpServers: [] })
await agent.newSession({ cwd: "/test/workspace2", mcpServers: [] })

// Execute
await agent.dispose()

// Verify - creating new session should work (sessions map is cleared)
// The next newSession would create a fresh session
})
})
})
Loading