diff --git a/.dockerignore b/.dockerignore
index 6359978833..4579d61580 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -76,14 +76,18 @@ src/node_modules
!pnpm-workspace.yaml
!scripts/bootstrap.mjs
!apps/web-evals/
+!apps/cli/
!src/
!webview-ui/
!packages/evals/.docker/entrypoints/runner.sh
!packages/build/
!packages/config-eslint/
!packages/config-typescript/
+!packages/core/
!packages/evals/
!packages/ipc/
!packages/telemetry/
!packages/types/
+!packages/vscode-shim/
+!packages/cloud/
!locales/
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 16e0c17e7a..443842c856 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -84,7 +84,6 @@ body:
- Google Gemini
- Google Vertex AI
- Groq
- - Human Relay Provider
- LiteLLM
- LM Studio
- Mistral AI
diff --git a/.roo/commands/release.md b/.roo/commands/release.md
index 707844cdc4..2e09783a58 100644
--- a/.roo/commands/release.md
+++ b/.roo/commands/release.md
@@ -1,6 +1,7 @@
---
description: "Create a new release of the Roo Code extension"
argument-hint: patch | minor | major
+mode: code
---
1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt`
diff --git a/.roo/rules-translate/instructions-zh-cn.md b/.roo/rules-translate/instructions-zh-cn.md
index 6141038728..b166a1e6a8 100644
--- a/.roo/rules-translate/instructions-zh-cn.md
+++ b/.roo/rules-translate/instructions-zh-cn.md
@@ -16,7 +16,6 @@
| Auto-approve | 自动批准 | 始终批准 | 权限相关术语 |
| Checkpoint | 存档点 | 检查点/快照 | 技术概念统一 |
| MCP Server | MCP 服务 | MCP 服务器 | 技术组件 |
-| Human Relay | 人工辅助模式 | 人工中继 | 功能描述清晰 |
| Network Timeout | 请求超时 | 网络超时 | 更准确描述 |
| Terminal | 终端 | 命令行 | 技术术语统一 |
| diff | 差异更新 | 差分/补丁 | 代码变更 |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85019b81f4..bf223708a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,81 @@
# Roo Code Changelog
+## [3.38.3] - 2026-01-03
+
+- Feat: Add option in Context settings to recursively load `.roo/rules` and `AGENTS.md` from subdirectories (PR #10446 by @mrubens)
+- Fix: Stop frequent Claude Code sign-ins by hardening OAuth refresh token handling (PR #10410 by @hannesrudolph)
+- Fix: Add `maxConcurrentFileReads` limit to native `read_file` tool schema (PR #10449 by @app/roomote)
+- Fix: Add type check for `lastMessage.text` in TTS useEffect to prevent runtime errors (PR #10431 by @app/roomote)
+
+## [3.38.2] - 2025-12-31
+
+
+
+- Align skills system with Agent Skills specification (PR #10409 by @hannesrudolph)
+- Prevent write_to_file from creating files at truncated paths (PR #10415 by @mrubens and @daniel-lxs)
+- Update Cerebras maxTokens to 16384 (PR #10387 by @sebastiand-cerebras)
+- Fix rate limit wait display (PR #10389 by @hannesrudolph)
+- Remove human-relay provider (PR #10388 by @hannesrudolph)
+- Replace Todo Lists video with Context Management video in documentation (PR #10375 by @SannidhyaSah)
+
+## [3.38.1] - 2025-12-29
+
+
+
+- Fix: Flush pending tool results before condensing context (PR #10379 by @daniel-lxs)
+- Fix: Revert mergeToolResultText for OpenAI-compatible providers (PR #10381 by @hannesrudolph)
+- Fix: Enforce maxConcurrentFileReads limit in read_file tool (PR #10363 by @roomote)
+- Fix: Improve feedback message when read_file is used on a directory (PR #10371 by @roomote)
+- Fix: Handle custom tool use similarly to MCP tools for IPC schema purposes (PR #10364 by @jr)
+- Fix: Correct GitHub repository URL in marketing page (#10376 by @jishnuteegala, PR #10377 by @roomote)
+- Docs: Clarify path to Security Settings in privacy policy (PR #10367 by @roomote)
+
+## [3.38.0] - 2025-12-27
+
+
+
+- Add support for [Agent Skills](https://agentskills.io/), enabling reusable packages of prompts, tools, and resources to extend Roo's capabilities (PR #10335 by @mrubens)
+- Add optional mode field to slash command front matter, allowing commands to automatically switch to a specific mode when triggered (PR #10344 by @app/roomote)
+- Add support for npm packages and .env files to custom tools, allowing custom tools to import dependencies and access environment variables (PR #10336 by @cte)
+- Remove simpleReadFileTool feature, streamlining the file reading experience (PR #10254 by @app/roomote)
+- Remove OpenRouter Transforms feature (PR #10341 by @app/roomote)
+- Fix mergeToolResultText handling in Roo provider (PR #10359 by @mrubens)
+
+## [3.37.1] - 2025-12-23
+
+
+
+- Fix: Send native tool definitions by default for OpenAI to ensure proper tool usage (PR #10314 by @hannesrudolph)
+- Fix: Preserve reasoning_details shape to prevent malformed responses when processing model output (PR #10313 by @hannesrudolph)
+- Fix: Drain queued messages while waiting for ask to prevent message loss (PR #10315 by @hannesrudolph)
+- Feat: Add grace retry for empty assistant messages to improve reliability (PR #10297 by @hannesrudolph)
+- Feat: Enable mergeToolResultText for all OpenAI-compatible providers for better tool result handling (PR #10299 by @hannesrudolph)
+- Feat: Enable mergeToolResultText for Roo Code Cloud provider (PR #10301 by @hannesrudolph)
+- Feat: Strengthen native tool-use guidance in prompts for improved model behavior (PR #10311 by @hannesrudolph)
+- UX: Account-centric signup flow for improved onboarding experience (PR #10306 by @brunobergher)
+
+## [3.37.0] - 2025-12-22
+
+
+
+- Add MiniMax M2.1 and improve environment_details handling for Minimax thinking models (PR #10284 by @hannesrudolph)
+- Add GLM-4.7 model with thinking mode support for Zai provider (PR #10282 by @hannesrudolph)
+- Add experimental custom tool calling - define custom tools that integrate seamlessly with your AI workflow (PR #10083 by @cte)
+- Deprecate XML tool protocol selection and force native tool format for new tasks (PR #10281 by @daniel-lxs)
+- Fix: Emit tool_call_end events in OpenAI handler when streaming ends (#10275 by @torxeon, PR #10280 by @daniel-lxs)
+- Fix: Emit tool_call_end events in BaseOpenAiCompatibleProvider (PR #10293 by @hannesrudolph)
+- Fix: Disable strict mode for MCP tools to preserve optional parameters (PR #10220 by @daniel-lxs)
+- Fix: Move array-specific properties into anyOf variant in normalizeToolSchema (PR #10276 by @daniel-lxs)
+- Fix: Add CRLF line ending normalization to search_replace and search_and_replace tools (PR #10288 by @hannesrudolph)
+- Fix: Add graceful fallback for model parsing in Chutes provider (PR #10279 by @hannesrudolph)
+- Fix: Enable Requesty refresh models with credentials (PR #10273 by @daniel-lxs)
+- Fix: Improve reasoning_details accumulation and serialization (PR #10285 by @hannesrudolph)
+- Fix: Preserve reasoning_content in condense summary for DeepSeek-reasoner (PR #10292 by @hannesrudolph)
+- Refactor Zai provider to merge environment_details into tool result instead of system message (PR #10289 by @hannesrudolph)
+- Remove parallel_tool_calls parameter from litellm provider (PR #10274 by @roomote)
+- Add Cloud Team page with comprehensive team management features (PR #10267 by @roomote)
+- Add message log deduper utility for evals (PR #10286 by @hannesrudolph)
+
## [3.36.16] - 2025-12-19
- Fix: Normalize tool schemas for VS Code LM API to resolve error 400 when using VS Code Language Model API providers (PR #10221 by @hannesrudolph)
diff --git a/README.md b/README.md
index 9e98cd8845..75f37762f9 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ Learn more: [Using Modes](https://docs.roocode.com/basic-usage/using-modes) •
| | | |
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
| 
Installing Roo Code | 
Configuring Profiles | 
Codebase Indexing |
-| 
Custom Modes | 
Checkpoints | 
Todo Lists |
+| 
Custom Modes | 
Checkpoints | 
Context Management |
diff --git a/apps/cli/README.md b/apps/cli/README.md
new file mode 100644
index 0000000000..3e78192d4e
--- /dev/null
+++ b/apps/cli/README.md
@@ -0,0 +1,231 @@
+# @roo-code/cli
+
+Command Line Interface for Roo Code - Run the Roo Code agent from the terminal without VSCode.
+
+## Overview
+
+This CLI uses the `@roo-code/vscode-shim` package to provide a VSCode API compatibility layer, allowing the main Roo Code extension to run in a Node.js environment.
+
+## Installation
+
+### Quick Install (Recommended)
+
+Install the Roo Code CLI with a single command:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
+```
+
+**Requirements:**
+
+- Node.js 20 or higher
+- macOS (Intel or Apple Silicon) or Linux (x64 or ARM64)
+
+**Custom installation directory:**
+
+```bash
+ROO_INSTALL_DIR=/opt/roo-code ROO_BIN_DIR=/usr/local/bin curl -fsSL ... | sh
+```
+
+**Install a specific version:**
+
+```bash
+ROO_VERSION=0.1.0 curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
+```
+
+### Updating
+
+Re-run the install script to update to the latest version:
+
+```bash
+curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
+```
+
+### Uninstalling
+
+```bash
+rm -rf ~/.roo/cli ~/.local/bin/roo
+```
+
+### Development Installation
+
+For contributing or development:
+
+```bash
+# From the monorepo root.
+pnpm install
+
+# Build the main extension first.
+pnpm --filter roo-cline bundle
+
+# Build the cli.
+pnpm --filter @roo-code/cli build
+```
+
+## Usage
+
+### Interactive Mode (Default)
+
+By default, the CLI prompts for approval before executing actions:
+
+```bash
+export OPENROUTER_API_KEY=sk-or-v1-...
+
+roo "What is this project?" --workspace ~/Documents/my-project
+```
+
+In interactive mode:
+
+- Tool executions prompt for yes/no approval
+- Commands prompt for yes/no approval
+- Followup questions show suggestions and wait for user input
+- Browser and MCP actions prompt for approval
+
+### Non-Interactive Mode (`-y`)
+
+For automation and scripts, use `-y` to auto-approve all actions:
+
+```bash
+roo -y "Refactor the utils.ts file" --workspace ~/Documents/my-project
+```
+
+In non-interactive mode:
+
+- Tool, command, browser, and MCP actions are auto-approved
+- Followup questions show a 10-second timeout, then auto-select the first suggestion
+- Typing any key cancels the timeout and allows manual input
+
+## Options
+
+| Option | Description | Default |
+| --------------------------------- | ------------------------------------------------------------------------------ | ----------------- |
+| `-w, --workspace ` | Workspace path to operate in | Current directory |
+| `-e, --extension ` | Path to the extension bundle directory | Auto-detected |
+| `-v, --verbose` | Enable verbose output (show VSCode and extension logs) | `false` |
+| `-d, --debug` | Enable debug output (includes detailed debug information, prompts, paths, etc) | `false` |
+| `-x, --exit-on-complete` | Exit the process when task completes (useful for testing) | `false` |
+| `-y, --yes` | Non-interactive mode: auto-approve all actions | `false` |
+| `-k, --api-key ` | API key for the LLM provider | From env var |
+| `-p, --provider ` | API provider (anthropic, openai, openrouter, etc.) | `openrouter` |
+| `-m, --model ` | Model to use | Provider default |
+| `-M, --mode ` | Mode to start in (code, architect, ask, debug, etc.) | `code` |
+| `-r, --reasoning-effort ` | Reasoning effort level (none, minimal, low, medium, high, xhigh) | `medium` |
+
+By default, the CLI runs in quiet mode (suppressing VSCode/extension logs) and only shows assistant output. Use `-v` to see all logs, or `-d` for detailed debug information.
+
+## Environment Variables
+
+The CLI will look for API keys in environment variables if not provided via `--api-key`:
+
+| Provider | Environment Variable |
+| ------------- | -------------------- |
+| anthropic | `ANTHROPIC_API_KEY` |
+| openai | `OPENAI_API_KEY` |
+| openrouter | `OPENROUTER_API_KEY` |
+| google/gemini | `GOOGLE_API_KEY` |
+| mistral | `MISTRAL_API_KEY` |
+| deepseek | `DEEPSEEK_API_KEY` |
+| bedrock | `AWS_ACCESS_KEY_ID` |
+
+## Architecture
+
+```
+┌─────────────────┐
+│ CLI Entry │
+│ (index.ts) │
+└────────┬────────┘
+ │
+ ▼
+┌─────────────────┐
+│ ExtensionHost │
+│ (extension- │
+│ host.ts) │
+└────────┬────────┘
+ │
+ ┌────┴────┐
+ │ │
+ ▼ ▼
+┌───────┐ ┌──────────┐
+│vscode │ │Extension │
+│-shim │ │ Bundle │
+└───────┘ └──────────┘
+```
+
+## How It Works
+
+1. **CLI Entry Point** (`index.ts`): Parses command line arguments and initializes the ExtensionHost
+
+2. **ExtensionHost** (`extension-host.ts`):
+
+ - Creates a VSCode API mock using `@roo-code/vscode-shim`
+ - Intercepts `require('vscode')` to return the mock
+ - Loads and activates the extension bundle
+ - Manages bidirectional message flow
+
+3. **Message Flow**:
+ - CLI → Extension: `emit("webviewMessage", {...})`
+ - Extension → CLI: `emit("extensionWebviewMessage", {...})`
+
+## Current Limitations
+
+- **No TUI**: Output is plain text (no React/Ink UI yet)
+- **No configuration file**: Settings are passed via command line flags
+- **No persistence**: Each run is a fresh session
+
+## Development
+
+```bash
+# Watch mode for development
+pnpm dev
+
+# Run tests
+pnpm test
+
+# Type checking
+pnpm check-types
+
+# Linting
+pnpm lint
+```
+
+## Releasing
+
+To create a new release, run the release script from the monorepo root:
+
+```bash
+# Release using version from package.json
+./apps/cli/scripts/release.sh
+
+# Release with a specific version
+./apps/cli/scripts/release.sh 0.1.0
+```
+
+The script will:
+
+1. Build the extension and CLI
+2. Create a platform-specific tarball (for your current OS/architecture)
+3. Create a GitHub release with the tarball attached
+
+**Prerequisites:**
+
+- GitHub CLI (`gh`) installed and authenticated (`gh auth login`)
+- pnpm installed
+
+## Troubleshooting
+
+### Extension bundle not found
+
+Make sure you've built the main extension first:
+
+```bash
+cd src
+pnpm bundle
+```
+
+### Module resolution errors
+
+The CLI expects the extension to be a CommonJS bundle. Make sure the extension's esbuild config outputs CommonJS.
+
+### "vscode" module not found
+
+The CLI intercepts `require('vscode')` calls. If you see this error, the module resolution interception may have failed.
diff --git a/apps/cli/eslint.config.mjs b/apps/cli/eslint.config.mjs
new file mode 100644
index 0000000000..694bf73664
--- /dev/null
+++ b/apps/cli/eslint.config.mjs
@@ -0,0 +1,4 @@
+import { config } from "@roo-code/config-eslint/base"
+
+/** @type {import("eslint").Linter.Config} */
+export default [...config]
diff --git a/apps/cli/install.sh b/apps/cli/install.sh
new file mode 100755
index 0000000000..ca82ecfdd8
--- /dev/null
+++ b/apps/cli/install.sh
@@ -0,0 +1,287 @@
+#!/bin/sh
+# Roo Code CLI Installer
+# Usage: curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
+#
+# Environment variables:
+# ROO_INSTALL_DIR - Installation directory (default: ~/.roo/cli)
+# ROO_BIN_DIR - Binary symlink directory (default: ~/.local/bin)
+# ROO_VERSION - Specific version to install (default: latest)
+
+set -e
+
+# Configuration
+INSTALL_DIR="${ROO_INSTALL_DIR:-$HOME/.roo/cli}"
+BIN_DIR="${ROO_BIN_DIR:-$HOME/.local/bin}"
+REPO="RooCodeInc/Roo-Code"
+MIN_NODE_VERSION=20
+
+# Color output (only if terminal supports it)
+if [ -t 1 ]; then
+ RED='\033[0;31m'
+ GREEN='\033[0;32m'
+ YELLOW='\033[1;33m'
+ BLUE='\033[0;34m'
+ BOLD='\033[1m'
+ NC='\033[0m'
+else
+ RED=''
+ GREEN=''
+ YELLOW=''
+ BLUE=''
+ BOLD=''
+ NC=''
+fi
+
+info() { printf "${GREEN}==>${NC} %s\n" "$1"; }
+warn() { printf "${YELLOW}Warning:${NC} %s\n" "$1"; }
+error() { printf "${RED}Error:${NC} %s\n" "$1" >&2; exit 1; }
+
+# Check Node.js version
+check_node() {
+ if ! command -v node >/dev/null 2>&1; then
+ error "Node.js is not installed. Please install Node.js $MIN_NODE_VERSION or higher.
+
+Install Node.js:
+ - macOS: brew install node
+ - Linux: https://nodejs.org/en/download/package-manager
+ - Or use a version manager like fnm, nvm, or mise"
+ fi
+
+ NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
+ if [ "$NODE_VERSION" -lt "$MIN_NODE_VERSION" ]; then
+ error "Node.js $MIN_NODE_VERSION+ required. Found: $(node -v)
+
+Please upgrade Node.js to version $MIN_NODE_VERSION or higher."
+ fi
+
+ info "Found Node.js $(node -v)"
+}
+
+# Detect OS and architecture
+detect_platform() {
+ OS=$(uname -s | tr '[:upper:]' '[:lower:]')
+ ARCH=$(uname -m)
+
+ case "$OS" in
+ darwin) OS="darwin" ;;
+ linux) OS="linux" ;;
+ mingw*|msys*|cygwin*)
+ error "Windows is not supported by this installer. Please use WSL or install manually."
+ ;;
+ *) error "Unsupported OS: $OS" ;;
+ esac
+
+ case "$ARCH" in
+ x86_64|amd64) ARCH="x64" ;;
+ arm64|aarch64) ARCH="arm64" ;;
+ *) error "Unsupported architecture: $ARCH" ;;
+ esac
+
+ PLATFORM="${OS}-${ARCH}"
+ info "Detected platform: $PLATFORM"
+}
+
+# Get latest release version or use specified version
+get_version() {
+ if [ -n "$ROO_VERSION" ]; then
+ VERSION="$ROO_VERSION"
+ info "Using specified version: $VERSION"
+ return
+ fi
+
+ info "Fetching latest version..."
+
+ # Try to get the latest cli release
+ RELEASES_JSON=$(curl -fsSL "https://api.github.com/repos/$REPO/releases" 2>/dev/null) || {
+ error "Failed to fetch releases from GitHub. Check your internet connection."
+ }
+
+ # Extract the latest cli-v* tag
+ VERSION=$(echo "$RELEASES_JSON" |
+ grep -o '"tag_name": "cli-v[^"]*"' |
+ head -1 |
+ sed 's/"tag_name": "cli-v//' |
+ sed 's/"//')
+
+ if [ -z "$VERSION" ]; then
+ error "Could not find any CLI releases. The CLI may not have been released yet."
+ fi
+
+ info "Latest version: $VERSION"
+}
+
+# Download and extract
+download_and_install() {
+ TARBALL="roo-cli-${PLATFORM}.tar.gz"
+ URL="https://github.com/$REPO/releases/download/cli-v${VERSION}/${TARBALL}"
+
+ info "Downloading from $URL..."
+
+ # Create temp directory
+ TMP_DIR=$(mktemp -d)
+ trap "rm -rf $TMP_DIR" EXIT
+
+ # Download with progress indicator
+ HTTP_CODE=$(curl -fsSL -w "%{http_code}" "$URL" -o "$TMP_DIR/$TARBALL" 2>/dev/null) || {
+ if [ "$HTTP_CODE" = "404" ]; then
+ error "Release not found for platform $PLATFORM version $VERSION.
+
+Available at: https://github.com/$REPO/releases"
+ fi
+ error "Download failed. HTTP code: $HTTP_CODE"
+ }
+
+ # Verify we got something
+ if [ ! -s "$TMP_DIR/$TARBALL" ]; then
+ error "Downloaded file is empty. Please try again."
+ fi
+
+ # Remove old installation if exists
+ if [ -d "$INSTALL_DIR" ]; then
+ info "Removing previous installation..."
+ rm -rf "$INSTALL_DIR"
+ fi
+
+ mkdir -p "$INSTALL_DIR"
+
+ # Extract
+ info "Extracting to $INSTALL_DIR..."
+ tar -xzf "$TMP_DIR/$TARBALL" -C "$INSTALL_DIR" --strip-components=1 || {
+ error "Failed to extract tarball. The download may be corrupted."
+ }
+
+ # Save ripgrep binary before npm install (npm install will overwrite node_modules)
+ RIPGREP_BIN=""
+ if [ -f "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg" ]; then
+ RIPGREP_BIN="$TMP_DIR/rg"
+ cp "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg" "$RIPGREP_BIN"
+ fi
+
+ # Install npm dependencies
+ info "Installing dependencies..."
+ cd "$INSTALL_DIR"
+ npm install --production --silent 2>/dev/null || {
+ warn "npm install failed, trying with --legacy-peer-deps..."
+ npm install --production --legacy-peer-deps --silent 2>/dev/null || {
+ error "Failed to install dependencies. Make sure npm is available."
+ }
+ }
+ cd - > /dev/null
+
+ # Restore ripgrep binary after npm install
+ if [ -n "$RIPGREP_BIN" ] && [ -f "$RIPGREP_BIN" ]; then
+ mkdir -p "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin"
+ cp "$RIPGREP_BIN" "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg"
+ chmod +x "$INSTALL_DIR/node_modules/@vscode/ripgrep/bin/rg"
+ fi
+
+ # Make executable
+ chmod +x "$INSTALL_DIR/bin/roo"
+
+ # Also make ripgrep executable if it exists
+ if [ -f "$INSTALL_DIR/bin/rg" ]; then
+ chmod +x "$INSTALL_DIR/bin/rg"
+ fi
+}
+
+# Create symlink in bin directory
+setup_bin() {
+ mkdir -p "$BIN_DIR"
+
+ # Remove old symlink if exists
+ if [ -L "$BIN_DIR/roo" ] || [ -f "$BIN_DIR/roo" ]; then
+ rm -f "$BIN_DIR/roo"
+ fi
+
+ ln -sf "$INSTALL_DIR/bin/roo" "$BIN_DIR/roo"
+ info "Created symlink: $BIN_DIR/roo"
+}
+
+# Check if bin dir is in PATH and provide instructions
+check_path() {
+ case ":$PATH:" in
+ *":$BIN_DIR:"*)
+ # Already in PATH
+ return 0
+ ;;
+ esac
+
+ warn "$BIN_DIR is not in your PATH"
+ echo ""
+ echo "Add this line to your shell profile:"
+ echo ""
+
+ # Detect shell and provide specific instructions
+ SHELL_NAME=$(basename "$SHELL")
+ case "$SHELL_NAME" in
+ zsh)
+ echo " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.zshrc"
+ echo " source ~/.zshrc"
+ ;;
+ bash)
+ if [ -f "$HOME/.bashrc" ]; then
+ echo " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.bashrc"
+ echo " source ~/.bashrc"
+ else
+ echo " echo 'export PATH=\"$BIN_DIR:\$PATH\"' >> ~/.bash_profile"
+ echo " source ~/.bash_profile"
+ fi
+ ;;
+ fish)
+ echo " set -Ux fish_user_paths $BIN_DIR \$fish_user_paths"
+ ;;
+ *)
+ echo " export PATH=\"$BIN_DIR:\$PATH\""
+ ;;
+ esac
+ echo ""
+}
+
+# Verify installation
+verify_install() {
+ if [ -x "$BIN_DIR/roo" ]; then
+ info "Verifying installation..."
+ # Just check if it runs without error
+ "$BIN_DIR/roo" --version >/dev/null 2>&1 || true
+ fi
+}
+
+# Print success message
+print_success() {
+ echo ""
+ printf "${GREEN}${BOLD}✓ Roo Code CLI installed successfully!${NC}\n"
+ echo ""
+ echo " Installation: $INSTALL_DIR"
+ echo " Binary: $BIN_DIR/roo"
+ echo " Version: $VERSION"
+ echo ""
+ echo " ${BOLD}Get started:${NC}"
+ echo " roo --help"
+ echo ""
+ echo " ${BOLD}Example:${NC}"
+ echo " export OPENROUTER_API_KEY=sk-or-v1-..."
+ echo " roo \"What is this project?\" --workspace ~/my-project"
+ echo ""
+}
+
+# Main
+main() {
+ echo ""
+ printf "${BLUE}${BOLD}"
+ echo " ╭─────────────────────────────────╮"
+ echo " │ Roo Code CLI Installer │"
+ echo " ╰─────────────────────────────────╯"
+ printf "${NC}"
+ echo ""
+
+ check_node
+ detect_platform
+ get_version
+ download_and_install
+ setup_bin
+ check_path
+ verify_install
+ print_success
+}
+
+main "$@"
diff --git a/apps/cli/package.json b/apps/cli/package.json
new file mode 100644
index 0000000000..f4c4a3bcb5
--- /dev/null
+++ b/apps/cli/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@roo-code/cli",
+ "version": "0.1.0",
+ "description": "Roo Code CLI - Run the Roo Code agent from the command line",
+ "private": true,
+ "type": "module",
+ "main": "dist/index.js",
+ "bin": {
+ "roo": "dist/index.js"
+ },
+ "scripts": {
+ "format": "prettier --write 'src/**/*.ts'",
+ "lint": "eslint src --ext .ts --max-warnings=0",
+ "check-types": "tsc --noEmit",
+ "test": "vitest run",
+ "build": "tsup",
+ "start": "node dist/index.js",
+ "clean": "rimraf dist .turbo"
+ },
+ "dependencies": {
+ "@roo-code/types": "workspace:^",
+ "@roo-code/vscode-shim": "workspace:^",
+ "@vscode/ripgrep": "^1.15.9",
+ "commander": "^12.1.0"
+ },
+ "devDependencies": {
+ "@roo-code/config-eslint": "workspace:^",
+ "@roo-code/config-typescript": "workspace:^",
+ "@types/node": "^24.1.0",
+ "rimraf": "^6.0.1",
+ "tsup": "^8.4.0",
+ "typescript": "5.8.3",
+ "vitest": "^3.2.3"
+ }
+}
diff --git a/apps/cli/scripts/release.sh b/apps/cli/scripts/release.sh
new file mode 100755
index 0000000000..43d5298956
--- /dev/null
+++ b/apps/cli/scripts/release.sh
@@ -0,0 +1,363 @@
+#!/bin/bash
+# Roo Code CLI Release Script
+#
+# Usage:
+# ./apps/cli/scripts/release.sh [version]
+#
+# Examples:
+# ./apps/cli/scripts/release.sh # Use version from package.json
+# ./apps/cli/scripts/release.sh 0.1.0 # Specify version
+#
+# This script:
+# 1. Builds the extension and CLI
+# 2. Creates a tarball for the current platform
+# 3. Creates a GitHub release and uploads the tarball
+#
+# Prerequisites:
+# - GitHub CLI (gh) installed and authenticated
+# - pnpm installed
+# - Run from the monorepo root directory
+
+set -e
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+BOLD='\033[1m'
+NC='\033[0m'
+
+info() { printf "${GREEN}==>${NC} %s\n" "$1"; }
+warn() { printf "${YELLOW}Warning:${NC} %s\n" "$1"; }
+error() { printf "${RED}Error:${NC} %s\n" "$1" >&2; exit 1; }
+step() { printf "${BLUE}${BOLD}[%s]${NC} %s\n" "$1" "$2"; }
+
+# Get script directory and repo root
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
+CLI_DIR="$REPO_ROOT/apps/cli"
+
+# Detect current platform
+detect_platform() {
+ OS=$(uname -s | tr '[:upper:]' '[:lower:]')
+ ARCH=$(uname -m)
+
+ case "$OS" in
+ darwin) OS="darwin" ;;
+ linux) OS="linux" ;;
+ *) error "Unsupported OS: $OS" ;;
+ esac
+
+ case "$ARCH" in
+ x86_64|amd64) ARCH="x64" ;;
+ arm64|aarch64) ARCH="arm64" ;;
+ *) error "Unsupported architecture: $ARCH" ;;
+ esac
+
+ PLATFORM="${OS}-${ARCH}"
+}
+
+# Check prerequisites
+check_prerequisites() {
+ step "1/7" "Checking prerequisites..."
+
+ if ! command -v gh &> /dev/null; then
+ error "GitHub CLI (gh) is not installed. Install it with: brew install gh"
+ fi
+
+ if ! gh auth status &> /dev/null; then
+ error "GitHub CLI is not authenticated. Run: gh auth login"
+ fi
+
+ if ! command -v pnpm &> /dev/null; then
+ error "pnpm is not installed."
+ fi
+
+ if ! command -v node &> /dev/null; then
+ error "Node.js is not installed."
+ fi
+
+ info "Prerequisites OK"
+}
+
+# Get version
+get_version() {
+ if [ -n "$1" ]; then
+ VERSION="$1"
+ else
+ VERSION=$(node -p "require('$CLI_DIR/package.json').version")
+ fi
+
+ # Validate semver format
+ if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
+ error "Invalid version format: $VERSION (expected semver like 0.1.0)"
+ fi
+
+ TAG="cli-v$VERSION"
+ info "Version: $VERSION (tag: $TAG)"
+}
+
+# Build everything
+build() {
+ step "2/7" "Building extension bundle..."
+ cd "$REPO_ROOT"
+ pnpm bundle
+
+ step "3/7" "Building CLI..."
+ pnpm --filter @roo-code/cli build
+
+ info "Build complete"
+}
+
+# Create release tarball
+create_tarball() {
+ step "4/7" "Creating release tarball for $PLATFORM..."
+
+ RELEASE_DIR="$REPO_ROOT/roo-cli-${PLATFORM}"
+ TARBALL="roo-cli-${PLATFORM}.tar.gz"
+
+ # Clean up any previous build
+ rm -rf "$RELEASE_DIR"
+ rm -f "$REPO_ROOT/$TARBALL"
+
+ # Create directory structure
+ mkdir -p "$RELEASE_DIR/bin"
+ mkdir -p "$RELEASE_DIR/lib"
+ mkdir -p "$RELEASE_DIR/extension"
+
+ # Copy CLI dist files
+ info "Copying CLI files..."
+ cp -r "$CLI_DIR/dist/"* "$RELEASE_DIR/lib/"
+
+ # Create package.json for npm install (only runtime dependencies)
+ info "Creating package.json..."
+ node -e "
+ const pkg = require('$CLI_DIR/package.json');
+ const newPkg = {
+ name: '@roo-code/cli',
+ version: pkg.version,
+ type: 'module',
+ dependencies: {
+ commander: pkg.dependencies.commander
+ }
+ };
+ console.log(JSON.stringify(newPkg, null, 2));
+ " > "$RELEASE_DIR/package.json"
+
+ # Copy extension bundle
+ info "Copying extension bundle..."
+ cp -r "$REPO_ROOT/src/dist/"* "$RELEASE_DIR/extension/"
+
+ # Add package.json to extension directory to mark it as CommonJS
+ # This is necessary because the main package.json has "type": "module"
+ # but the extension bundle is CommonJS
+ echo '{"type": "commonjs"}' > "$RELEASE_DIR/extension/package.json"
+
+ # Find and copy ripgrep binary
+ # The extension looks for ripgrep at: appRoot/node_modules/@vscode/ripgrep/bin/rg
+ # The CLI sets appRoot to the CLI package root, so we need to put ripgrep there
+ info "Looking for ripgrep binary..."
+ RIPGREP_PATH=$(find "$REPO_ROOT/node_modules" -path "*/@vscode/ripgrep/bin/rg" -type f 2>/dev/null | head -1)
+ if [ -n "$RIPGREP_PATH" ] && [ -f "$RIPGREP_PATH" ]; then
+ info "Found ripgrep at: $RIPGREP_PATH"
+ # Create the expected directory structure for the extension to find ripgrep
+ mkdir -p "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin"
+ cp "$RIPGREP_PATH" "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin/"
+ chmod +x "$RELEASE_DIR/node_modules/@vscode/ripgrep/bin/rg"
+ # Also keep a copy in bin/ for direct access
+ mkdir -p "$RELEASE_DIR/bin"
+ cp "$RIPGREP_PATH" "$RELEASE_DIR/bin/"
+ chmod +x "$RELEASE_DIR/bin/rg"
+ else
+ warn "ripgrep binary not found - users will need ripgrep installed"
+ fi
+
+ # Create the wrapper script
+ info "Creating wrapper script..."
+ cat > "$RELEASE_DIR/bin/roo" << 'WRAPPER_EOF'
+#!/usr/bin/env node
+
+import { fileURLToPath } from 'url';
+import { dirname, join } from 'path';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+// Set environment variables for the CLI
+process.env.ROO_EXTENSION_PATH = join(__dirname, '..', 'extension');
+process.env.ROO_RIPGREP_PATH = join(__dirname, 'rg');
+
+// Import and run the actual CLI
+await import(join(__dirname, '..', 'lib', 'index.js'));
+WRAPPER_EOF
+
+ chmod +x "$RELEASE_DIR/bin/roo"
+
+ # Create version file
+ echo "$VERSION" > "$RELEASE_DIR/VERSION"
+
+ # Create tarball
+ info "Creating tarball..."
+ cd "$REPO_ROOT"
+ tar -czvf "$TARBALL" "$(basename "$RELEASE_DIR")"
+
+ # Clean up release directory
+ rm -rf "$RELEASE_DIR"
+
+ # Show size
+ TARBALL_PATH="$REPO_ROOT/$TARBALL"
+ TARBALL_SIZE=$(ls -lh "$TARBALL_PATH" | awk '{print $5}')
+ info "Created: $TARBALL ($TARBALL_SIZE)"
+}
+
+# Create checksum
+create_checksum() {
+ step "5/7" "Creating checksum..."
+ cd "$REPO_ROOT"
+
+ if command -v sha256sum &> /dev/null; then
+ sha256sum "$TARBALL" > "${TARBALL}.sha256"
+ elif command -v shasum &> /dev/null; then
+ shasum -a 256 "$TARBALL" > "${TARBALL}.sha256"
+ else
+ warn "No sha256sum or shasum found, skipping checksum"
+ return
+ fi
+
+ info "Checksum: $(cat "${TARBALL}.sha256")"
+}
+
+# Check if release already exists
+check_existing_release() {
+ step "6/7" "Checking for existing release..."
+
+ if gh release view "$TAG" &> /dev/null; then
+ warn "Release $TAG already exists"
+ read -p "Do you want to delete it and create a new one? [y/N] " -n 1 -r
+ echo
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ info "Deleting existing release..."
+ gh release delete "$TAG" --yes
+ # Also delete the tag if it exists
+ git tag -d "$TAG" 2>/dev/null || true
+ git push origin ":refs/tags/$TAG" 2>/dev/null || true
+ else
+ error "Aborted. Use a different version or delete the existing release manually."
+ fi
+ fi
+}
+
+# Create GitHub release
+create_release() {
+ step "7/7" "Creating GitHub release..."
+ cd "$REPO_ROOT"
+
+ RELEASE_NOTES=$(cat << EOF
+## Installation
+
+\`\`\`bash
+curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
+\`\`\`
+
+Or install a specific version:
+\`\`\`bash
+ROO_VERSION=$VERSION curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh
+\`\`\`
+
+## Requirements
+
+- Node.js 20 or higher
+- macOS (Intel or Apple Silicon) or Linux (x64 or ARM64)
+
+## Usage
+
+\`\`\`bash
+# Set your API key
+export OPENROUTER_API_KEY=sk-or-v1-...
+
+# Run a task
+roo "What is this project?" --workspace ~/my-project
+
+# See all options
+roo --help
+\`\`\`
+
+## Platform Support
+
+This release includes:
+- \`roo-cli-${PLATFORM}.tar.gz\` - Built on $(uname -s) $(uname -m)
+
+> **Note:** Additional platforms will be added as needed. If you need a different platform, please open an issue.
+
+## Checksum
+
+\`\`\`
+$(cat "${TARBALL}.sha256" 2>/dev/null || echo "N/A")
+\`\`\`
+EOF
+)
+
+ # Get the current commit SHA for the release target
+ COMMIT_SHA=$(git rev-parse HEAD)
+ info "Creating release at commit: ${COMMIT_SHA:0:8}"
+
+ # Create release (gh will create the tag automatically)
+ info "Creating release..."
+ RELEASE_FILES="$TARBALL"
+ if [ -f "${TARBALL}.sha256" ]; then
+ RELEASE_FILES="$RELEASE_FILES ${TARBALL}.sha256"
+ fi
+
+ gh release create "$TAG" \
+ --title "Roo Code CLI v$VERSION" \
+ --notes "$RELEASE_NOTES" \
+ --prerelease \
+ --target "$COMMIT_SHA" \
+ $RELEASE_FILES
+
+ info "Release created!"
+}
+
+# Cleanup
+cleanup() {
+ info "Cleaning up..."
+ cd "$REPO_ROOT"
+ rm -f "$TARBALL" "${TARBALL}.sha256"
+}
+
+# Print summary
+print_summary() {
+ echo ""
+ printf "${GREEN}${BOLD}✓ Release v$VERSION created successfully!${NC}\n"
+ echo ""
+ echo " Release URL: https://github.com/RooCodeInc/Roo-Code/releases/tag/$TAG"
+ echo ""
+ echo " Install with:"
+ echo " curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh"
+ echo ""
+}
+
+# Main
+main() {
+ echo ""
+ printf "${BLUE}${BOLD}"
+ echo " ╭─────────────────────────────────╮"
+ echo " │ Roo Code CLI Release Script │"
+ echo " ╰─────────────────────────────────╯"
+ printf "${NC}"
+ echo ""
+
+ detect_platform
+ check_prerequisites
+ get_version "$1"
+ build
+ create_tarball
+ create_checksum
+ check_existing_release
+ create_release
+ cleanup
+ print_summary
+}
+
+main "$@"
diff --git a/apps/cli/src/__tests__/extension-host.test.ts b/apps/cli/src/__tests__/extension-host.test.ts
new file mode 100644
index 0000000000..509ad27d1e
--- /dev/null
+++ b/apps/cli/src/__tests__/extension-host.test.ts
@@ -0,0 +1,1164 @@
+// pnpm --filter @roo-code/cli test src/__tests__/extension-host.test.ts
+
+import { ExtensionHost, type ExtensionHostOptions } from "../extension-host.js"
+import { EventEmitter } from "events"
+import type { ProviderName } from "@roo-code/types"
+
+vi.mock("@roo-code/vscode-shim", () => ({
+ createVSCodeAPI: vi.fn(() => ({
+ context: { extensionPath: "/test/extension" },
+ })),
+}))
+
+/**
+ * Create a test ExtensionHost with default options
+ */
+function createTestHost({
+ mode = "code",
+ apiProvider = "openrouter",
+ model = "test-model",
+ ...options
+}: Partial = {}): ExtensionHost {
+ return new ExtensionHost({
+ mode,
+ apiProvider,
+ model,
+ workspacePath: "/test/workspace",
+ extensionPath: "/test/extension",
+ ...options,
+ })
+}
+
+// Type for accessing private members
+type PrivateHost = Record
+
+/**
+ * Helper to access private members for testing
+ */
+function getPrivate(host: ExtensionHost, key: string): T {
+ return (host as unknown as PrivateHost)[key] as T
+}
+
+/**
+ * Helper to call private methods for testing
+ */
+function callPrivate(host: ExtensionHost, method: string, ...args: unknown[]): T {
+ const fn = (host as unknown as PrivateHost)[method] as ((...a: unknown[]) => T) | undefined
+ if (!fn) throw new Error(`Method ${method} not found`)
+ return fn.apply(host, args)
+}
+
+/**
+ * Helper to spy on private methods
+ * This uses a more permissive type to avoid TypeScript errors with vi.spyOn on private methods
+ */
+function spyOnPrivate(host: ExtensionHost, method: string) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return vi.spyOn(host as any, method)
+}
+
+describe("ExtensionHost", () => {
+ beforeEach(() => {
+ vi.resetAllMocks()
+ // Clean up globals
+ delete (global as Record).vscode
+ delete (global as Record).__extensionHost
+ })
+
+ describe("constructor", () => {
+ it("should store options correctly", () => {
+ const options: ExtensionHostOptions = {
+ mode: "code",
+ workspacePath: "/my/workspace",
+ extensionPath: "/my/extension",
+ verbose: true,
+ quiet: true,
+ apiKey: "test-key",
+ apiProvider: "openrouter",
+ model: "test-model",
+ }
+
+ const host = new ExtensionHost(options)
+
+ expect(getPrivate(host, "options")).toEqual(options)
+ })
+
+ it("should be an EventEmitter instance", () => {
+ const host = createTestHost()
+ expect(host).toBeInstanceOf(EventEmitter)
+ })
+
+ it("should initialize with default state values", () => {
+ const host = createTestHost()
+
+ expect(getPrivate(host, "isWebviewReady")).toBe(false)
+ expect(getPrivate(host, "pendingMessages")).toEqual([])
+ expect(getPrivate(host, "vscode")).toBeNull()
+ expect(getPrivate(host, "extensionModule")).toBeNull()
+ })
+ })
+
+ describe("buildApiConfiguration", () => {
+ it.each([
+ [
+ "anthropic",
+ "test-key",
+ "test-model",
+ { apiProvider: "anthropic", apiKey: "test-key", apiModelId: "test-model" },
+ ],
+ [
+ "openrouter",
+ "or-key",
+ "or-model",
+ {
+ apiProvider: "openrouter",
+ openRouterApiKey: "or-key",
+ openRouterModelId: "or-model",
+ },
+ ],
+ [
+ "gemini",
+ "gem-key",
+ "gem-model",
+ { apiProvider: "gemini", geminiApiKey: "gem-key", apiModelId: "gem-model" },
+ ],
+ [
+ "openai-native",
+ "oai-key",
+ "oai-model",
+ { apiProvider: "openai-native", openAiNativeApiKey: "oai-key", apiModelId: "oai-model" },
+ ],
+ [
+ "openai",
+ "oai-key",
+ "oai-model",
+ { apiProvider: "openai", openAiApiKey: "oai-key", openAiModelId: "oai-model" },
+ ],
+ [
+ "mistral",
+ "mis-key",
+ "mis-model",
+ { apiProvider: "mistral", mistralApiKey: "mis-key", apiModelId: "mis-model" },
+ ],
+ [
+ "deepseek",
+ "ds-key",
+ "ds-model",
+ { apiProvider: "deepseek", deepSeekApiKey: "ds-key", apiModelId: "ds-model" },
+ ],
+ ["xai", "xai-key", "xai-model", { apiProvider: "xai", xaiApiKey: "xai-key", apiModelId: "xai-model" }],
+ [
+ "groq",
+ "groq-key",
+ "groq-model",
+ { apiProvider: "groq", groqApiKey: "groq-key", apiModelId: "groq-model" },
+ ],
+ [
+ "fireworks",
+ "fw-key",
+ "fw-model",
+ { apiProvider: "fireworks", fireworksApiKey: "fw-key", apiModelId: "fw-model" },
+ ],
+ [
+ "cerebras",
+ "cer-key",
+ "cer-model",
+ { apiProvider: "cerebras", cerebrasApiKey: "cer-key", apiModelId: "cer-model" },
+ ],
+ [
+ "sambanova",
+ "sn-key",
+ "sn-model",
+ { apiProvider: "sambanova", sambaNovaApiKey: "sn-key", apiModelId: "sn-model" },
+ ],
+ [
+ "ollama",
+ "oll-key",
+ "oll-model",
+ { apiProvider: "ollama", ollamaApiKey: "oll-key", ollamaModelId: "oll-model" },
+ ],
+ ["lmstudio", undefined, "lm-model", { apiProvider: "lmstudio", lmStudioModelId: "lm-model" }],
+ [
+ "litellm",
+ "lite-key",
+ "lite-model",
+ { apiProvider: "litellm", litellmApiKey: "lite-key", litellmModelId: "lite-model" },
+ ],
+ [
+ "huggingface",
+ "hf-key",
+ "hf-model",
+ { apiProvider: "huggingface", huggingFaceApiKey: "hf-key", huggingFaceModelId: "hf-model" },
+ ],
+ ["chutes", "ch-key", "ch-model", { apiProvider: "chutes", chutesApiKey: "ch-key", apiModelId: "ch-model" }],
+ [
+ "featherless",
+ "fl-key",
+ "fl-model",
+ { apiProvider: "featherless", featherlessApiKey: "fl-key", apiModelId: "fl-model" },
+ ],
+ [
+ "unbound",
+ "ub-key",
+ "ub-model",
+ { apiProvider: "unbound", unboundApiKey: "ub-key", unboundModelId: "ub-model" },
+ ],
+ [
+ "requesty",
+ "req-key",
+ "req-model",
+ { apiProvider: "requesty", requestyApiKey: "req-key", requestyModelId: "req-model" },
+ ],
+ [
+ "deepinfra",
+ "di-key",
+ "di-model",
+ { apiProvider: "deepinfra", deepInfraApiKey: "di-key", deepInfraModelId: "di-model" },
+ ],
+ [
+ "vercel-ai-gateway",
+ "vai-key",
+ "vai-model",
+ {
+ apiProvider: "vercel-ai-gateway",
+ vercelAiGatewayApiKey: "vai-key",
+ vercelAiGatewayModelId: "vai-model",
+ },
+ ],
+ ["zai", "zai-key", "zai-model", { apiProvider: "zai", zaiApiKey: "zai-key", apiModelId: "zai-model" }],
+ [
+ "baseten",
+ "bt-key",
+ "bt-model",
+ { apiProvider: "baseten", basetenApiKey: "bt-key", apiModelId: "bt-model" },
+ ],
+ ["doubao", "db-key", "db-model", { apiProvider: "doubao", doubaoApiKey: "db-key", apiModelId: "db-model" }],
+ [
+ "moonshot",
+ "ms-key",
+ "ms-model",
+ { apiProvider: "moonshot", moonshotApiKey: "ms-key", apiModelId: "ms-model" },
+ ],
+ [
+ "minimax",
+ "mm-key",
+ "mm-model",
+ { apiProvider: "minimax", minimaxApiKey: "mm-key", apiModelId: "mm-model" },
+ ],
+ [
+ "io-intelligence",
+ "io-key",
+ "io-model",
+ { apiProvider: "io-intelligence", ioIntelligenceApiKey: "io-key", ioIntelligenceModelId: "io-model" },
+ ],
+ ])("should configure %s provider correctly", (provider, apiKey, model, expected) => {
+ const host = createTestHost({
+ apiProvider: provider as ProviderName,
+ apiKey,
+ model,
+ })
+
+ const config = callPrivate>(host, "buildApiConfiguration")
+
+ expect(config).toEqual(expected)
+ })
+
+ it("should use default provider when not specified", () => {
+ const host = createTestHost({
+ apiKey: "test-key",
+ model: "test-model",
+ })
+
+ const config = callPrivate>(host, "buildApiConfiguration")
+
+ expect(config.apiProvider).toBe("openrouter")
+ })
+
+ it("should handle missing apiKey gracefully", () => {
+ const host = createTestHost({
+ apiProvider: "anthropic",
+ model: "test-model",
+ })
+
+ const config = callPrivate>(host, "buildApiConfiguration")
+
+ expect(config.apiProvider).toBe("anthropic")
+ expect(config.apiKey).toBeUndefined()
+ expect(config.apiModelId).toBe("test-model")
+ })
+
+ it("should use default config for unknown providers", () => {
+ const host = createTestHost({
+ apiProvider: "unknown-provider" as ProviderName,
+ apiKey: "test-key",
+ model: "test-model",
+ })
+
+ const config = callPrivate>(host, "buildApiConfiguration")
+
+ expect(config.apiProvider).toBe("unknown-provider")
+ expect(config.apiKey).toBe("test-key")
+ expect(config.apiModelId).toBe("test-model")
+ })
+ })
+
+ describe("webview provider registration", () => {
+ it("should register webview provider", () => {
+ const host = createTestHost()
+ const mockProvider = { resolveWebviewView: vi.fn() }
+
+ host.registerWebviewProvider("test-view", mockProvider)
+
+ const providers = getPrivate