mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-13 23:11:10 +00:00
Merge branch 'RooCodeInc:main' into add-dart-codebase-index
This commit is contained in:
commit
52c2574bec
419 changed files with 30183 additions and 3536 deletions
|
|
@ -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/
|
||||
|
|
|
|||
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -84,7 +84,6 @@ body:
|
|||
- Google Gemini
|
||||
- Google Vertex AI
|
||||
- Groq
|
||||
- Human Relay Provider
|
||||
- LiteLLM
|
||||
- LM Studio
|
||||
- Mistral AI
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
| Auto-approve | 自动批准 | 始终批准 | 权限相关术语 |
|
||||
| Checkpoint | 存档点 | 检查点/快照 | 技术概念统一 |
|
||||
| MCP Server | MCP 服务 | MCP 服务器 | 技术组件 |
|
||||
| Human Relay | 人工辅助模式 | 人工中继 | 功能描述清晰 |
|
||||
| Network Timeout | 请求超时 | 网络超时 | 更准确描述 |
|
||||
| Terminal | 终端 | 命令行 | 技术术语统一 |
|
||||
| diff | 差异更新 | 差分/补丁 | 代码变更 |
|
||||
|
|
|
|||
76
CHANGELOG.md
76
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)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ Learn more: [Using Modes](https://docs.roocode.com/basic-usage/using-modes) •
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Installing Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configuring Profiles</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Codebase Indexing</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Custom Modes</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Todo Lists</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Custom Modes</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Context Management</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
231
apps/cli/README.md
Normal file
231
apps/cli/README.md
Normal file
|
|
@ -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 <path>` | Workspace path to operate in | Current directory |
|
||||
| `-e, --extension <path>` | 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 <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 | Provider default |
|
||||
| `-M, --mode <mode>` | Mode to start in (code, architect, ask, debug, etc.) | `code` |
|
||||
| `-r, --reasoning-effort <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.
|
||||
4
apps/cli/eslint.config.mjs
Normal file
4
apps/cli/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { config } from "@roo-code/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [...config]
|
||||
287
apps/cli/install.sh
Executable file
287
apps/cli/install.sh
Executable file
|
|
@ -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 "$@"
|
||||
35
apps/cli/package.json
Normal file
35
apps/cli/package.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
363
apps/cli/scripts/release.sh
Executable file
363
apps/cli/scripts/release.sh
Executable file
|
|
@ -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 "$@"
|
||||
1164
apps/cli/src/__tests__/extension-host.test.ts
Normal file
1164
apps/cli/src/__tests__/extension-host.test.ts
Normal file
File diff suppressed because it is too large
Load diff
144
apps/cli/src/__tests__/integration.test.ts
Normal file
144
apps/cli/src/__tests__/integration.test.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* Integration tests for CLI
|
||||
*
|
||||
* These tests require a valid OPENROUTER_API_KEY environment variable.
|
||||
* They will be skipped if the API key is not available.
|
||||
*
|
||||
* Run with: OPENROUTER_API_KEY=sk-or-v1-... pnpm test
|
||||
*/
|
||||
|
||||
import { ExtensionHost } from "../extension-host.js"
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import os from "os"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY
|
||||
const hasApiKey = !!OPENROUTER_API_KEY
|
||||
|
||||
// Find the extension path - we need a built extension for integration tests
|
||||
function findExtensionPath(): string | null {
|
||||
// From apps/cli/src/__tests__, go up to monorepo root then to src/dist
|
||||
const monorepoPath = path.resolve(__dirname, "../../../../src/dist")
|
||||
if (fs.existsSync(path.join(monorepoPath, "extension.js"))) {
|
||||
return monorepoPath
|
||||
}
|
||||
// Also try from the apps/cli level
|
||||
const altPath = path.resolve(__dirname, "../../../src/dist")
|
||||
if (fs.existsSync(path.join(altPath, "extension.js"))) {
|
||||
return altPath
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const extensionPath = findExtensionPath()
|
||||
const hasExtension = !!extensionPath
|
||||
|
||||
// Create a temporary workspace directory for tests
|
||||
function createTempWorkspace(): string {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "roo-cli-test-"))
|
||||
return tempDir
|
||||
}
|
||||
|
||||
// Clean up temporary workspace
|
||||
function cleanupWorkspace(workspacePath: string): void {
|
||||
try {
|
||||
fs.rmSync(workspacePath, { recursive: true, force: true })
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
describe.skipIf(!hasApiKey || !hasExtension)(
|
||||
"CLI Integration Tests (requires OPENROUTER_API_KEY and built extension)",
|
||||
() => {
|
||||
let workspacePath: string
|
||||
let host: ExtensionHost
|
||||
|
||||
beforeAll(() => {
|
||||
console.log("Integration tests running with:")
|
||||
console.log(` - API Key: ${OPENROUTER_API_KEY?.substring(0, 12)}...`)
|
||||
console.log(` - Extension Path: ${extensionPath}`)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
workspacePath = createTempWorkspace()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (host) {
|
||||
await host.dispose()
|
||||
}
|
||||
cleanupWorkspace(workspacePath)
|
||||
})
|
||||
|
||||
/**
|
||||
* Main integration test - tests the complete end-to-end flow
|
||||
*
|
||||
* NOTE: Due to the extension using singletons (TelemetryService, etc.),
|
||||
* only one integration test can run per process. This single test covers
|
||||
* the main functionality: activation, task execution, completion, and disposal.
|
||||
*/
|
||||
it("should complete end-to-end task execution with proper lifecycle", async () => {
|
||||
host = new ExtensionHost({
|
||||
mode: "code",
|
||||
apiProvider: "openrouter",
|
||||
apiKey: OPENROUTER_API_KEY!,
|
||||
model: "anthropic/claude-haiku-4.5", // Use fast, cheap model for tests.
|
||||
workspacePath,
|
||||
extensionPath: extensionPath!,
|
||||
quiet: true,
|
||||
})
|
||||
|
||||
// Test activation
|
||||
await host.activate()
|
||||
|
||||
// Track state messages
|
||||
const stateMessages: unknown[] = []
|
||||
host.on("extensionWebviewMessage", (msg: Record<string, unknown>) => {
|
||||
if (msg.type === "state") {
|
||||
stateMessages.push(msg)
|
||||
}
|
||||
})
|
||||
|
||||
// Test task execution with completion
|
||||
// Note: runTask internally waits for webview to be ready before sending messages
|
||||
await expect(host.runTask("Say hello in exactly 5 words")).resolves.toBeUndefined()
|
||||
|
||||
// After task completes, webview should have been ready
|
||||
expect(host.isInInitialSetup()).toBe(false)
|
||||
|
||||
// Verify we received state updates
|
||||
expect(stateMessages.length).toBeGreaterThan(0)
|
||||
|
||||
// Test disposal
|
||||
await host.dispose()
|
||||
expect((global as Record<string, unknown>).vscode).toBeUndefined()
|
||||
expect((global as Record<string, unknown>).__extensionHost).toBeUndefined()
|
||||
}, 120000) // 2 minute timeout
|
||||
},
|
||||
)
|
||||
|
||||
// Additional test to verify skip behavior
|
||||
describe("Integration test skip behavior", () => {
|
||||
it("should have OPENROUTER_API_KEY check", () => {
|
||||
if (hasApiKey) {
|
||||
console.log("OPENROUTER_API_KEY is set, integration tests will run")
|
||||
} else {
|
||||
console.log("OPENROUTER_API_KEY is not set, integration tests will be skipped")
|
||||
}
|
||||
expect(true).toBe(true) // Always passes
|
||||
})
|
||||
|
||||
it("should have extension check", () => {
|
||||
if (hasExtension) {
|
||||
console.log(`Extension found at: ${extensionPath}`)
|
||||
} else {
|
||||
console.log("Extension not found, integration tests will be skipped")
|
||||
}
|
||||
expect(true).toBe(true) // Always passes
|
||||
})
|
||||
})
|
||||
119
apps/cli/src/__tests__/utils.test.ts
Normal file
119
apps/cli/src/__tests__/utils.test.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/**
|
||||
* Unit tests for CLI utility functions
|
||||
*/
|
||||
|
||||
import { getEnvVarName, getApiKeyFromEnv, getDefaultExtensionPath } from "../utils.js"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
|
||||
// Mock fs module
|
||||
vi.mock("fs")
|
||||
|
||||
describe("getEnvVarName", () => {
|
||||
it.each([
|
||||
["anthropic", "ANTHROPIC_API_KEY"],
|
||||
["openai", "OPENAI_API_KEY"],
|
||||
["openrouter", "OPENROUTER_API_KEY"],
|
||||
["google", "GOOGLE_API_KEY"],
|
||||
["gemini", "GOOGLE_API_KEY"],
|
||||
["bedrock", "AWS_ACCESS_KEY_ID"],
|
||||
["ollama", "OLLAMA_API_KEY"],
|
||||
["mistral", "MISTRAL_API_KEY"],
|
||||
["deepseek", "DEEPSEEK_API_KEY"],
|
||||
])("should return %s for %s provider", (provider, expectedEnvVar) => {
|
||||
expect(getEnvVarName(provider)).toBe(expectedEnvVar)
|
||||
})
|
||||
|
||||
it("should handle case-insensitive provider names", () => {
|
||||
expect(getEnvVarName("ANTHROPIC")).toBe("ANTHROPIC_API_KEY")
|
||||
expect(getEnvVarName("Anthropic")).toBe("ANTHROPIC_API_KEY")
|
||||
expect(getEnvVarName("OpenRouter")).toBe("OPENROUTER_API_KEY")
|
||||
})
|
||||
|
||||
it("should return uppercase provider name with _API_KEY suffix for unknown providers", () => {
|
||||
expect(getEnvVarName("custom")).toBe("CUSTOM_API_KEY")
|
||||
expect(getEnvVarName("myProvider")).toBe("MYPROVIDER_API_KEY")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getApiKeyFromEnv", () => {
|
||||
const originalEnv = process.env
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset process.env before each test
|
||||
process.env = { ...originalEnv }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv
|
||||
})
|
||||
|
||||
it("should return API key from environment variable for anthropic", () => {
|
||||
process.env.ANTHROPIC_API_KEY = "test-anthropic-key"
|
||||
expect(getApiKeyFromEnv("anthropic")).toBe("test-anthropic-key")
|
||||
})
|
||||
|
||||
it("should return API key from environment variable for openrouter", () => {
|
||||
process.env.OPENROUTER_API_KEY = "test-openrouter-key"
|
||||
expect(getApiKeyFromEnv("openrouter")).toBe("test-openrouter-key")
|
||||
})
|
||||
|
||||
it("should return API key from environment variable for openai", () => {
|
||||
process.env.OPENAI_API_KEY = "test-openai-key"
|
||||
expect(getApiKeyFromEnv("openai")).toBe("test-openai-key")
|
||||
})
|
||||
|
||||
it("should return undefined when API key is not set", () => {
|
||||
delete process.env.ANTHROPIC_API_KEY
|
||||
expect(getApiKeyFromEnv("anthropic")).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle custom provider names", () => {
|
||||
process.env.CUSTOM_API_KEY = "test-custom-key"
|
||||
expect(getApiKeyFromEnv("custom")).toBe("test-custom-key")
|
||||
})
|
||||
|
||||
it("should handle case-insensitive provider lookup", () => {
|
||||
process.env.ANTHROPIC_API_KEY = "test-key"
|
||||
expect(getApiKeyFromEnv("ANTHROPIC")).toBe("test-key")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getDefaultExtensionPath", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it("should return monorepo path when extension.js exists there", () => {
|
||||
const mockDirname = "/test/apps/cli/dist"
|
||||
const expectedMonorepoPath = path.resolve(mockDirname, "../../../src/dist")
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||
|
||||
const result = getDefaultExtensionPath(mockDirname)
|
||||
|
||||
expect(result).toBe(expectedMonorepoPath)
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(path.join(expectedMonorepoPath, "extension.js"))
|
||||
})
|
||||
|
||||
it("should return package path when extension.js does not exist in monorepo path", () => {
|
||||
const mockDirname = "/test/apps/cli/dist"
|
||||
const expectedPackagePath = path.resolve(mockDirname, "../extension")
|
||||
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false)
|
||||
|
||||
const result = getDefaultExtensionPath(mockDirname)
|
||||
|
||||
expect(result).toBe(expectedPackagePath)
|
||||
})
|
||||
|
||||
it("should check monorepo path first", () => {
|
||||
const mockDirname = "/some/path"
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false)
|
||||
|
||||
getDefaultExtensionPath(mockDirname)
|
||||
|
||||
const expectedMonorepoPath = path.resolve(mockDirname, "../../../src/dist")
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(path.join(expectedMonorepoPath, "extension.js"))
|
||||
})
|
||||
})
|
||||
1663
apps/cli/src/extension-host.ts
Normal file
1663
apps/cli/src/extension-host.ts
Normal file
File diff suppressed because it is too large
Load diff
163
apps/cli/src/index.ts
Normal file
163
apps/cli/src/index.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* @roo-code/cli - Command Line Interface for Roo Code
|
||||
*/
|
||||
|
||||
import { Command } from "commander"
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
import {
|
||||
type ProviderName,
|
||||
type ReasoningEffortExtended,
|
||||
isProviderName,
|
||||
reasoningEffortsExtended,
|
||||
} from "@roo-code/types"
|
||||
import { setLogger } from "@roo-code/vscode-shim"
|
||||
|
||||
import { ExtensionHost } from "./extension-host.js"
|
||||
import { getEnvVarName, getApiKeyFromEnv, getDefaultExtensionPath } from "./utils.js"
|
||||
|
||||
const DEFAULTS = {
|
||||
mode: "code",
|
||||
reasoningEffort: "medium" as const,
|
||||
model: "anthropic/claude-sonnet-4.5",
|
||||
}
|
||||
|
||||
const REASONING_EFFORTS = [...reasoningEffortsExtended, "unspecified", "disabled"]
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const program = new Command()
|
||||
|
||||
program.name("roo").description("Roo Code CLI - Run the Roo Code agent from the command line").version("0.1.0")
|
||||
|
||||
program
|
||||
.argument("<prompt>", "The prompt/task to execute")
|
||||
.option("-w, --workspace <path>", "Workspace path to operate in", process.cwd())
|
||||
.option("-e, --extension <path>", "Path to the extension bundle directory")
|
||||
.option("-v, --verbose", "Enable verbose output (show VSCode and extension logs)", false)
|
||||
.option("-d, --debug", "Enable debug output (includes detailed debug information)", false)
|
||||
.option("-x, --exit-on-complete", "Exit the process when the task completes (useful for testing)", false)
|
||||
.option("-y, --yes", "Auto-approve all prompts (non-interactive mode)", false)
|
||||
.option("-k, --api-key <key>", "API key for the LLM provider (defaults to ANTHROPIC_API_KEY env var)")
|
||||
.option("-p, --provider <provider>", "API provider (anthropic, openai, openrouter, etc.)", "openrouter")
|
||||
.option("-m, --model <model>", "Model to use", DEFAULTS.model)
|
||||
.option("-M, --mode <mode>", "Mode to start in (code, architect, ask, debug, etc.)", DEFAULTS.mode)
|
||||
.option(
|
||||
"-r, --reasoning-effort <effort>",
|
||||
"Reasoning effort level (unspecified, disabled, none, minimal, low, medium, high, xhigh)",
|
||||
DEFAULTS.reasoningEffort,
|
||||
)
|
||||
.action(
|
||||
async (
|
||||
prompt: string,
|
||||
options: {
|
||||
workspace: string
|
||||
extension?: string
|
||||
verbose: boolean
|
||||
debug: boolean
|
||||
exitOnComplete: boolean
|
||||
yes: boolean
|
||||
apiKey?: string
|
||||
provider: ProviderName
|
||||
model?: string
|
||||
mode?: string
|
||||
reasoningEffort?: ReasoningEffortExtended | "unspecified" | "disabled"
|
||||
},
|
||||
) => {
|
||||
// Default is quiet mode - suppress VSCode shim logs unless verbose
|
||||
// or debug is specified.
|
||||
if (!options.verbose && !options.debug) {
|
||||
setLogger({
|
||||
info: () => {},
|
||||
warn: () => {},
|
||||
error: () => {},
|
||||
debug: () => {},
|
||||
})
|
||||
}
|
||||
|
||||
const extensionPath = options.extension || getDefaultExtensionPath(__dirname)
|
||||
const apiKey = options.apiKey || getApiKeyFromEnv(options.provider)
|
||||
const workspacePath = path.resolve(options.workspace)
|
||||
|
||||
if (!apiKey) {
|
||||
console.error(
|
||||
`[CLI] Error: No API key provided. Use --api-key or set the appropriate environment variable.`,
|
||||
)
|
||||
console.error(`[CLI] For ${options.provider}, set ${getEnvVarName(options.provider)}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(workspacePath)) {
|
||||
console.error(`[CLI] Error: Workspace path does not exist: ${workspacePath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!isProviderName(options.provider)) {
|
||||
console.error(`[CLI] Error: Invalid provider: ${options.provider}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (options.reasoningEffort && !REASONING_EFFORTS.includes(options.reasoningEffort)) {
|
||||
console.error(
|
||||
`[CLI] Error: Invalid reasoning effort: ${options.reasoningEffort}, must be one of: ${REASONING_EFFORTS.join(", ")}`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`[CLI] Mode: ${options.mode || "default"}`)
|
||||
console.log(`[CLI] Reasoning Effort: ${options.reasoningEffort || "default"}`)
|
||||
console.log(`[CLI] Provider: ${options.provider}`)
|
||||
console.log(`[CLI] Model: ${options.model || "default"}`)
|
||||
console.log(`[CLI] Workspace: ${workspacePath}`)
|
||||
|
||||
const host = new ExtensionHost({
|
||||
mode: options.mode || DEFAULTS.mode,
|
||||
reasoningEffort: options.reasoningEffort === "unspecified" ? undefined : options.reasoningEffort,
|
||||
apiProvider: options.provider,
|
||||
apiKey,
|
||||
model: options.model || DEFAULTS.model,
|
||||
workspacePath,
|
||||
extensionPath: path.resolve(extensionPath),
|
||||
verbose: options.debug,
|
||||
quiet: !options.verbose && !options.debug,
|
||||
nonInteractive: options.yes,
|
||||
})
|
||||
|
||||
// Handle SIGINT (Ctrl+C)
|
||||
process.on("SIGINT", async () => {
|
||||
console.log("\n[CLI] Received SIGINT, shutting down...")
|
||||
await host.dispose()
|
||||
process.exit(130)
|
||||
})
|
||||
|
||||
// Handle SIGTERM
|
||||
process.on("SIGTERM", async () => {
|
||||
console.log("\n[CLI] Received SIGTERM, shutting down...")
|
||||
await host.dispose()
|
||||
process.exit(143)
|
||||
})
|
||||
|
||||
try {
|
||||
await host.activate()
|
||||
await host.runTask(prompt)
|
||||
await host.dispose()
|
||||
|
||||
if (options.exitOnComplete) {
|
||||
process.exit(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[CLI] Error:", error instanceof Error ? error.message : String(error))
|
||||
|
||||
if (options.debug && error instanceof Error) {
|
||||
console.error(error.stack)
|
||||
}
|
||||
|
||||
await host.dispose()
|
||||
process.exit(1)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
program.parse()
|
||||
62
apps/cli/src/utils.ts
Normal file
62
apps/cli/src/utils.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* Utility functions for the Roo Code CLI
|
||||
*/
|
||||
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
|
||||
/**
|
||||
* Get the environment variable name for a provider's API key
|
||||
*/
|
||||
export function getEnvVarName(provider: string): string {
|
||||
const envVarMap: Record<string, string> = {
|
||||
anthropic: "ANTHROPIC_API_KEY",
|
||||
openai: "OPENAI_API_KEY",
|
||||
openrouter: "OPENROUTER_API_KEY",
|
||||
google: "GOOGLE_API_KEY",
|
||||
gemini: "GOOGLE_API_KEY",
|
||||
bedrock: "AWS_ACCESS_KEY_ID",
|
||||
ollama: "OLLAMA_API_KEY",
|
||||
mistral: "MISTRAL_API_KEY",
|
||||
deepseek: "DEEPSEEK_API_KEY",
|
||||
}
|
||||
return envVarMap[provider.toLowerCase()] || `${provider.toUpperCase()}_API_KEY`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API key from environment variable based on provider
|
||||
*/
|
||||
export function getApiKeyFromEnv(provider: string): string | undefined {
|
||||
const envVar = getEnvVarName(provider)
|
||||
return process.env[envVar]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default path to the extension bundle.
|
||||
* This assumes the CLI is installed alongside the built extension.
|
||||
*
|
||||
* @param dirname - The __dirname equivalent for the calling module
|
||||
*/
|
||||
export function getDefaultExtensionPath(dirname: string): string {
|
||||
// Check for environment variable first (set by install script)
|
||||
if (process.env.ROO_EXTENSION_PATH) {
|
||||
const envPath = process.env.ROO_EXTENSION_PATH
|
||||
if (fs.existsSync(path.join(envPath, "extension.js"))) {
|
||||
return envPath
|
||||
}
|
||||
}
|
||||
|
||||
// __dirname is apps/cli/dist when bundled
|
||||
// The extension is at src/dist (relative to monorepo root)
|
||||
// So from apps/cli/dist, we need to go ../../../src/dist
|
||||
const monorepoPath = path.resolve(dirname, "../../../src/dist")
|
||||
|
||||
// Try monorepo path first (for development)
|
||||
if (fs.existsSync(path.join(monorepoPath, "extension.js"))) {
|
||||
return monorepoPath
|
||||
}
|
||||
|
||||
// Fallback: when installed via curl script, extension is at ../extension
|
||||
const packagePath = path.resolve(dirname, "../extension")
|
||||
return packagePath
|
||||
}
|
||||
9
apps/cli/tsconfig.json
Normal file
9
apps/cli/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@roo-code/config-typescript/base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"],
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "*.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
24
apps/cli/tsup.config.ts
Normal file
24
apps/cli/tsup.config.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { defineConfig } from "tsup"
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/index.ts"],
|
||||
format: ["esm"],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
target: "node20",
|
||||
platform: "node",
|
||||
banner: {
|
||||
js: "#!/usr/bin/env node",
|
||||
},
|
||||
// Bundle workspace packages that export TypeScript
|
||||
noExternal: ["@roo-code/types", "@roo-code/vscode-shim"],
|
||||
external: [
|
||||
// Keep native modules external
|
||||
"@anthropic-ai/sdk",
|
||||
"@anthropic-ai/bedrock-sdk",
|
||||
"@anthropic-ai/vertex-sdk",
|
||||
// Keep @vscode/ripgrep external - we bundle the binary separately
|
||||
"@vscode/ripgrep",
|
||||
],
|
||||
})
|
||||
11
apps/cli/vitest.config.ts
Normal file
11
apps/cli/vitest.config.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
watch: false,
|
||||
testTimeout: 120_000, // 2m for integration tests.
|
||||
include: ["src/**/*.test.ts"],
|
||||
},
|
||||
})
|
||||
|
|
@ -19,10 +19,6 @@ suite("Roo Code Extension", function () {
|
|||
"openInNewTab",
|
||||
"settingsButtonClicked",
|
||||
"historyButtonClicked",
|
||||
"showHumanRelayDialog",
|
||||
"registerHumanRelayCallback",
|
||||
"unregisterHumanRelayCallback",
|
||||
"handleHumanRelayResponse",
|
||||
"newTask",
|
||||
"setCustomStoragePath",
|
||||
"focusInput",
|
||||
|
|
|
|||
|
|
@ -28,10 +28,18 @@ const EVALS_STORAGE_PATH = "/tmp/evals/runs"
|
|||
|
||||
const EVALS_REPO_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../../evals")
|
||||
|
||||
export async function createRun({ suite, exercises = [], timeout, iterations = 1, ...values }: CreateRun) {
|
||||
export async function createRun({
|
||||
suite,
|
||||
exercises = [],
|
||||
timeout,
|
||||
iterations = 1,
|
||||
executionMethod = "vscode",
|
||||
...values
|
||||
}: CreateRun) {
|
||||
const run = await _createRun({
|
||||
...values,
|
||||
timeout,
|
||||
executionMethod,
|
||||
socketPath: "", // TODO: Get rid of this.
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -7,15 +7,26 @@ import { useQuery } from "@tanstack/react-query"
|
|||
import { useForm, FormProvider } from "react-hook-form"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { toast } from "sonner"
|
||||
import { X, Rocket, Check, ChevronsUpDown, SlidersHorizontal, Info, Plus, Minus } from "lucide-react"
|
||||
import {
|
||||
X,
|
||||
Rocket,
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
SlidersHorizontal,
|
||||
Info,
|
||||
Plus,
|
||||
Minus,
|
||||
Terminal,
|
||||
MonitorPlay,
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
globalSettingsSchema,
|
||||
providerSettingsSchema,
|
||||
EVALS_SETTINGS,
|
||||
getModelId,
|
||||
type ProviderSettings,
|
||||
type GlobalSettings,
|
||||
globalSettingsSchema,
|
||||
providerSettingsSchema,
|
||||
getModelId,
|
||||
EVALS_SETTINGS,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { createRun } from "@/actions/runs"
|
||||
|
|
@ -23,6 +34,7 @@ import { getExercises } from "@/actions/exercises"
|
|||
|
||||
import {
|
||||
type CreateRun,
|
||||
type ExecutionMethod,
|
||||
createRunSchema,
|
||||
CONCURRENCY_MIN,
|
||||
CONCURRENCY_MAX,
|
||||
|
|
@ -77,14 +89,12 @@ type ImportedSettings = {
|
|||
currentApiConfigName: string
|
||||
}
|
||||
|
||||
// Type for a model selection entry
|
||||
type ModelSelection = {
|
||||
id: string
|
||||
model: string
|
||||
popoverOpen: boolean
|
||||
}
|
||||
|
||||
// Type for a config selection entry (for import mode)
|
||||
type ConfigSelection = {
|
||||
id: string
|
||||
configName: string
|
||||
|
|
@ -95,16 +105,15 @@ export function NewRun() {
|
|||
const router = useRouter()
|
||||
|
||||
const [provider, setModelSource] = useState<"roo" | "openrouter" | "other">("other")
|
||||
const [executionMethod, setExecutionMethod] = useState<ExecutionMethod>("vscode")
|
||||
const [useNativeToolProtocol, setUseNativeToolProtocol] = useState(true)
|
||||
const [commandExecutionTimeout, setCommandExecutionTimeout] = useState(20)
|
||||
const [terminalShellIntegrationTimeout, setTerminalShellIntegrationTimeout] = useState(30) // seconds
|
||||
|
||||
// State for multiple model selections
|
||||
const [modelSelections, setModelSelections] = useState<ModelSelection[]>([
|
||||
{ id: crypto.randomUUID(), model: "", popoverOpen: false },
|
||||
])
|
||||
|
||||
// State for imported settings with multiple config selections
|
||||
const [importedSettings, setImportedSettings] = useState<ImportedSettings | null>(null)
|
||||
const [configSelections, setConfigSelections] = useState<ConfigSelection[]>([
|
||||
{ id: crypto.randomUUID(), configName: "", popoverOpen: false },
|
||||
|
|
@ -119,7 +128,6 @@ export function NewRun() {
|
|||
|
||||
const exercises = useQuery({ queryKey: ["getExercises"], queryFn: () => getExercises() })
|
||||
|
||||
// State for selected exercises (needed for language toggle buttons)
|
||||
const [selectedExercises, setSelectedExercises] = useState<string[]>([])
|
||||
|
||||
const form = useForm<CreateRun>({
|
||||
|
|
@ -134,6 +142,7 @@ export function NewRun() {
|
|||
timeout: TIMEOUT_DEFAULT,
|
||||
iterations: ITERATIONS_DEFAULT,
|
||||
jobToken: "",
|
||||
executionMethod: "vscode",
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -146,38 +155,49 @@ export function NewRun() {
|
|||
|
||||
const [suite, settings] = watch(["suite", "settings", "concurrency"])
|
||||
|
||||
// Load settings from localStorage on mount
|
||||
useEffect(() => {
|
||||
const savedConcurrency = localStorage.getItem("evals-concurrency")
|
||||
|
||||
if (savedConcurrency) {
|
||||
const parsed = parseInt(savedConcurrency, 10)
|
||||
|
||||
if (!isNaN(parsed) && parsed >= CONCURRENCY_MIN && parsed <= CONCURRENCY_MAX) {
|
||||
setValue("concurrency", parsed)
|
||||
}
|
||||
}
|
||||
|
||||
const savedTimeout = localStorage.getItem("evals-timeout")
|
||||
|
||||
if (savedTimeout) {
|
||||
const parsed = parseInt(savedTimeout, 10)
|
||||
|
||||
if (!isNaN(parsed) && parsed >= TIMEOUT_MIN && parsed <= TIMEOUT_MAX) {
|
||||
setValue("timeout", parsed)
|
||||
}
|
||||
}
|
||||
|
||||
const savedCommandTimeout = localStorage.getItem("evals-command-execution-timeout")
|
||||
|
||||
if (savedCommandTimeout) {
|
||||
const parsed = parseInt(savedCommandTimeout, 10)
|
||||
|
||||
if (!isNaN(parsed) && parsed >= 20 && parsed <= 60) {
|
||||
setCommandExecutionTimeout(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
const savedShellTimeout = localStorage.getItem("evals-shell-integration-timeout")
|
||||
|
||||
if (savedShellTimeout) {
|
||||
const parsed = parseInt(savedShellTimeout, 10)
|
||||
|
||||
if (!isNaN(parsed) && parsed >= 30 && parsed <= 60) {
|
||||
setTerminalShellIntegrationTimeout(parsed)
|
||||
}
|
||||
}
|
||||
// Load saved exercises selection
|
||||
|
||||
const savedSuite = localStorage.getItem("evals-suite")
|
||||
|
||||
if (savedSuite === "partial") {
|
||||
setValue("suite", "partial")
|
||||
const savedExercises = localStorage.getItem("evals-exercises")
|
||||
|
|
@ -189,48 +209,57 @@ export function NewRun() {
|
|||
setValue("exercises", parsed)
|
||||
}
|
||||
} catch {
|
||||
// Invalid JSON, ignore
|
||||
// Invalid JSON, ignore.
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [setValue])
|
||||
|
||||
// Extract unique languages from exercises
|
||||
const languages = useMemo(() => {
|
||||
if (!exercises.data) return []
|
||||
if (!exercises.data) {
|
||||
return []
|
||||
}
|
||||
|
||||
const langs = new Set<string>()
|
||||
|
||||
for (const path of exercises.data) {
|
||||
const lang = path.split("/")[0]
|
||||
if (lang) langs.add(lang)
|
||||
|
||||
if (lang) {
|
||||
langs.add(lang)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(langs).sort()
|
||||
}, [exercises.data])
|
||||
|
||||
// Get exercises for a specific language
|
||||
const getExercisesForLanguage = useCallback(
|
||||
(lang: string) => {
|
||||
if (!exercises.data) return []
|
||||
if (!exercises.data) {
|
||||
return []
|
||||
}
|
||||
|
||||
return exercises.data.filter((path) => path.startsWith(`${lang}/`))
|
||||
},
|
||||
[exercises.data],
|
||||
)
|
||||
|
||||
// Toggle all exercises for a language
|
||||
const toggleLanguage = useCallback(
|
||||
(lang: string) => {
|
||||
const langExercises = getExercisesForLanguage(lang)
|
||||
const allSelected = langExercises.every((ex) => selectedExercises.includes(ex))
|
||||
|
||||
let newSelected: string[]
|
||||
|
||||
if (allSelected) {
|
||||
// Remove all exercises for this language
|
||||
newSelected = selectedExercises.filter((ex) => !ex.startsWith(`${lang}/`))
|
||||
} else {
|
||||
// Add all exercises for this language (avoiding duplicates)
|
||||
const existing = new Set(selectedExercises)
|
||||
|
||||
for (const ex of langExercises) {
|
||||
existing.add(ex)
|
||||
}
|
||||
|
||||
newSelected = Array.from(existing)
|
||||
}
|
||||
|
||||
|
|
@ -241,7 +270,6 @@ export function NewRun() {
|
|||
[getExercisesForLanguage, selectedExercises, setValue],
|
||||
)
|
||||
|
||||
// Check if all exercises for a language are selected
|
||||
const isLanguageSelected = useCallback(
|
||||
(lang: string) => {
|
||||
const langExercises = getExercisesForLanguage(lang)
|
||||
|
|
@ -250,7 +278,6 @@ export function NewRun() {
|
|||
[getExercisesForLanguage, selectedExercises],
|
||||
)
|
||||
|
||||
// Check if some (but not all) exercises for a language are selected
|
||||
const isLanguagePartiallySelected = useCallback(
|
||||
(lang: string) => {
|
||||
const langExercises = getExercisesForLanguage(lang)
|
||||
|
|
@ -260,46 +287,40 @@ export function NewRun() {
|
|||
[getExercisesForLanguage, selectedExercises],
|
||||
)
|
||||
|
||||
// Add a new model selection
|
||||
const addModelSelection = useCallback(() => {
|
||||
setModelSelections((prev) => [...prev, { id: crypto.randomUUID(), model: "", popoverOpen: false }])
|
||||
}, [])
|
||||
|
||||
// Remove a model selection
|
||||
const removeModelSelection = useCallback((id: string) => {
|
||||
setModelSelections((prev) => prev.filter((s) => s.id !== id))
|
||||
}, [])
|
||||
|
||||
// Update a model selection
|
||||
const updateModelSelection = useCallback(
|
||||
(id: string, model: string) => {
|
||||
setModelSelections((prev) => prev.map((s) => (s.id === id ? { ...s, model, popoverOpen: false } : s)))
|
||||
// Also set the form model field for validation (use first non-empty model)
|
||||
// Also set the form model field for validation (use first non-empty model).
|
||||
setValue("model", model)
|
||||
},
|
||||
[setValue],
|
||||
)
|
||||
|
||||
// Toggle popover for a model selection
|
||||
const toggleModelPopover = useCallback((id: string, open: boolean) => {
|
||||
setModelSelections((prev) => prev.map((s) => (s.id === id ? { ...s, popoverOpen: open } : s)))
|
||||
}, [])
|
||||
|
||||
// Add a new config selection
|
||||
const addConfigSelection = useCallback(() => {
|
||||
setConfigSelections((prev) => [...prev, { id: crypto.randomUUID(), configName: "", popoverOpen: false }])
|
||||
}, [])
|
||||
|
||||
// Remove a config selection
|
||||
const removeConfigSelection = useCallback((id: string) => {
|
||||
setConfigSelections((prev) => prev.filter((s) => s.id !== id))
|
||||
}, [])
|
||||
|
||||
// Update a config selection
|
||||
const updateConfigSelection = useCallback(
|
||||
(id: string, configName: string) => {
|
||||
setConfigSelections((prev) => prev.map((s) => (s.id === id ? { ...s, configName, popoverOpen: false } : s)))
|
||||
// Also update the form settings for the first config (for validation)
|
||||
|
||||
// Also update the form settings for the first config (for validation).
|
||||
if (importedSettings) {
|
||||
const providerSettings = importedSettings.apiConfigs[configName] ?? {}
|
||||
setValue("model", getModelId(providerSettings) ?? "")
|
||||
|
|
@ -309,7 +330,6 @@ export function NewRun() {
|
|||
[importedSettings, setValue],
|
||||
)
|
||||
|
||||
// Toggle popover for a config selection
|
||||
const toggleConfigPopover = useCallback((id: string, open: boolean) => {
|
||||
setConfigSelections((prev) => prev.map((s) => (s.id === id ? { ...s, popoverOpen: open } : s)))
|
||||
}, [])
|
||||
|
|
@ -317,24 +337,20 @@ export function NewRun() {
|
|||
const onSubmit = useCallback(
|
||||
async (values: CreateRun) => {
|
||||
try {
|
||||
// Validate jobToken for Roo Code Cloud provider
|
||||
if (provider === "roo" && !values.jobToken?.trim()) {
|
||||
toast.error("Roo Code Cloud Token is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Determine which selections to use based on provider
|
||||
const selectionsToLaunch: { model: string; configName?: string }[] = []
|
||||
|
||||
if (provider === "other") {
|
||||
// For import mode, use config selections
|
||||
for (const config of configSelections) {
|
||||
if (config.configName) {
|
||||
selectionsToLaunch.push({ model: "", configName: config.configName })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For openrouter/roo, use model selections
|
||||
for (const selection of modelSelections) {
|
||||
if (selection.model) {
|
||||
selectionsToLaunch.push({ model: selection.model })
|
||||
|
|
@ -347,20 +363,19 @@ export function NewRun() {
|
|||
return
|
||||
}
|
||||
|
||||
// Show launching toast
|
||||
const totalRuns = selectionsToLaunch.length
|
||||
toast.info(totalRuns > 1 ? `Launching ${totalRuns} runs (every 20 seconds)...` : "Launching run...")
|
||||
|
||||
// Launch runs with 20-second delay between each
|
||||
for (let i = 0; i < selectionsToLaunch.length; i++) {
|
||||
const selection = selectionsToLaunch[i]!
|
||||
|
||||
// Wait 20 seconds between runs (except for the first one)
|
||||
// Wait 20 seconds between runs (except for the first one).
|
||||
if (i > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20000))
|
||||
await new Promise((resolve) => setTimeout(resolve, 20_000))
|
||||
}
|
||||
|
||||
const runValues = { ...values }
|
||||
runValues.executionMethod = executionMethod
|
||||
|
||||
if (provider === "openrouter") {
|
||||
runValues.model = selection.model
|
||||
|
|
@ -403,7 +418,6 @@ export function NewRun() {
|
|||
}
|
||||
}
|
||||
|
||||
// Navigate back to main evals UI
|
||||
router.push("/")
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "An unknown error occurred.")
|
||||
|
|
@ -411,6 +425,7 @@ export function NewRun() {
|
|||
},
|
||||
[
|
||||
provider,
|
||||
executionMethod,
|
||||
modelSelections,
|
||||
configSelections,
|
||||
importedSettings,
|
||||
|
|
@ -442,18 +457,15 @@ export function NewRun() {
|
|||
})
|
||||
.parse(JSON.parse(await file.text()))
|
||||
|
||||
// Store all imported configs for user selection
|
||||
setImportedSettings({
|
||||
apiConfigs: providerProfiles.apiConfigs,
|
||||
globalSettings,
|
||||
currentApiConfigName: providerProfiles.currentApiConfigName,
|
||||
})
|
||||
|
||||
// Default to the current config for the first selection
|
||||
const defaultConfigName = providerProfiles.currentApiConfigName
|
||||
setConfigSelections([{ id: crypto.randomUUID(), configName: defaultConfigName, popoverOpen: false }])
|
||||
|
||||
// Apply the default config
|
||||
const providerSettings = providerProfiles.apiConfigs[defaultConfigName] ?? {}
|
||||
setValue("model", getModelId(providerSettings) ?? "")
|
||||
setValue("settings", { ...EVALS_SETTINGS, ...providerSettings, ...globalSettings })
|
||||
|
|
@ -971,6 +983,36 @@ export function NewRun() {
|
|||
</FormItem>
|
||||
</div>
|
||||
|
||||
{/* Execution Method */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="executionMethod"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Execution Method</FormLabel>
|
||||
<Tabs
|
||||
value={executionMethod}
|
||||
onValueChange={(value) => {
|
||||
const newExecutionMethod = value as ExecutionMethod
|
||||
setExecutionMethod(newExecutionMethod)
|
||||
setValue("executionMethod", newExecutionMethod)
|
||||
}}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="vscode" className="flex items-center gap-2">
|
||||
<MonitorPlay className="size-4" />
|
||||
VSCode
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="cli" className="flex items-center gap-2">
|
||||
<Terminal className="size-4" />
|
||||
CLI
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@ import { z } from "zod"
|
|||
|
||||
import { rooCodeSettingsSchema } from "@roo-code/types"
|
||||
|
||||
/**
|
||||
* ExecutionMethod
|
||||
*/
|
||||
|
||||
export const executionMethodSchema = z.enum(["vscode", "cli"])
|
||||
export type ExecutionMethod = z.infer<typeof executionMethodSchema>
|
||||
|
||||
/**
|
||||
* CreateRun
|
||||
*/
|
||||
|
|
@ -29,6 +36,7 @@ export const createRunSchema = z
|
|||
timeout: z.number().int().min(TIMEOUT_MIN).max(TIMEOUT_MAX),
|
||||
iterations: z.number().int().min(ITERATIONS_MIN).max(ITERATIONS_MAX),
|
||||
jobToken: z.string().optional(),
|
||||
executionMethod: executionMethodSchema,
|
||||
})
|
||||
.refine((data) => data.suite === "full" || (data.exercises || []).length > 0, {
|
||||
message: "Exercises are required when running a partial suite.",
|
||||
|
|
|
|||
297
apps/web-roo-code/src/app/cloud/team/page.tsx
Normal file
297
apps/web-roo-code/src/app/cloud/team/page.tsx
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
import {
|
||||
ArrowRight,
|
||||
Users,
|
||||
Settings,
|
||||
BarChart3,
|
||||
Lock,
|
||||
Puzzle,
|
||||
ShieldCheck,
|
||||
DollarSign,
|
||||
Share2,
|
||||
LucideIcon,
|
||||
Server,
|
||||
ServerIcon,
|
||||
RefreshCcw,
|
||||
} from "lucide-react"
|
||||
import type { Metadata } from "next"
|
||||
|
||||
import { Button } from "@/components/ui"
|
||||
import { AnimatedBackground } from "@/components/homepage"
|
||||
import { SEO } from "@/lib/seo"
|
||||
import { ogImageUrl } from "@/lib/og"
|
||||
import { EXTERNAL_LINKS } from "@/lib/constants"
|
||||
|
||||
const TITLE = "Roo Code Cloud Team Plan"
|
||||
const DESCRIPTION =
|
||||
"Scale your development with team collaboration features. Centralized billing, shared configuration, team-wide analytics, and unified GitHub and Slack integrations."
|
||||
const OG_DESCRIPTION = "Team collaboration for AI-powered development"
|
||||
const PATH = "/cloud/team"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
alternates: {
|
||||
canonical: `${SEO.url}${PATH}`,
|
||||
},
|
||||
openGraph: {
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
url: `${SEO.url}${PATH}`,
|
||||
siteName: SEO.name,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl(TITLE, OG_DESCRIPTION),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: TITLE,
|
||||
},
|
||||
],
|
||||
locale: SEO.locale,
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
card: SEO.twitterCard,
|
||||
title: TITLE,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
|
||||
},
|
||||
keywords: [
|
||||
...SEO.keywords,
|
||||
"team",
|
||||
"collaboration",
|
||||
"enterprise",
|
||||
"organization",
|
||||
"centralized billing",
|
||||
"team management",
|
||||
],
|
||||
}
|
||||
|
||||
const keyBenefits = [
|
||||
{
|
||||
title: "No Per-Seat Costs",
|
||||
description: "Add unlimited team members without worrying about escalating per-seat charges.",
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: "Centralized Billing",
|
||||
description:
|
||||
"Single billing point for all team members using Cloud Agents and the Roo Code Cloud Provider. No more API key management.",
|
||||
icon: DollarSign,
|
||||
},
|
||||
{
|
||||
title: "Unified Integrations",
|
||||
description:
|
||||
"Connect GitHub and Slack once for the entire team. No need for each member to set up individual integrations.",
|
||||
icon: Settings,
|
||||
},
|
||||
{
|
||||
title: "Team-Wide Visibility",
|
||||
description: "Access task history and usage analytics across your entire team with granular per-user filters.",
|
||||
icon: BarChart3,
|
||||
},
|
||||
{
|
||||
title: "Configuration Enforcement",
|
||||
description:
|
||||
"Set policies for providers, models, and MCP servers to ensure your team follows organizational standards.",
|
||||
icon: ShieldCheck,
|
||||
},
|
||||
{
|
||||
title: "Secure Environment Variables",
|
||||
description:
|
||||
"Centrally manage secrets, API keys, and environment variables for Cloud Agents in our encrypted secret store.",
|
||||
icon: Lock,
|
||||
},
|
||||
]
|
||||
|
||||
interface Feature {
|
||||
icon: LucideIcon
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
title: "Configuration Enforcement",
|
||||
description:
|
||||
"Require team members to log in to the VS Code Extension so policies can be enforced via MDM distribution.",
|
||||
},
|
||||
{
|
||||
icon: Server,
|
||||
title: "Provider Management",
|
||||
description:
|
||||
"Configure and manage the model providers your team can access for both the Extension and Cloud Agents, with API-key-free management.",
|
||||
},
|
||||
{
|
||||
icon: Puzzle,
|
||||
title: "Centralized Integration",
|
||||
description:
|
||||
"Centralized GitHub and Slack connection for the entire team. Agents can review PRs, collaborate on your repositories and respond on your team channels.",
|
||||
},
|
||||
{
|
||||
icon: RefreshCcw,
|
||||
title: "Extension Task Sync Config",
|
||||
description:
|
||||
"Require task syncing from VS Code Extension and control visibility settings for who can view each other's tasks.",
|
||||
},
|
||||
{
|
||||
icon: Share2,
|
||||
title: "Task Sharing Controls",
|
||||
description: "Enable per-task sharing with customizable audience controls and link expiration times.",
|
||||
},
|
||||
{
|
||||
icon: ServerIcon,
|
||||
title: "MCP Server Controls",
|
||||
description: "Control access to the Roo MCP Marketplace and what custom MCPs to make available to your team.",
|
||||
},
|
||||
]
|
||||
|
||||
export default function CloudTeamPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero Section */}
|
||||
<section className="relative flex pt-32 pb-20 items-center overflow-hidden">
|
||||
<AnimatedBackground />
|
||||
<div className="container relative flex flex-col items-center h-full z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center max-w-4xl mx-auto mb-12">
|
||||
<h1 className="text-4xl font-bold tracking-tight mb-6 md:text-6xl">Roo Code Cloud Team</h1>
|
||||
<h2 className="text-2xl font-bold tracking-tight mb-6 md:text-4xl">
|
||||
Built for <span className="text-violet-500">AI-Forward Teams</span>
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-8 mx-auto">
|
||||
Empower your entire team with confidence with team-wide configuration, centralized billing,
|
||||
analytics and more. No per-seat costs, no API key juggling.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
size="xl"
|
||||
className="bg-violet-600 hover:bg-violet-700 text-white transition-all duration-300 shadow-lg hover:shadow-violet-500/25"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/checkout/team"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Start 14-Day Free Trial
|
||||
<ArrowRight className="ml-2 size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="xl" className="backdrop-blur-sm" asChild>
|
||||
<a href="/pricing" className="flex items-center justify-center">
|
||||
View Pricing
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Key Benefits Section */}
|
||||
<section className="relative overflow-hidden border-t border-border py-32">
|
||||
<div className="container relative z-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[400px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-500/10 dark:bg-blue-700/20 blur-[140px]" />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mb-12 md:mb-24 max-w-5xl text-center">
|
||||
<div>
|
||||
<h2 className="text-4xl font-bold tracking-tight mb-4">Why Teams Choose Roo</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Streamline collaboration and scale your development capacity with team-first features.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto md:max-w-[1200px]">
|
||||
<ul className="grid grid-cols-1 place-items-center gap-6 md:grid-cols-2 lg:grid-cols-3 lg:gap-6">
|
||||
{keyBenefits.map((benefit, index) => {
|
||||
const Icon = benefit.icon
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className="rounded-3xl bg-card outline outline-border/50 hover:outline-8 shadow-xl p-8 h-full w-full group transition-all hover:shadow-2xl hover:shadow-violet-800/30 relative">
|
||||
{Icon && (
|
||||
<div className="size-15 p-3 rounded-full flex items-center justify-center shadow-lg absolute -top-4 -right-2 transition-all outline outline-foreground/5 bg-card group-hover:outline-3 group-hover:scale-105">
|
||||
<Icon className="size-5 text-violet-500" />
|
||||
</div>
|
||||
)}
|
||||
<h3 className="text-xl font-bold tracking-tight mb-2">{benefit.title}</h3>
|
||||
<div className="text-sm text-muted-foreground leading-relaxed">
|
||||
{benefit.description}
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features Grid */}
|
||||
<section className="py-24 bg-muted/30">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 relative">
|
||||
<div className="absolute inset-y-0 left-1/2 h-full w-full max-w-[1200px] -translate-x-1/2 z-1">
|
||||
<div className="absolute left-1/2 top-1/2 h-[800px] w-full -translate-x-1/2 -translate-y-1/2 rounded-full bg-violet-500/10 dark:bg-violet-700/20 blur-[140px]" />
|
||||
</div>
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-4xl font-bold tracking-tight mb-4">Complete Team Management</h2>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
Access all capabilities from your Organization Settings. Everything you need to manage your
|
||||
team in one place.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto relative">
|
||||
{features.map((feature, index) => {
|
||||
const Icon = feature.icon
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="relative h-full border border-border rounded-2xl bg-background p-8 transition-all duration-300">
|
||||
<Icon className="size-6 text-foreground/80" />
|
||||
<h3 className="mb-3 mt-3 text-xl font-semibold text-foreground">{feature.title}</h3>
|
||||
<p className="leading-relaxed font-light text-muted-foreground">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-24">
|
||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-4xl rounded-3xl border border-border/50 bg-gradient-to-br from-violet-500/10 via-purple-500/5 to-blue-500/5 p-8 text-center shadow-2xl backdrop-blur-xl dark:border-white/10 sm:p-16">
|
||||
<h2 className="mb-6 text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
Ready to scale your team's development?
|
||||
</h2>
|
||||
<p className="mx-auto mb-10 max-w-2xl text-lg text-muted-foreground">
|
||||
Start your free 14-day trial today. Got questions? Get in touch.
|
||||
</p>
|
||||
<div className="flex flex-col justify-center space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-foreground text-background hover:bg-foreground/90 transition-all duration-300"
|
||||
asChild>
|
||||
<a
|
||||
href={EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/checkout/team"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center">
|
||||
Start Free Trial
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" className="backdrop-blur-sm" asChild>
|
||||
<a href={EXTERNAL_LINKS.SUPPORT} className="flex items-center justify-center">
|
||||
Contact Support
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -71,6 +71,7 @@ interface PricingTier {
|
|||
text: string
|
||||
href?: string
|
||||
}
|
||||
learnMoreLink?: string
|
||||
}
|
||||
|
||||
const pricingTiers: PricingTier[] = [
|
||||
|
|
@ -123,6 +124,7 @@ const pricingTiers: PricingTier[] = [
|
|||
text: "Sign up",
|
||||
href: EXTERNAL_LINKS.CLOUD_APP_SIGNUP + "?redirect_url=/billing",
|
||||
},
|
||||
learnMoreLink: "/cloud/team",
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -164,11 +166,11 @@ export default function PricingPage() {
|
|||
<Icon className="size-6" strokeWidth={1.5} />
|
||||
</div>
|
||||
|
||||
<div className="grow mb-8">
|
||||
<div className="grow mb-8 md:h-[214px]">
|
||||
<p className="text-sm text-muted-foreground font-light mb-2">
|
||||
{tier.featuresIntro}
|
||||
</p>
|
||||
<ul className="space-y-3 my-0 md:h-[192px]">
|
||||
<ul className="space-y-3 my-0">
|
||||
{tier.features.map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-2">
|
||||
<Check className="mt-0.5 h-4 w-4 text-muted-foreground shrink-0" />
|
||||
|
|
@ -176,6 +178,15 @@ export default function PricingPage() {
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{tier.learnMoreLink && (
|
||||
<div className="mt-2">
|
||||
<Link
|
||||
href={tier.learnMoreLink}
|
||||
className="text-sm text-violet-600 dark:text-violet-400 hover:underline">
|
||||
Learn more →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-base font-light">{tier.trial}</p>
|
||||
|
|
|
|||
|
|
@ -283,7 +283,8 @@ export default function Privacy() {
|
|||
</li>
|
||||
<li>
|
||||
<strong>Delete your Cloud account</strong> at any time from{" "}
|
||||
<strong>Security Settings</strong> inside Roo Code Cloud.
|
||||
<strong>Security Settings</strong> inside Roo Code Cloud (User Menu → My Settings
|
||||
→ Open Profile).
|
||||
</li>
|
||||
<li>
|
||||
<strong>Marketing communications:</strong> You can unsubscribe from marketing and
|
||||
|
|
|
|||
|
|
@ -70,11 +70,6 @@ export function NavBar({ stars, downloads }: NavBarProps) {
|
|||
</button>
|
||||
{/* Dropdown Menu */}
|
||||
<div className="absolute left-0 top-12 mt-2 w-40 rounded-md border border-border bg-background py-1 shadow-lg opacity-0 -translate-y-2 pointer-events-none group-hover:opacity-100 group-hover:translate-y-0 group-hover:pointer-events-auto transition-all duration-200">
|
||||
<ScrollButton
|
||||
targetId="faq"
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
|
||||
FAQ
|
||||
</ScrollButton>
|
||||
<Link
|
||||
href="/evals"
|
||||
className="block px-4 py-2 text-sm transition-colors hover:bg-accent hover:text-foreground">
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ export function PillarsSection() {
|
|||
<div className="text-muted-foreground my-4 space-y-1">
|
||||
<p>
|
||||
The Roo Code Extension is{" "}
|
||||
<Link target="_blank" href="https://github.com/Roo-Code-Inc/Roo-Code">
|
||||
<Link target="_blank" href="https://github.com/RooCodeInc/Roo-Code">
|
||||
open source
|
||||
</Link>{" "}
|
||||
so you can see for yourself exactly what it's doing and we don't use
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export const EXTERNAL_LINKS = {
|
|||
CLOUD_APP_SIGNUP: "https://app.roocode.com/sign-up",
|
||||
CLOUD_APP_SIGNUP_HOME: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
|
||||
CLOUD_APP_SIGNUP_PRO: "https://app.roocode.com/sign-up?redirect_url=/cloud-agents/setup",
|
||||
SUPPORT: "mailto:support@roocode.com",
|
||||
}
|
||||
|
||||
export const INTERNAL_LINKS = {
|
||||
|
|
|
|||
2
locales/ca/README.md
generated
2
locales/ca/README.md
generated
|
|
@ -69,7 +69,7 @@ Més informació: [Ús de Modes](https://docs.roocode.com/basic-usage/using-mode
|
|||
| | | |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Instal·lant Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configurant perfils</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indexació de la base de codi</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modes personalitzats</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Punts de control</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Llistes de tasques</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modes personalitzats</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Punts de control</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Gestió de Context</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/de/README.md
generated
2
locales/de/README.md
generated
|
|
@ -69,7 +69,7 @@ Mehr erfahren: [Modi verwenden](https://docs.roocode.com/basic-usage/using-modes
|
|||
| | | |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Roo Code installieren</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Profile konfigurieren</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Codebasis-Indizierung</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Benutzerdefinierte Modi</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Todo-Listen</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Benutzerdefinierte Modi</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Kontextverwaltung</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/es/README.md
generated
2
locales/es/README.md
generated
|
|
@ -69,7 +69,7 @@ Más info: [Usar Modos](https://docs.roocode.com/basic-usage/using-modes) • [M
|
|||
| | | |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Instalando Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configurando perfiles</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indexación de la base de código</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modos personalizados</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Listas de Tareas</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modos personalizados</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Gestión de Contexto</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/fr/README.md
generated
2
locales/fr/README.md
generated
|
|
@ -69,7 +69,7 @@ En savoir plus : [Utiliser les Modes](https://docs.roocode.com/basic-usage/using
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Installer Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configurer les profils</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indexation de la base de code</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modes personnalisés</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Listes de tâches</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modes personnalisés</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Gestion du Contexte</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/hi/README.md
generated
2
locales/hi/README.md
generated
|
|
@ -69,7 +69,7 @@
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>रू कोड इंस्टॉल करना</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>प्रोफाइल कॉन्फ़िगर करना</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>कोडबेस इंडेक्सिंग</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>कस्टम मोड</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>चेकपॉइंट्स</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>टू-डू लिस्ट</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>कस्टम मोड</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>चेकपॉइंट्स</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>संदर्भ प्रबंधन</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/id/README.md
generated
2
locales/id/README.md
generated
|
|
@ -69,7 +69,7 @@ Pelajari lebih lanjut: [Menggunakan Mode](https://docs.roocode.com/basic-usage/u
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Menginstal Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Mengonfigurasi Profil</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Pengindeksan Basis Kode</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Mode Kustom</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Pos Pemeriksaan</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Daftar Tugas</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Mode Kustom</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Pos Pemeriksaan</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Manajemen Konteks</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/it/README.md
generated
2
locales/it/README.md
generated
|
|
@ -69,7 +69,7 @@ Scopri di più: [Usare le Modalità](https://docs.roocode.com/basic-usage/using-
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Installazione di Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configurazione dei profili</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indicizzazione della codebase</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modalità personalizzate</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoint</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Elenchi di cose da fare</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modalità personalizzate</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoint</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Gestione del Contesto</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/ja/README.md
generated
2
locales/ja/README.md
generated
|
|
@ -69,7 +69,7 @@ Roo Codeは、あなたの働き方に合わせるように適応します。
|
|||
| | | |
|
||||
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Roo Codeのインストール</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>プロファイルの設定</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>コードベースのインデックス作成</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>カスタムモード</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>チェックポイント</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>ToDoリスト</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>カスタムモード</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>チェックポイント</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>コンテキスト管理</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/ko/README.md
generated
2
locales/ko/README.md
generated
|
|
@ -69,7 +69,7 @@ Roo Code는 당신의 작업 방식에 맞춰 적응합니다.
|
|||
| | | |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Roo Code 설치하기</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>프로필 구성하기</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>코드베이스 인덱싱</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>사용자 지정 모드</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>체크포인트</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>할 일 목록</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>사용자 지정 모드</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>체크포인트</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>컨텍스트 관리</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/nl/README.md
generated
2
locales/nl/README.md
generated
|
|
@ -69,7 +69,7 @@ Meer info: [Modi gebruiken](https://docs.roocode.com/basic-usage/using-modes)
|
|||
| | | |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Roo Code installeren</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Profielen configureren</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Codebase indexeren</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Aangepaste modi</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>To-Do Lijsten</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Aangepaste modi</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Contextbeheer</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/pl/README.md
generated
2
locales/pl/README.md
generated
|
|
@ -69,7 +69,7 @@ Więcej: [Korzystanie z trybów](https://docs.roocode.com/basic-usage/using-mode
|
|||
| | | |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Instalacja Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Konfiguracja profili</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indeksowanie bazy kodu</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Tryby niestandardowe</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Punkty kontrolne</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Listy zadań</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Tryby niestandardowe</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Punkty kontrolne</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Zarządzanie Kontekstem</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/pt-BR/README.md
generated
2
locales/pt-BR/README.md
generated
|
|
@ -69,7 +69,7 @@ Saiba mais: [Usar Modos](https://docs.roocode.com/basic-usage/using-modes) • [
|
|||
| | | |
|
||||
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Instalando o Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Configurando perfis</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Indexação da base de código</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modos personalizados</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Listas de tarefas</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Modos personalizados</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Checkpoints</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Gerenciamento de Contexto</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/ru/README.md
generated
2
locales/ru/README.md
generated
|
|
@ -69,7 +69,7 @@ Roo Code адаптируется к вашему стилю работы, а н
|
|||
| | | |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Установка Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Настройка профилей</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Индексация кодовой базы</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Пользовательские режимы</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Контрольные точки</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Списки дел</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Пользовательские режимы</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Контрольные точки</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Управление Контекстом</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/tr/README.md
generated
2
locales/tr/README.md
generated
|
|
@ -69,7 +69,7 @@ Daha fazla: [Modları kullanma](https://docs.roocode.com/basic-usage/using-modes
|
|||
| | | |
|
||||
| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Roo Code Kurulumu</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Profilleri Yapılandırma</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Kod Tabanı İndeksleme</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Özel Modlar</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Kontrol Noktaları</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Yapılacaklar Listeleri</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Özel Modlar</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Kontrol Noktaları</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Bağlam Yönetimi</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/vi/README.md
generated
2
locales/vi/README.md
generated
|
|
@ -69,7 +69,7 @@ Xem thêm: [Sử dụng Chế độ](https://docs.roocode.com/basic-usage/using-
|
|||
| | | |
|
||||
| :--------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>Cài đặt Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>Định cấu hình Hồ sơ</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>Lập chỉ mục cơ sở mã</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Chế độ tùy chỉnh</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Điểm kiểm tra</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>Danh sách việc cần làm</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>Chế độ tùy chỉnh</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>Điểm kiểm tra</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>Quản lý Ngữ cảnh</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/zh-CN/README.md
generated
2
locales/zh-CN/README.md
generated
|
|
@ -69,7 +69,7 @@ Roo Code 适应您的工作方式,而不是相反:
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>安装 Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>配置个人资料</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>代码库索引</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>自定义模式</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>检查点</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>待办事项列表</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>自定义模式</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>检查点</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>上下文管理</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
2
locales/zh-TW/README.md
generated
2
locales/zh-TW/README.md
generated
|
|
@ -69,7 +69,7 @@ Roo Code 適應您的工作方式,而不是相反:
|
|||
| | | |
|
||||
| :-----------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------: |
|
||||
| <a href="https://www.youtube.com/watch?v=Mcq3r1EPZ-4"><img src="https://img.youtube.com/vi/Mcq3r1EPZ-4/maxresdefault.jpg" width="100%"></a><br><b>安裝 Roo Code</b> | <a href="https://www.youtube.com/watch?v=ZBML8h5cCgo"><img src="https://img.youtube.com/vi/ZBML8h5cCgo/maxresdefault.jpg" width="100%"></a><br><b>設定設定檔</b> | <a href="https://www.youtube.com/watch?v=r1bpod1VWhg"><img src="https://img.youtube.com/vi/r1bpod1VWhg/maxresdefault.jpg" width="100%"></a><br><b>程式碼庫索引</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>自訂模式</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>檢查點</b> | <a href="https://www.youtube.com/watch?v=6h5vB9PpoPk"><img src="https://img.youtube.com/vi/6h5vB9PpoPk/maxresdefault.jpg" width="100%"></a><br><b>待辦事項清單</b> |
|
||||
| <a href="https://www.youtube.com/watch?v=iiAv1eKOaxk"><img src="https://img.youtube.com/vi/iiAv1eKOaxk/maxresdefault.jpg" width="100%"></a><br><b>自訂模式</b> | <a href="https://www.youtube.com/watch?v=Ho30nyY332E"><img src="https://img.youtube.com/vi/Ho30nyY332E/maxresdefault.jpg" width="100%"></a><br><b>檢查點</b> | <a href="https://www.youtube.com/watch?v=HmnNSasv7T8"><img src="https://img.youtube.com/vi/HmnNSasv7T8/maxresdefault.jpg" width="100%"></a><br><b>上下文管理</b> |
|
||||
|
||||
</div>
|
||||
<p align="center">
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@
|
|||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@vscode/ripgrep"
|
||||
],
|
||||
"overrides": {
|
||||
"tar-fs": ">=3.1.1",
|
||||
"esbuild": ">=0.25.0",
|
||||
|
|
|
|||
|
|
@ -158,6 +158,58 @@ export function copyWasms(srcDir: string, distDir: string): void {
|
|||
})
|
||||
|
||||
console.log(`[copyWasms] Copied ${wasmFiles.length} tree-sitter language wasms to ${distDir}`)
|
||||
|
||||
// Copy esbuild-wasm files for custom tool transpilation (cross-platform).
|
||||
copyEsbuildWasmFiles(nodeModulesDir, distDir)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy esbuild-wasm files to the dist/bin directory.
|
||||
*
|
||||
* This function copies the esbuild-wasm CLI and WASM binary, which provides
|
||||
* a cross-platform esbuild implementation that works on all platforms.
|
||||
*
|
||||
* Files copied:
|
||||
* - bin/esbuild (Node.js CLI script)
|
||||
* - esbuild.wasm (WASM binary)
|
||||
* - wasm_exec_node.js (Go WASM runtime for Node.js)
|
||||
* - wasm_exec.js (Go WASM runtime dependency)
|
||||
*/
|
||||
function copyEsbuildWasmFiles(nodeModulesDir: string, distDir: string): void {
|
||||
const esbuildWasmDir = path.join(nodeModulesDir, "esbuild-wasm")
|
||||
|
||||
if (!fs.existsSync(esbuildWasmDir)) {
|
||||
throw new Error(`Directory does not exist: ${esbuildWasmDir}`)
|
||||
}
|
||||
|
||||
// Create bin directory in dist.
|
||||
const binDir = path.join(distDir, "bin")
|
||||
fs.mkdirSync(binDir, { recursive: true })
|
||||
|
||||
// Files to copy - the esbuild CLI script expects wasm_exec_node.js and esbuild.wasm
|
||||
// to be one directory level up from the bin directory (i.e., in distDir directly).
|
||||
// wasm_exec_node.js requires wasm_exec.js, so we need to copy that too.
|
||||
const filesToCopy = [
|
||||
{ src: path.join(esbuildWasmDir, "bin", "esbuild"), dest: path.join(binDir, "esbuild") },
|
||||
{ src: path.join(esbuildWasmDir, "esbuild.wasm"), dest: path.join(distDir, "esbuild.wasm") },
|
||||
{ src: path.join(esbuildWasmDir, "wasm_exec_node.js"), dest: path.join(distDir, "wasm_exec_node.js") },
|
||||
{ src: path.join(esbuildWasmDir, "wasm_exec.js"), dest: path.join(distDir, "wasm_exec.js") },
|
||||
]
|
||||
|
||||
for (const { src, dest } of filesToCopy) {
|
||||
fs.copyFileSync(src, dest)
|
||||
|
||||
// Make CLI executable.
|
||||
if (src.endsWith("esbuild")) {
|
||||
try {
|
||||
fs.chmodSync(dest, 0o755)
|
||||
} catch {
|
||||
// Ignore chmod errors on Windows.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[copyWasms] Copied ${filesToCopy.length} esbuild-wasm files to ${distDir}`)
|
||||
}
|
||||
|
||||
export function copyLocales(srcDir: string, distDir: string): void {
|
||||
|
|
|
|||
|
|
@ -331,10 +331,15 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
|
|||
|
||||
await this.storeCredentials(credentials)
|
||||
|
||||
// Store the provider model if provided
|
||||
// Store the provider model if provided, or flag that no model was selected
|
||||
if (providerModel) {
|
||||
await this.context.globalState.update("roo-provider-model", providerModel)
|
||||
await this.context.globalState.update("roo-auth-skip-model", undefined)
|
||||
this.log(`[auth] Stored provider model: ${providerModel}`)
|
||||
} else {
|
||||
// No model was selected during signup - flag this for the webview
|
||||
await this.context.globalState.update("roo-auth-skip-model", true)
|
||||
this.log(`[auth] No provider model selected during signup`)
|
||||
}
|
||||
|
||||
const vscode = await importVscode()
|
||||
|
|
|
|||
|
|
@ -395,9 +395,38 @@ describe("WebAuthService", () => {
|
|||
await authService.handleCallback("auth-code", storedState, null, "xai/grok-code-fast-1")
|
||||
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("roo-provider-model", "xai/grok-code-fast-1")
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("roo-auth-skip-model", undefined)
|
||||
expect(mockLog).toHaveBeenCalledWith("[auth] Stored provider model: xai/grok-code-fast-1")
|
||||
})
|
||||
|
||||
it("should set skip model flag when provider model is NOT provided in callback", async () => {
|
||||
const storedState = "valid-state"
|
||||
mockContext.globalState.get.mockReturnValue(storedState)
|
||||
|
||||
// Mock successful Clerk sign-in response
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
response: { created_session_id: "session-123" },
|
||||
}),
|
||||
headers: {
|
||||
get: (header: string) => (header === "authorization" ? "Bearer token-123" : null),
|
||||
},
|
||||
}
|
||||
mockFetch.mockResolvedValue(mockResponse)
|
||||
|
||||
const vscode = await import("vscode")
|
||||
const mockShowInfo = vi.fn()
|
||||
vi.mocked(vscode.window.showInformationMessage).mockImplementation(mockShowInfo)
|
||||
|
||||
// Call without provider model
|
||||
await authService.handleCallback("auth-code", storedState, null)
|
||||
|
||||
expect(mockContext.globalState.update).toHaveBeenCalledWith("roo-auth-skip-model", true)
|
||||
expect(mockLog).toHaveBeenCalledWith("[auth] No provider model selected during signup")
|
||||
})
|
||||
|
||||
it("should handle Clerk API errors", async () => {
|
||||
const storedState = "valid-state"
|
||||
mockContext.globalState.get.mockReturnValue(storedState)
|
||||
|
|
|
|||
4
packages/core/eslint.config.mjs
Normal file
4
packages/core/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { config } from "@roo-code/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [...config]
|
||||
26
packages/core/package.json
Normal file
26
packages/core/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "@roo-code/core",
|
||||
"description": "Platform agnostic core functionality for Roo Code.",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src --ext=ts --max-warnings=0",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"clean": "rimraf .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@roo-code/types": "workspace:^",
|
||||
"esbuild": "^0.25.0",
|
||||
"execa": "^9.5.2",
|
||||
"openai": "^5.12.2",
|
||||
"zod": "^3.25.61"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@roo-code/config-eslint": "workspace:^",
|
||||
"@roo-code/config-typescript": "workspace:^",
|
||||
"@types/node": "^24.1.0",
|
||||
"vitest": "^3.2.3"
|
||||
}
|
||||
}
|
||||
231
packages/core/src/custom-tools/__tests__/__snapshots__/format-native.spec.ts.snap
generated
Normal file
231
packages/core/src/custom-tools/__tests__/__snapshots__/format-native.spec.ts.snap
generated
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definition for cached tool 1`] = `
|
||||
{
|
||||
"function": {
|
||||
"description": "Cached tool",
|
||||
"name": "cached",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definition for legacy tool (using args) 1`] = `
|
||||
{
|
||||
"function": {
|
||||
"description": "Legacy tool using args",
|
||||
"name": "legacy",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"input": {
|
||||
"description": "The input string",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definition for mixed export tool 1`] = `
|
||||
{
|
||||
"function": {
|
||||
"description": "Valid",
|
||||
"name": "mixed_validTool",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definition for simple tool 1`] = `
|
||||
{
|
||||
"function": {
|
||||
"description": "Simple tool",
|
||||
"name": "simple",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "The input value",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"value",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definitions for all fixtures combined 1`] = `
|
||||
[
|
||||
{
|
||||
"function": {
|
||||
"description": "Simple tool",
|
||||
"name": "simple",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "The input value",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"value",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Cached tool",
|
||||
"name": "cached",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Legacy tool using args",
|
||||
"name": "legacy",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"input": {
|
||||
"description": "The input string",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Tool A",
|
||||
"name": "multi_toolA",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Tool B",
|
||||
"name": "multi_toolB",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Valid",
|
||||
"name": "mixed_validTool",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Native Protocol snapshots > should generate correct native definitions for multi export tools 1`] = `
|
||||
[
|
||||
{
|
||||
"function": {
|
||||
"description": "Tool A",
|
||||
"name": "multi_toolA",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Tool B",
|
||||
"name": "multi_toolB",
|
||||
"parameters": {
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
"strict": true,
|
||||
},
|
||||
"type": "function",
|
||||
},
|
||||
]
|
||||
`;
|
||||
129
packages/core/src/custom-tools/__tests__/__snapshots__/format-xml.spec.ts.snap
generated
Normal file
129
packages/core/src/custom-tools/__tests__/__snapshots__/format-xml.spec.ts.snap
generated
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for all fixtures combined 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## simple
|
||||
Description: Simple tool
|
||||
Parameters:
|
||||
- value: (required) The input value (type: string)
|
||||
Usage:
|
||||
<simple>
|
||||
<value>value value here</value>
|
||||
</simple>
|
||||
|
||||
## cached
|
||||
Description: Cached tool
|
||||
Parameters:
|
||||
Usage:
|
||||
<cached>
|
||||
</cached>
|
||||
|
||||
## legacy
|
||||
Description: Legacy tool using args
|
||||
Parameters:
|
||||
- input: (required) The input string (type: string)
|
||||
Usage:
|
||||
<legacy>
|
||||
<input>input value here</input>
|
||||
</legacy>
|
||||
|
||||
## multi_toolA
|
||||
Description: Tool A
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolA>
|
||||
</multi_toolA>
|
||||
|
||||
## multi_toolB
|
||||
Description: Tool B
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolB>
|
||||
</multi_toolB>
|
||||
|
||||
## mixed_validTool
|
||||
Description: Valid
|
||||
Parameters:
|
||||
Usage:
|
||||
<mixed_validTool>
|
||||
</mixed_validTool>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for cached tool 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## cached
|
||||
Description: Cached tool
|
||||
Parameters:
|
||||
Usage:
|
||||
<cached>
|
||||
</cached>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for legacy tool (using args) 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## legacy
|
||||
Description: Legacy tool using args
|
||||
Parameters:
|
||||
- input: (required) The input string (type: string)
|
||||
Usage:
|
||||
<legacy>
|
||||
<input>input value here</input>
|
||||
</legacy>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for mixed export tool 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## mixed_validTool
|
||||
Description: Valid
|
||||
Parameters:
|
||||
Usage:
|
||||
<mixed_validTool>
|
||||
</mixed_validTool>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for multi export tools 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## multi_toolA
|
||||
Description: Tool A
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolA>
|
||||
</multi_toolA>
|
||||
|
||||
## multi_toolB
|
||||
Description: Tool B
|
||||
Parameters:
|
||||
Usage:
|
||||
<multi_toolB>
|
||||
</multi_toolB>"
|
||||
`;
|
||||
|
||||
exports[`XML Protocol snapshots > should generate correct XML description for simple tool 1`] = `
|
||||
"# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
## simple
|
||||
Description: Simple tool
|
||||
Parameters:
|
||||
- value: (required) The input value (type: string)
|
||||
Usage:
|
||||
<simple>
|
||||
<value>value value here</value>
|
||||
</simple>"
|
||||
`;
|
||||
146
packages/core/src/custom-tools/__tests__/__snapshots__/serialize.spec.ts.snap
generated
Normal file
146
packages/core/src/custom-tools/__tests__/__snapshots__/serialize.spec.ts.snap
generated
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Serialization snapshots > should correctly serialize all fixtures 1`] = `
|
||||
[
|
||||
{
|
||||
"description": "Simple tool",
|
||||
"name": "simple",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "The input value",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"value",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
{
|
||||
"description": "Cached tool",
|
||||
"name": "cached",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
{
|
||||
"description": "Legacy tool using args",
|
||||
"name": "legacy",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"input": {
|
||||
"description": "The input string",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
{
|
||||
"description": "Tool A",
|
||||
"name": "multi_toolA",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
{
|
||||
"description": "Tool B",
|
||||
"name": "multi_toolB",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
{
|
||||
"description": "Valid",
|
||||
"name": "mixed_validTool",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Serialization snapshots > should correctly serialize cached tool 1`] = `
|
||||
{
|
||||
"description": "Cached tool",
|
||||
"name": "cached",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {},
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Serialization snapshots > should correctly serialize legacy tool (using args) 1`] = `
|
||||
{
|
||||
"description": "Legacy tool using args",
|
||||
"name": "legacy",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"input": {
|
||||
"description": "The input string",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Serialization snapshots > should correctly serialize simple tool 1`] = `
|
||||
{
|
||||
"description": "Simple tool",
|
||||
"name": "simple",
|
||||
"parameters": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"value": {
|
||||
"description": "The input value",
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": [
|
||||
"value",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"source": undefined,
|
||||
}
|
||||
`;
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
// pnpm --filter @roo-code/core test src/custom-tools/__tests__/custom-tool-registry.spec.ts
|
||||
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
|
||||
import { type CustomToolDefinition, parametersSchema as z } from "@roo-code/types"
|
||||
|
||||
import { CustomToolRegistry } from "../custom-tool-registry.js"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const TEST_FIXTURES_DIR = path.join(__dirname, "fixtures")
|
||||
const TEST_FIXTURES_OVERRIDE_DIR = path.join(__dirname, "fixtures-override")
|
||||
|
||||
describe("CustomToolRegistry", () => {
|
||||
let registry: CustomToolRegistry
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new CustomToolRegistry()
|
||||
})
|
||||
|
||||
describe("validation", () => {
|
||||
it("should accept a valid tool definition", () => {
|
||||
const validTool = {
|
||||
name: "valid_tool",
|
||||
description: "A valid tool",
|
||||
parameters: z.object({ name: z.string() }),
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(validTool)).not.toThrow()
|
||||
expect(registry.has("valid_tool")).toBe(true)
|
||||
})
|
||||
|
||||
it("should reject empty description", () => {
|
||||
const invalidTool = {
|
||||
name: "invalid_tool",
|
||||
description: "",
|
||||
parameters: z.object({}),
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(invalidTool as CustomToolDefinition)).toThrow(/Invalid tool definition/)
|
||||
})
|
||||
|
||||
it("should reject non-Zod parameters", () => {
|
||||
const invalidTool = {
|
||||
name: "bad_params_tool",
|
||||
description: "Tool with bad params",
|
||||
parameters: { foo: "bar" },
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(invalidTool as unknown as CustomToolDefinition)).toThrow(
|
||||
/Invalid tool definition/,
|
||||
)
|
||||
})
|
||||
|
||||
it("should allow missing parameters", () => {
|
||||
const toolWithoutParams = {
|
||||
name: "no_params_tool",
|
||||
description: "Tool without parameters",
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(toolWithoutParams)).not.toThrow()
|
||||
expect(registry.has("no_params_tool")).toBe(true)
|
||||
})
|
||||
|
||||
it("should reject empty name", () => {
|
||||
const invalidTool = {
|
||||
name: "",
|
||||
description: "Tool with empty name",
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(invalidTool as CustomToolDefinition)).toThrow(/Invalid tool definition/)
|
||||
})
|
||||
|
||||
it("should reject missing name", () => {
|
||||
const invalidTool = {
|
||||
description: "Tool without name",
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(invalidTool as unknown as CustomToolDefinition)).toThrow(
|
||||
/Invalid tool definition/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("register", () => {
|
||||
it("should register a valid tool", () => {
|
||||
const tool: CustomToolDefinition = {
|
||||
name: "test_tool",
|
||||
description: "Test tool",
|
||||
parameters: z.object({ input: z.string() }),
|
||||
execute: async (args: { input: string }) => `Processed: ${args.input}`,
|
||||
}
|
||||
|
||||
registry.register(tool)
|
||||
|
||||
expect(registry.has("test_tool")).toBe(true)
|
||||
expect(registry.size).toBe(1)
|
||||
})
|
||||
|
||||
it("should throw for invalid tool definition", () => {
|
||||
const invalidTool = {
|
||||
name: "bad_tool",
|
||||
description: "",
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(() => registry.register(invalidTool as CustomToolDefinition)).toThrow(/Invalid tool definition/)
|
||||
})
|
||||
|
||||
it("should overwrite existing tool with same id", () => {
|
||||
const tool1: CustomToolDefinition = {
|
||||
name: "tool",
|
||||
description: "First version",
|
||||
execute: async () => "v1",
|
||||
}
|
||||
|
||||
const tool2: CustomToolDefinition = {
|
||||
name: "tool",
|
||||
description: "Second version",
|
||||
execute: async () => "v2",
|
||||
}
|
||||
|
||||
registry.register(tool1)
|
||||
registry.register(tool2)
|
||||
|
||||
expect(registry.size).toBe(1)
|
||||
expect(registry.get("tool")?.description).toBe("Second version")
|
||||
})
|
||||
})
|
||||
|
||||
describe("unregister", () => {
|
||||
it("should remove a registered tool", () => {
|
||||
registry.register({
|
||||
name: "tool",
|
||||
description: "Test",
|
||||
execute: async () => "result",
|
||||
})
|
||||
|
||||
const result = registry.unregister("tool")
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(registry.has("tool")).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false for non-existent tool", () => {
|
||||
const result = registry.unregister("nonexistent")
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("get", () => {
|
||||
it("should return registered tool", () => {
|
||||
registry.register({
|
||||
name: "my_tool",
|
||||
description: "My tool",
|
||||
execute: async () => "result",
|
||||
})
|
||||
|
||||
const tool = registry.get("my_tool")
|
||||
|
||||
expect(tool).toBeDefined()
|
||||
expect(tool?.name).toBe("my_tool")
|
||||
expect(tool?.description).toBe("My tool")
|
||||
})
|
||||
|
||||
it("should return undefined for non-existent tool", () => {
|
||||
expect(registry.get("nonexistent")).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("list", () => {
|
||||
it("should return all tool IDs", () => {
|
||||
registry.register({ name: "tool_a", description: "A", execute: async () => "a" })
|
||||
registry.register({ name: "tool_b", description: "B", execute: async () => "b" })
|
||||
registry.register({ name: "tool_c", description: "C", execute: async () => "c" })
|
||||
|
||||
const ids = registry.list()
|
||||
|
||||
expect(ids).toHaveLength(3)
|
||||
expect(ids).toContain("tool_a")
|
||||
expect(ids).toContain("tool_b")
|
||||
expect(ids).toContain("tool_c")
|
||||
})
|
||||
|
||||
it("should return empty array when no tools registered", () => {
|
||||
expect(registry.list()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAll", () => {
|
||||
it("should return all tools as array", () => {
|
||||
registry.register({ name: "tool1", description: "Tool 1", execute: async () => "1" })
|
||||
registry.register({ name: "tool2", description: "Tool 2", execute: async () => "2" })
|
||||
|
||||
const all = registry.getAll()
|
||||
|
||||
expect(all).toHaveLength(2)
|
||||
expect(all.find((t) => t.name === "tool1")?.description).toBe("Tool 1")
|
||||
expect(all.find((t) => t.name === "tool2")?.description).toBe("Tool 2")
|
||||
})
|
||||
})
|
||||
|
||||
describe("clear", () => {
|
||||
it("should remove all registered tools", () => {
|
||||
registry.register({ name: "tool1", description: "1", execute: async () => "1" })
|
||||
registry.register({ name: "tool2", description: "2", execute: async () => "2" })
|
||||
|
||||
expect(registry.size).toBe(2)
|
||||
|
||||
registry.clear()
|
||||
|
||||
expect(registry.size).toBe(0)
|
||||
expect(registry.list()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe.sequential("loadFromDirectory", () => {
|
||||
it("should load tools from TypeScript files", async () => {
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
expect(result.loaded).toContain("simple")
|
||||
expect(registry.has("simple")).toBe(true)
|
||||
}, 60000)
|
||||
|
||||
it("should handle named exports", async () => {
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
expect(result.loaded).toContain("multi_toolA")
|
||||
expect(result.loaded).toContain("multi_toolB")
|
||||
}, 30000)
|
||||
|
||||
it("should report validation failures", async () => {
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
const invalidFailure = result.failed.find((f) => f.file === "invalid.ts")
|
||||
expect(invalidFailure).toBeDefined()
|
||||
expect(invalidFailure?.error).toContain("Invalid tool definition")
|
||||
}, 30000)
|
||||
|
||||
it("should return empty results for non-existent directory", async () => {
|
||||
const result = await registry.loadFromDirectory("/nonexistent/path")
|
||||
|
||||
expect(result.loaded).toHaveLength(0)
|
||||
expect(result.failed).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should skip non-tool exports silently", async () => {
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
expect(result.loaded).toContain("mixed_validTool")
|
||||
// The non-tool exports should not appear in loaded or failed.
|
||||
expect(result.loaded).not.toContain("mixed_someString")
|
||||
expect(result.loaded).not.toContain("mixed_someNumber")
|
||||
expect(result.loaded).not.toContain("mixed_someObject")
|
||||
}, 30000)
|
||||
|
||||
it("should support args as alias for parameters", async () => {
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
expect(result.loaded).toContain("legacy")
|
||||
|
||||
const tool = registry.get("legacy")
|
||||
expect(tool?.parameters).toBeDefined()
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
describe.sequential("clearCache", () => {
|
||||
it("should clear the TypeScript compilation cache", async () => {
|
||||
await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
registry.clearCache()
|
||||
|
||||
// Should be able to load again without issues.
|
||||
registry.clear()
|
||||
const result = await registry.loadFromDirectory(TEST_FIXTURES_DIR)
|
||||
|
||||
expect(result.loaded).toContain("cached")
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
describe.sequential("loadFromDirectories", () => {
|
||||
it("should load tools from multiple directories", async () => {
|
||||
const result = await registry.loadFromDirectories([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])
|
||||
|
||||
// Should load tools from both directories.
|
||||
expect(result.loaded).toContain("simple") // From both directories (override wins).
|
||||
expect(result.loaded).toContain("unique_override") // Only in override directory.
|
||||
expect(result.loaded).toContain("multi_toolA") // Only in fixtures directory.
|
||||
}, 60000)
|
||||
|
||||
it("should allow later directories to override earlier ones", async () => {
|
||||
await registry.loadFromDirectories([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])
|
||||
|
||||
// The simple tool should have the overridden description.
|
||||
const simpleTool = registry.get("simple")
|
||||
expect(simpleTool).toBeDefined()
|
||||
expect(simpleTool?.description).toBe("Simple tool - OVERRIDDEN")
|
||||
}, 60000)
|
||||
|
||||
it("should preserve order: first directory loaded first, second overrides", async () => {
|
||||
// Load in reverse order: override first, then fixtures.
|
||||
await registry.loadFromDirectories([TEST_FIXTURES_OVERRIDE_DIR, TEST_FIXTURES_DIR])
|
||||
|
||||
// Now the original fixtures directory should win.
|
||||
const simpleTool = registry.get("simple")
|
||||
expect(simpleTool).toBeDefined()
|
||||
expect(simpleTool?.description).toBe("Simple tool") // Original wins when loaded second.
|
||||
}, 60000)
|
||||
|
||||
it("should handle non-existent directories in the array", async () => {
|
||||
const result = await registry.loadFromDirectories([
|
||||
"/nonexistent/path",
|
||||
TEST_FIXTURES_DIR,
|
||||
"/another/nonexistent",
|
||||
])
|
||||
|
||||
// Should still load from the existing directory.
|
||||
expect(result.loaded).toContain("simple")
|
||||
expect(result.failed).toHaveLength(1) // Only the invalid.ts from fixtures.
|
||||
}, 60000)
|
||||
|
||||
it("should handle empty array", async () => {
|
||||
const result = await registry.loadFromDirectories([])
|
||||
|
||||
expect(result.loaded).toHaveLength(0)
|
||||
expect(result.failed).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should combine results from all directories", async () => {
|
||||
const result = await registry.loadFromDirectories([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])
|
||||
|
||||
// Loaded should include tools from both (with duplicates since simple is loaded twice).
|
||||
// The "simple" tool is loaded from both directories.
|
||||
const simpleCount = result.loaded.filter((name) => name === "simple").length
|
||||
expect(simpleCount).toBe(2) // Listed twice in loaded results.
|
||||
}, 60000)
|
||||
})
|
||||
|
||||
describe.sequential("loadFromDirectoriesIfStale", () => {
|
||||
it("should load tools from multiple directories when stale", async () => {
|
||||
const result = await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])
|
||||
|
||||
expect(result.loaded).toContain("simple")
|
||||
expect(result.loaded).toContain("unique_override")
|
||||
}, 60000)
|
||||
|
||||
it("should not reload if directories are not stale", async () => {
|
||||
// First load.
|
||||
await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR])
|
||||
|
||||
// Clear tools but keep staleness tracking.
|
||||
// (firstLoadSize is captured to document that tools were loaded, then cleared).
|
||||
const _firstLoadSize = registry.size
|
||||
registry.clear()
|
||||
|
||||
// Second load - should return cached tool names without reloading.
|
||||
const result = await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR])
|
||||
|
||||
// Registry was cleared, not stale so no reload.
|
||||
expect(result.loaded).toEqual([])
|
||||
}, 30000)
|
||||
|
||||
it("should handle mixed stale and non-stale directories", async () => {
|
||||
// Load from fixtures first.
|
||||
await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR])
|
||||
|
||||
// Load from both - fixtures is not stale, override is new (stale).
|
||||
const result = await registry.loadFromDirectoriesIfStale([TEST_FIXTURES_DIR, TEST_FIXTURES_OVERRIDE_DIR])
|
||||
|
||||
// Override directory tools should be loaded (it's stale/new).
|
||||
expect(result.loaded).toContain("simple") // From override (stale).
|
||||
expect(result.loaded).toContain("unique_override") // From override (stale).
|
||||
}, 60000)
|
||||
})
|
||||
})
|
||||
253
packages/core/src/custom-tools/__tests__/esbuild-runner.spec.ts
Normal file
253
packages/core/src/custom-tools/__tests__/esbuild-runner.spec.ts
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import fs from "fs"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
|
||||
import { getEsbuildScriptPath, runEsbuild, NODE_BUILTIN_MODULES, COMMONJS_REQUIRE_BANNER } from "../esbuild-runner.js"
|
||||
|
||||
describe("getEsbuildScriptPath", () => {
|
||||
it("should find esbuild-wasm script in node_modules in development", () => {
|
||||
const scriptPath = getEsbuildScriptPath()
|
||||
|
||||
// Should find the script.
|
||||
expect(typeof scriptPath).toBe("string")
|
||||
expect(scriptPath.length).toBeGreaterThan(0)
|
||||
|
||||
// The script should exist.
|
||||
expect(fs.existsSync(scriptPath)).toBe(true)
|
||||
|
||||
// Should be the esbuild script (not a binary).
|
||||
expect(scriptPath).toMatch(/esbuild$/)
|
||||
})
|
||||
|
||||
it("should prefer production path when extensionPath is provided and script exists", () => {
|
||||
// Create a temporary directory with a fake script.
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "esbuild-runner-test-"))
|
||||
const binDir = path.join(tempDir, "dist", "bin")
|
||||
fs.mkdirSync(binDir, { recursive: true })
|
||||
|
||||
const fakeScriptPath = path.join(binDir, "esbuild")
|
||||
fs.writeFileSync(fakeScriptPath, "#!/usr/bin/env node\nconsole.log('fake esbuild')")
|
||||
|
||||
try {
|
||||
const result = getEsbuildScriptPath(tempDir)
|
||||
expect(result).toBe(fakeScriptPath)
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("should fall back to node_modules when production script does not exist", () => {
|
||||
// Pass a non-existent extension path.
|
||||
const result = getEsbuildScriptPath("/nonexistent/extension/path")
|
||||
|
||||
// Should fall back to development path.
|
||||
expect(typeof result).toBe("string")
|
||||
expect(result.length).toBeGreaterThan(0)
|
||||
expect(fs.existsSync(result)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("runEsbuild", () => {
|
||||
let tempDir: string
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "esbuild-runner-test-"))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("should compile a TypeScript file to ESM", async () => {
|
||||
// Create a simple TypeScript file.
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(
|
||||
inputFile,
|
||||
`
|
||||
export const greeting = "Hello, World!"
|
||||
export function add(a: number, b: number): number {
|
||||
return a + b
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await runEsbuild({
|
||||
entryPoint: inputFile,
|
||||
outfile: outputFile,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node18",
|
||||
bundle: true,
|
||||
})
|
||||
|
||||
// Verify output file exists.
|
||||
expect(fs.existsSync(outputFile)).toBe(true)
|
||||
|
||||
// Verify output content is valid JavaScript.
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
expect(outputContent).toContain("Hello, World!")
|
||||
expect(outputContent).toContain("add")
|
||||
}, 30000)
|
||||
|
||||
it("should generate inline source maps when specified", async () => {
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(inputFile, `export const value = 42`)
|
||||
|
||||
await runEsbuild({ entryPoint: inputFile, outfile: outputFile, format: "esm", sourcemap: "inline" })
|
||||
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
expect(outputContent).toContain("sourceMappingURL=data:")
|
||||
}, 30000)
|
||||
|
||||
it("should throw an error for invalid TypeScript", async () => {
|
||||
const inputFile = path.join(tempDir, "invalid.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
// Write syntactically invalid TypeScript.
|
||||
fs.writeFileSync(inputFile, `export const value = {{{ invalid syntax`)
|
||||
|
||||
await expect(runEsbuild({ entryPoint: inputFile, outfile: outputFile, format: "esm" })).rejects.toThrow()
|
||||
}, 30000)
|
||||
|
||||
it("should throw an error for non-existent file", async () => {
|
||||
const nonExistentFile = path.join(tempDir, "does-not-exist.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
await expect(runEsbuild({ entryPoint: nonExistentFile, outfile: outputFile, format: "esm" })).rejects.toThrow()
|
||||
}, 30000)
|
||||
|
||||
it("should bundle dependencies when bundle option is true", async () => {
|
||||
// Create two files where one imports the other.
|
||||
const libFile = path.join(tempDir, "lib.ts")
|
||||
const mainFile = path.join(tempDir, "main.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(libFile, `export const PI = 3.14159`)
|
||||
fs.writeFileSync(
|
||||
mainFile,
|
||||
`
|
||||
import { PI } from "./lib.js"
|
||||
export const circumference = (r: number) => 2 * PI * r
|
||||
`,
|
||||
)
|
||||
|
||||
await runEsbuild({ entryPoint: mainFile, outfile: outputFile, format: "esm", bundle: true })
|
||||
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
// The PI constant should be bundled inline.
|
||||
expect(outputContent).toContain("3.14159")
|
||||
}, 30000)
|
||||
|
||||
it("should respect platform option", async () => {
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(inputFile, `export const value = process.env.NODE_ENV`)
|
||||
|
||||
await runEsbuild({ entryPoint: inputFile, outfile: outputFile, format: "esm", platform: "node" })
|
||||
|
||||
// File should be created successfully.
|
||||
expect(fs.existsSync(outputFile)).toBe(true)
|
||||
}, 30000)
|
||||
|
||||
it("should keep external modules as imports instead of bundling", async () => {
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
// Write code that imports fs (a Node.js built-in).
|
||||
fs.writeFileSync(
|
||||
inputFile,
|
||||
`
|
||||
import fs from "fs"
|
||||
export function fileExists(p: string): boolean {
|
||||
return fs.existsSync(p)
|
||||
}
|
||||
`,
|
||||
)
|
||||
|
||||
await runEsbuild({
|
||||
entryPoint: inputFile,
|
||||
outfile: outputFile,
|
||||
format: "esm",
|
||||
bundle: true,
|
||||
external: ["fs"],
|
||||
})
|
||||
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
// fs should remain as an import, not bundled.
|
||||
expect(outputContent).toMatch(/import.*from\s*["']fs["']/)
|
||||
}, 30000)
|
||||
|
||||
it("should add banner code when specified", async () => {
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(inputFile, `export const greeting = "Hello"`)
|
||||
|
||||
const customBanner = "// This is a custom banner comment"
|
||||
await runEsbuild({
|
||||
entryPoint: inputFile,
|
||||
outfile: outputFile,
|
||||
format: "esm",
|
||||
banner: customBanner,
|
||||
})
|
||||
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
// Banner should be at the start of the file.
|
||||
expect(outputContent.startsWith(customBanner)).toBe(true)
|
||||
}, 30000)
|
||||
|
||||
it("should add CommonJS require shim banner for ESM bundles", async () => {
|
||||
const inputFile = path.join(tempDir, "input.ts")
|
||||
const outputFile = path.join(tempDir, "output.mjs")
|
||||
|
||||
fs.writeFileSync(inputFile, `export const value = 42`)
|
||||
|
||||
await runEsbuild({
|
||||
entryPoint: inputFile,
|
||||
outfile: outputFile,
|
||||
format: "esm",
|
||||
banner: COMMONJS_REQUIRE_BANNER,
|
||||
})
|
||||
|
||||
const outputContent = fs.readFileSync(outputFile, "utf-8")
|
||||
// Should contain the createRequire shim.
|
||||
expect(outputContent).toContain("createRequire")
|
||||
expect(outputContent).toContain("import.meta.url")
|
||||
}, 30000)
|
||||
})
|
||||
|
||||
describe("NODE_BUILTIN_MODULES", () => {
|
||||
it("should include common Node.js built-in modules", () => {
|
||||
expect(NODE_BUILTIN_MODULES).toContain("fs")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("path")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("crypto")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("http")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("https")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("os")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("child_process")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("stream")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("util")
|
||||
expect(NODE_BUILTIN_MODULES).toContain("events")
|
||||
})
|
||||
|
||||
it("should be an array of strings", () => {
|
||||
expect(Array.isArray(NODE_BUILTIN_MODULES)).toBe(true)
|
||||
expect(NODE_BUILTIN_MODULES.every((m) => typeof m === "string")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("COMMONJS_REQUIRE_BANNER", () => {
|
||||
it("should provide createRequire shim", () => {
|
||||
expect(COMMONJS_REQUIRE_BANNER).toContain("createRequire")
|
||||
expect(COMMONJS_REQUIRE_BANNER).toContain("import.meta.url")
|
||||
})
|
||||
|
||||
it("should define require variable", () => {
|
||||
expect(COMMONJS_REQUIRE_BANNER).toMatch(/var require\s*=/)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
// This tool has the same name as the one in fixtures/ to test override behavior.
|
||||
export default defineCustomTool({
|
||||
name: "simple",
|
||||
description: "Simple tool - OVERRIDDEN",
|
||||
parameters: parametersSchema.object({ value: parametersSchema.string().describe("The input value") }),
|
||||
async execute(args: { value: string }) {
|
||||
return "Overridden Result: " + args.value
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
// This tool only exists in fixtures-override/ to test combined loading.
|
||||
export default defineCustomTool({
|
||||
name: "unique_override",
|
||||
description: "A unique tool only in override directory",
|
||||
parameters: parametersSchema.object({ input: parametersSchema.string().describe("The input") }),
|
||||
async execute(args: { input: string }) {
|
||||
return "Unique: " + args.input
|
||||
},
|
||||
})
|
||||
10
packages/core/src/custom-tools/__tests__/fixtures/cached.ts
Normal file
10
packages/core/src/custom-tools/__tests__/fixtures/cached.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
export default defineCustomTool({
|
||||
name: "cached",
|
||||
description: "Cached tool",
|
||||
parameters: parametersSchema.object({}),
|
||||
async execute() {
|
||||
return "cached"
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
description: "", // Invalid: empty description.
|
||||
execute: async () => "result",
|
||||
}
|
||||
10
packages/core/src/custom-tools/__tests__/fixtures/legacy.ts
Normal file
10
packages/core/src/custom-tools/__tests__/fixtures/legacy.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
export default defineCustomTool({
|
||||
name: "legacy",
|
||||
description: "Legacy tool using args",
|
||||
parameters: parametersSchema.object({ input: parametersSchema.string().describe("The input string") }),
|
||||
async execute(args: { input: string }) {
|
||||
return args.input
|
||||
},
|
||||
})
|
||||
16
packages/core/src/custom-tools/__tests__/fixtures/mixed.ts
Normal file
16
packages/core/src/custom-tools/__tests__/fixtures/mixed.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
// This is a valid tool.
|
||||
export const validTool = defineCustomTool({
|
||||
name: "mixed_validTool",
|
||||
description: "Valid",
|
||||
parameters: parametersSchema.object({}),
|
||||
async execute() {
|
||||
return "valid"
|
||||
},
|
||||
})
|
||||
|
||||
// These should be silently skipped.
|
||||
export const someString = "not a tool"
|
||||
export const someNumber = 42
|
||||
export const someObject = { foo: "bar" }
|
||||
19
packages/core/src/custom-tools/__tests__/fixtures/multi.ts
Normal file
19
packages/core/src/custom-tools/__tests__/fixtures/multi.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
export const toolA = defineCustomTool({
|
||||
name: "multi_toolA",
|
||||
description: "Tool A",
|
||||
parameters: parametersSchema.object({}),
|
||||
async execute() {
|
||||
return "A"
|
||||
},
|
||||
})
|
||||
|
||||
export const toolB = defineCustomTool({
|
||||
name: "multi_toolB",
|
||||
description: "Tool B",
|
||||
parameters: parametersSchema.object({}),
|
||||
async execute() {
|
||||
return "B"
|
||||
},
|
||||
})
|
||||
10
packages/core/src/custom-tools/__tests__/fixtures/simple.ts
Normal file
10
packages/core/src/custom-tools/__tests__/fixtures/simple.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { parametersSchema, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
export default defineCustomTool({
|
||||
name: "simple",
|
||||
description: "Simple tool",
|
||||
parameters: parametersSchema.object({ value: parametersSchema.string().describe("The input value") }),
|
||||
async execute(args: { value: string }) {
|
||||
return "Result: " + args.value
|
||||
},
|
||||
})
|
||||
250
packages/core/src/custom-tools/__tests__/format-native.spec.ts
Normal file
250
packages/core/src/custom-tools/__tests__/format-native.spec.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
// pnpm --filter @roo-code/core test src/custom-tools/__tests__/format-native.spec.ts
|
||||
|
||||
import { type SerializedCustomToolDefinition, parametersSchema as z, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
import { serializeCustomTool, serializeCustomTools } from "../serialize.js"
|
||||
import { formatNative } from "../format-native.js"
|
||||
|
||||
import simpleTool from "./fixtures/simple.js"
|
||||
import cachedTool from "./fixtures/cached.js"
|
||||
import legacyTool from "./fixtures/legacy.js"
|
||||
import { toolA, toolB } from "./fixtures/multi.js"
|
||||
import { validTool as mixedValidTool } from "./fixtures/mixed.js"
|
||||
|
||||
const fixtureTools = {
|
||||
simple: simpleTool,
|
||||
cached: cachedTool,
|
||||
legacy: legacyTool,
|
||||
multi_toolA: toolA,
|
||||
multi_toolB: toolB,
|
||||
mixed_validTool: mixedValidTool,
|
||||
}
|
||||
|
||||
describe("formatNative", () => {
|
||||
it("should convert a tool without args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "simple_tool",
|
||||
description: "A simple tool",
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatNative(serialized)
|
||||
|
||||
expect(result).toEqual({
|
||||
type: "function",
|
||||
function: {
|
||||
name: "simple_tool",
|
||||
description: "A simple tool",
|
||||
parameters: undefined,
|
||||
strict: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should convert a tool with required args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "greeter",
|
||||
description: "Greets a person",
|
||||
parameters: z.object({
|
||||
name: z.string().describe("Person's name"),
|
||||
}),
|
||||
async execute({ name }) {
|
||||
return `Hello, ${name}!`
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatNative(serialized)
|
||||
|
||||
expect(result.type).toBe("function")
|
||||
expect(result.function.name).toBe("greeter")
|
||||
expect(result.function.description).toBe("Greets a person")
|
||||
expect(result.function.parameters?.properties).toEqual({
|
||||
name: {
|
||||
type: "string",
|
||||
description: "Person's name",
|
||||
},
|
||||
})
|
||||
expect(result.function.parameters?.required).toEqual(["name"])
|
||||
expect(result.function.parameters?.additionalProperties).toBe(false)
|
||||
})
|
||||
|
||||
it("should convert a tool with optional args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "optional_tool",
|
||||
description: "Tool with optional args",
|
||||
parameters: z.object({
|
||||
format: z.string().optional().describe("Output format"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatNative(serialized)
|
||||
|
||||
expect(result.function.parameters?.required).toEqual([])
|
||||
expect(result.function.parameters?.properties).toEqual({
|
||||
format: {
|
||||
type: "string",
|
||||
description: "Output format",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should convert a tool with mixed required and optional args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "mixed_tool",
|
||||
description: "Tool with mixed args",
|
||||
parameters: z.object({
|
||||
input: z.string().describe("Required input"),
|
||||
options: z.object({}).optional().describe("Optional config"),
|
||||
count: z.number().describe("Also required"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatNative(serialized)
|
||||
|
||||
expect(result.function.parameters?.required).toEqual(["input", "count"])
|
||||
expect(result.function.parameters?.properties).toEqual({
|
||||
input: {
|
||||
type: "string",
|
||||
description: "Required input",
|
||||
},
|
||||
options: {
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
type: "object",
|
||||
description: "Optional config",
|
||||
},
|
||||
count: {
|
||||
type: "number",
|
||||
description: "Also required",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("should map type strings to JSON Schema types", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "typed_tool",
|
||||
description: "Tool with various types",
|
||||
parameters: z.object({
|
||||
str: z.string().describe("A string"),
|
||||
num: z.number().describe("A number"),
|
||||
bool: z.boolean().describe("A boolean"),
|
||||
obj: z.object({}).describe("An object"),
|
||||
arr: z.array(z.string()).describe("An array"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatNative(serialized)
|
||||
const props = result.function.parameters?.properties as
|
||||
| Record<string, { type: string; description?: string }>
|
||||
| undefined
|
||||
|
||||
expect(props?.str?.type).toBe("string")
|
||||
expect(props?.num?.type).toBe("number")
|
||||
expect(props?.bool?.type).toBe("boolean")
|
||||
expect(props?.obj?.type).toBe("object")
|
||||
expect(props?.arr?.type).toBe("array")
|
||||
})
|
||||
|
||||
it("should pass through raw parameters as-is", () => {
|
||||
// formatNative is a simple wrapper that passes through parameters unchanged
|
||||
const serialized = {
|
||||
name: "test_tool",
|
||||
description: "Tool with specific type",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
data: { type: "integer", description: "Integer type" },
|
||||
},
|
||||
},
|
||||
} as SerializedCustomToolDefinition
|
||||
|
||||
const result = formatNative(serialized)
|
||||
|
||||
expect(result.type).toBe("function")
|
||||
expect(result.function.name).toBe("test_tool")
|
||||
const props = result.function.parameters?.properties as Record<string, { type: string }> | undefined
|
||||
expect(props?.data?.type).toBe("integer")
|
||||
})
|
||||
|
||||
it("should convert multiple tools", () => {
|
||||
const tools = [
|
||||
defineCustomTool({
|
||||
name: "tool_a",
|
||||
description: "First tool",
|
||||
async execute() {
|
||||
return "a"
|
||||
},
|
||||
}),
|
||||
defineCustomTool({
|
||||
name: "tool_b",
|
||||
description: "Second tool",
|
||||
async execute() {
|
||||
return "b"
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
const serialized = serializeCustomTools(tools)
|
||||
const result = serialized.map(formatNative)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]?.function.name).toBe("tool_a")
|
||||
expect(result[1]?.function.name).toBe("tool_b")
|
||||
expect(result.every((t) => t.type === "function")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Native Protocol snapshots", () => {
|
||||
it("should generate correct native definition for simple tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.simple)
|
||||
const result = formatNative(serialized)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct native definition for cached tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.cached)
|
||||
const result = formatNative(serialized)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct native definition for legacy tool (using args)", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.legacy)
|
||||
const result = formatNative(serialized)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct native definitions for multi export tools", () => {
|
||||
const serializedA = serializeCustomTool(fixtureTools.multi_toolA)
|
||||
const serializedB = serializeCustomTool(fixtureTools.multi_toolB)
|
||||
const result = [serializedA, serializedB].map(formatNative)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct native definition for mixed export tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.mixed_validTool)
|
||||
const result = formatNative(serialized)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct native definitions for all fixtures combined", () => {
|
||||
const allSerialized = Object.values(fixtureTools).map(serializeCustomTool)
|
||||
const result = allSerialized.map(formatNative)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
192
packages/core/src/custom-tools/__tests__/format-xml.spec.ts
Normal file
192
packages/core/src/custom-tools/__tests__/format-xml.spec.ts
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
// pnpm --filter @roo-code/core test src/custom-tools/__tests__/format-xml.spec.ts
|
||||
|
||||
import { type SerializedCustomToolDefinition, parametersSchema as z, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
import { serializeCustomTool, serializeCustomTools } from "../serialize.js"
|
||||
import { formatXml } from "../format-xml.js"
|
||||
|
||||
import simpleTool from "./fixtures/simple.js"
|
||||
import cachedTool from "./fixtures/cached.js"
|
||||
import legacyTool from "./fixtures/legacy.js"
|
||||
import { toolA, toolB } from "./fixtures/multi.js"
|
||||
import { validTool as mixedValidTool } from "./fixtures/mixed.js"
|
||||
|
||||
const fixtureTools = {
|
||||
simple: simpleTool,
|
||||
cached: cachedTool,
|
||||
legacy: legacyTool,
|
||||
multi_toolA: toolA,
|
||||
multi_toolB: toolB,
|
||||
mixed_validTool: mixedValidTool,
|
||||
}
|
||||
|
||||
describe("formatXml", () => {
|
||||
it("should return empty string for empty tools array", () => {
|
||||
expect(formatXml([])).toBe("")
|
||||
})
|
||||
|
||||
it("should throw for undefined tools", () => {
|
||||
expect(() => formatXml(undefined as unknown as SerializedCustomToolDefinition[])).toThrow()
|
||||
})
|
||||
|
||||
it("should generate description for a single tool without args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "my_tool",
|
||||
description: "A simple tool that does something",
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatXml([serialized])
|
||||
|
||||
expect(result).toContain("# Custom Tools")
|
||||
expect(result).toContain("## my_tool")
|
||||
expect(result).toContain("Description: A simple tool that does something")
|
||||
expect(result).toContain("Parameters: None")
|
||||
expect(result).toContain("<my_tool>")
|
||||
expect(result).toContain("</my_tool>")
|
||||
})
|
||||
|
||||
it("should generate description for a tool with required args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "greeter",
|
||||
description: "Greets a person by name",
|
||||
parameters: z.object({
|
||||
name: z.string().describe("The name of the person to greet"),
|
||||
}),
|
||||
async execute({ name }) {
|
||||
return `Hello, ${name}!`
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatXml([serialized])
|
||||
|
||||
expect(result).toContain("## greeter")
|
||||
expect(result).toContain("Description: Greets a person by name")
|
||||
expect(result).toContain("Parameters:")
|
||||
expect(result).toContain("- name: (required) The name of the person to greet (type: string)")
|
||||
expect(result).toContain("<greeter>")
|
||||
expect(result).toContain("<name>name value here</name>")
|
||||
expect(result).toContain("</greeter>")
|
||||
})
|
||||
|
||||
it("should generate description for a tool with optional args", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "configurable_tool",
|
||||
description: "A tool with optional configuration",
|
||||
parameters: z.object({
|
||||
input: z.string().describe("The input to process"),
|
||||
format: z.string().optional().describe("Output format"),
|
||||
}),
|
||||
async execute({ input, format }) {
|
||||
return format ? `${input} (${format})` : input
|
||||
},
|
||||
})
|
||||
|
||||
const serialized = serializeCustomTool(tool)
|
||||
const result = formatXml([serialized])
|
||||
|
||||
expect(result).toContain("- input: (required) The input to process (type: string)")
|
||||
expect(result).toContain("- format: (optional) Output format (type: string)")
|
||||
expect(result).toContain("<input>input value here</input>")
|
||||
expect(result).toContain("<format>optional format value</format>")
|
||||
})
|
||||
|
||||
it("should generate descriptions for multiple tools", () => {
|
||||
const tools = [
|
||||
defineCustomTool({
|
||||
name: "tool_a",
|
||||
description: "First tool",
|
||||
async execute() {
|
||||
return "a"
|
||||
},
|
||||
}),
|
||||
defineCustomTool({
|
||||
name: "tool_b",
|
||||
description: "Second tool",
|
||||
parameters: z.object({
|
||||
value: z.number().describe("A numeric value"),
|
||||
}),
|
||||
async execute() {
|
||||
return "b"
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
const serialized = serializeCustomTools(tools)
|
||||
const result = formatXml(serialized)
|
||||
|
||||
expect(result).toContain("## tool_a")
|
||||
expect(result).toContain("Description: First tool")
|
||||
expect(result).toContain("## tool_b")
|
||||
expect(result).toContain("Description: Second tool")
|
||||
expect(result).toContain("- value: (required) A numeric value (type: number)")
|
||||
})
|
||||
|
||||
it("should treat args in required array as required", () => {
|
||||
// Using a raw SerializedToolDefinition to test the required behavior.
|
||||
const tools: SerializedCustomToolDefinition[] = [
|
||||
{
|
||||
name: "test_tool",
|
||||
description: "Test tool",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
data: {
|
||||
type: "object",
|
||||
description: "Some data",
|
||||
},
|
||||
},
|
||||
required: ["data"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const result = formatXml(tools)
|
||||
|
||||
expect(result).toContain("- data: (required) Some data (type: object)")
|
||||
expect(result).toContain("<data>data value here</data>")
|
||||
})
|
||||
})
|
||||
|
||||
describe("XML Protocol snapshots", () => {
|
||||
it("should generate correct XML description for simple tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.simple)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for cached tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.cached)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for legacy tool (using args)", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.legacy)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for multi export tools", () => {
|
||||
const serializedA = serializeCustomTool(fixtureTools.multi_toolA)
|
||||
const serializedB = serializeCustomTool(fixtureTools.multi_toolB)
|
||||
const result = formatXml([serializedA, serializedB])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for mixed export tool", () => {
|
||||
const serialized = serializeCustomTool(fixtureTools.mixed_validTool)
|
||||
const result = formatXml([serialized])
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should generate correct XML description for all fixtures combined", () => {
|
||||
const allSerialized = Object.values(fixtureTools).map(serializeCustomTool)
|
||||
const result = formatXml(allSerialized)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
225
packages/core/src/custom-tools/__tests__/serialize.spec.ts
Normal file
225
packages/core/src/custom-tools/__tests__/serialize.spec.ts
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
// pnpm --filter @roo-code/core test src/custom-tools/__tests__/serialize.spec.ts
|
||||
|
||||
import { parametersSchema as z, defineCustomTool } from "@roo-code/types"
|
||||
|
||||
import { serializeCustomTool, serializeCustomTools } from "../serialize.js"
|
||||
|
||||
import simpleTool from "./fixtures/simple.js"
|
||||
import cachedTool from "./fixtures/cached.js"
|
||||
import legacyTool from "./fixtures/legacy.js"
|
||||
import { toolA, toolB } from "./fixtures/multi.js"
|
||||
import { validTool as mixedValidTool } from "./fixtures/mixed.js"
|
||||
|
||||
const fixtureTools = {
|
||||
simple: simpleTool,
|
||||
cached: cachedTool,
|
||||
legacy: legacyTool,
|
||||
multi_toolA: toolA,
|
||||
multi_toolB: toolB,
|
||||
mixed_validTool: mixedValidTool,
|
||||
}
|
||||
|
||||
describe("serializeCustomTool", () => {
|
||||
it("should serialize a tool without parameters", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "simple_tool",
|
||||
description: "A simple tool that does something",
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result).toEqual({
|
||||
name: "simple_tool",
|
||||
description: "A simple tool that does something",
|
||||
})
|
||||
})
|
||||
|
||||
it("should serialize a tool with required string parameter", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "greeter",
|
||||
description: "Greets a person by name",
|
||||
parameters: z.object({
|
||||
name: z.string().describe("The name of the person to greet"),
|
||||
}),
|
||||
async execute({ name }) {
|
||||
return `Hello, ${name}!`
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result.name).toBe("greeter")
|
||||
expect(result.description).toBe("Greets a person by name")
|
||||
expect(result.parameters?.properties?.name).toEqual({
|
||||
type: "string",
|
||||
description: "The name of the person to greet",
|
||||
})
|
||||
expect(result.parameters?.required).toEqual(["name"])
|
||||
})
|
||||
|
||||
it("should serialize a tool with optional parameter", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "configurable_tool",
|
||||
description: "A tool with optional configuration",
|
||||
parameters: z.object({
|
||||
input: z.string().describe("The input to process"),
|
||||
format: z.string().optional().describe("Output format"),
|
||||
}),
|
||||
async execute({ input, format }) {
|
||||
return format ? `${input} (${format})` : input
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result.parameters?.properties?.input).toEqual({
|
||||
type: "string",
|
||||
description: "The input to process",
|
||||
})
|
||||
|
||||
expect(result.parameters?.properties?.format).toEqual({
|
||||
type: "string",
|
||||
description: "Output format",
|
||||
})
|
||||
|
||||
// Only required params should be in the required array
|
||||
expect(result.parameters?.required).toEqual(["input"])
|
||||
})
|
||||
|
||||
it("should serialize a tool with various types", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "typed_tool",
|
||||
description: "Tool with various types",
|
||||
parameters: z.object({
|
||||
str: z.string().describe("A string"),
|
||||
num: z.number().describe("A number"),
|
||||
bool: z.boolean().describe("A boolean"),
|
||||
obj: z.object({}).describe("An object"),
|
||||
arr: z.array(z.string()).describe("An array"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result.parameters?.properties?.str).toEqual({
|
||||
description: "A string",
|
||||
type: "string",
|
||||
})
|
||||
expect(result.parameters?.properties?.num).toEqual({
|
||||
description: "A number",
|
||||
type: "number",
|
||||
})
|
||||
expect(result.parameters?.properties?.bool).toEqual({
|
||||
description: "A boolean",
|
||||
type: "boolean",
|
||||
})
|
||||
expect(result.parameters?.properties?.obj).toEqual({
|
||||
additionalProperties: false,
|
||||
description: "An object",
|
||||
properties: {},
|
||||
type: "object",
|
||||
})
|
||||
expect(result.parameters?.properties?.arr).toEqual({
|
||||
description: "An array",
|
||||
items: { type: "string" },
|
||||
type: "array",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle nullable parameters as optional", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "nullable_tool",
|
||||
description: "Tool with nullable param",
|
||||
parameters: z.object({
|
||||
value: z.string().nullable().describe("A nullable value"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result.parameters?.required).toEqual(["value"])
|
||||
})
|
||||
|
||||
it("should handle default values as optional", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "default_tool",
|
||||
description: "Tool with default param",
|
||||
parameters: z.object({
|
||||
count: z.number().default(10).describe("A count with default"),
|
||||
}),
|
||||
async execute() {
|
||||
return "done"
|
||||
},
|
||||
})
|
||||
|
||||
const result = serializeCustomTool(tool)
|
||||
|
||||
expect(result.parameters?.required).toEqual(["count"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("serializeCustomTools", () => {
|
||||
it("should return empty array for empty tools array", () => {
|
||||
expect(serializeCustomTools([])).toEqual([])
|
||||
})
|
||||
|
||||
it("should serialize multiple tools", () => {
|
||||
const tools = [
|
||||
defineCustomTool({
|
||||
name: "tool_a",
|
||||
description: "First tool",
|
||||
async execute() {
|
||||
return "a"
|
||||
},
|
||||
}),
|
||||
defineCustomTool({
|
||||
name: "tool_b",
|
||||
description: "Second tool",
|
||||
parameters: z.object({
|
||||
value: z.number().describe("A numeric value"),
|
||||
}),
|
||||
async execute() {
|
||||
return "b"
|
||||
},
|
||||
}),
|
||||
]
|
||||
|
||||
const result = serializeCustomTools(tools)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0]?.name).toBe("tool_a")
|
||||
expect(result[1]?.name).toBe("tool_b")
|
||||
expect(result[1]?.parameters?.properties?.value).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Serialization snapshots", () => {
|
||||
it("should correctly serialize simple tool", () => {
|
||||
const result = serializeCustomTool(fixtureTools.simple)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should correctly serialize cached tool", () => {
|
||||
const result = serializeCustomTool(fixtureTools.cached)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should correctly serialize legacy tool (using args)", () => {
|
||||
const result = serializeCustomTool(fixtureTools.legacy)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("should correctly serialize all fixtures", () => {
|
||||
const result = Object.values(fixtureTools).map(serializeCustomTool)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
432
packages/core/src/custom-tools/custom-tool-registry.ts
Normal file
432
packages/core/src/custom-tools/custom-tool-registry.ts
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
/**
|
||||
* CustomToolRegistry - A reusable class for dynamically loading and managing TypeScript tools.
|
||||
*
|
||||
* Features:
|
||||
* - Dynamic TypeScript/JavaScript tool loading with esbuild transpilation.
|
||||
* - Runtime validation of tool definitions.
|
||||
* - Tool execution with context.
|
||||
* - JSON Schema generation for LLM integration.
|
||||
*/
|
||||
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { createHash } from "crypto"
|
||||
import os from "os"
|
||||
|
||||
import type { CustomToolDefinition, SerializedCustomToolDefinition, CustomToolParametersSchema } from "@roo-code/types"
|
||||
|
||||
import type { StoredCustomTool, LoadResult } from "./types.js"
|
||||
import { serializeCustomTool } from "./serialize.js"
|
||||
import { runEsbuild, NODE_BUILTIN_MODULES, COMMONJS_REQUIRE_BANNER } from "./esbuild-runner.js"
|
||||
|
||||
export interface RegistryOptions {
|
||||
/** Directory for caching compiled TypeScript files. */
|
||||
cacheDir?: string
|
||||
/** Additional paths for resolving node modules (useful for tools outside node_modules). */
|
||||
nodePaths?: string[]
|
||||
/** Path to the extension root directory (for finding bundled esbuild binary in production). */
|
||||
extensionPath?: string
|
||||
}
|
||||
|
||||
export class CustomToolRegistry {
|
||||
private tools = new Map<string, StoredCustomTool>()
|
||||
private tsCache = new Map<string, string>()
|
||||
private cacheDir: string
|
||||
private nodePaths: string[]
|
||||
private extensionPath?: string
|
||||
private lastLoaded: Map<string, number> = new Map()
|
||||
|
||||
constructor(options?: RegistryOptions) {
|
||||
this.cacheDir = options?.cacheDir ?? path.join(os.tmpdir(), "dynamic-tools-cache")
|
||||
// Default to current working directory's node_modules.
|
||||
this.nodePaths = options?.nodePaths ?? [path.join(process.cwd(), "node_modules")]
|
||||
this.extensionPath = options?.extensionPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all tools from a directory.
|
||||
* Supports both .ts and .js files.
|
||||
*
|
||||
* @param toolDir - Absolute path to the tools directory
|
||||
* @returns LoadResult with lists of loaded and failed tools
|
||||
*/
|
||||
async loadFromDirectory(toolDir: string): Promise<LoadResult> {
|
||||
const result: LoadResult = { loaded: [], failed: [] }
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(toolDir)) {
|
||||
return result
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(toolDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"))
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(toolDir, file)
|
||||
|
||||
try {
|
||||
console.log(`[CustomToolRegistry] importing tool from ${filePath}`)
|
||||
const mod = await this.import(filePath)
|
||||
|
||||
for (const [exportName, value] of Object.entries(mod)) {
|
||||
const def = this.validate(exportName, value)
|
||||
|
||||
if (!def) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.tools.set(def.name, { ...def, source: filePath })
|
||||
console.log(`[CustomToolRegistry] loaded tool ${def.name} from ${filePath}`)
|
||||
result.loaded.push(def.name)
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[CustomToolRegistry] import(${filePath}) failed: ${message}`)
|
||||
result.failed.push({ file, error: message })
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
console.error(`[CustomToolRegistry] loadFromDirectory(${toolDir}) failed: ${message}`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async loadFromDirectoryIfStale(toolDir: string): Promise<LoadResult> {
|
||||
if (!fs.existsSync(toolDir)) {
|
||||
return { loaded: [], failed: [] }
|
||||
}
|
||||
|
||||
const lastLoaded = this.lastLoaded.get(toolDir)
|
||||
const stat = fs.statSync(toolDir)
|
||||
const isStale = lastLoaded ? stat.mtimeMs > lastLoaded : true
|
||||
|
||||
if (isStale) {
|
||||
this.lastLoaded.set(toolDir, stat.mtimeMs)
|
||||
return this.loadFromDirectory(toolDir)
|
||||
}
|
||||
|
||||
return { loaded: this.list(), failed: [] }
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all tools from multiple directories.
|
||||
* Directories are processed in order, so later directories can override tools from earlier ones.
|
||||
* Supports both .ts and .js files.
|
||||
*
|
||||
* @param toolDirs - Array of absolute paths to tools directories
|
||||
* @returns LoadResult with lists of loaded and failed tools from all directories
|
||||
*/
|
||||
async loadFromDirectories(toolDirs: string[]): Promise<LoadResult> {
|
||||
const result: LoadResult = { loaded: [], failed: [] }
|
||||
|
||||
for (const toolDir of toolDirs) {
|
||||
const dirResult = await this.loadFromDirectory(toolDir)
|
||||
result.loaded.push(...dirResult.loaded)
|
||||
result.failed.push(...dirResult.failed)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all tools from multiple directories if any has become stale.
|
||||
* Directories are processed in order, so later directories can override tools from earlier ones.
|
||||
*
|
||||
* @param toolDirs - Array of absolute paths to tools directories
|
||||
* @returns LoadResult with lists of loaded and failed tools
|
||||
*/
|
||||
async loadFromDirectoriesIfStale(toolDirs: string[]): Promise<LoadResult> {
|
||||
const result: LoadResult = { loaded: [], failed: [] }
|
||||
|
||||
for (const toolDir of toolDirs) {
|
||||
const dirResult = await this.loadFromDirectoryIfStale(toolDir)
|
||||
result.loaded.push(...dirResult.loaded)
|
||||
result.failed.push(...dirResult.failed)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a tool directly (without loading from file).
|
||||
*/
|
||||
register(definition: CustomToolDefinition, source?: string): void {
|
||||
const { name: id } = definition
|
||||
const validated = this.validate(id, definition)
|
||||
|
||||
if (!validated) {
|
||||
throw new Error(`Invalid tool definition for '${id}'`)
|
||||
}
|
||||
|
||||
const storedTool: StoredCustomTool = source ? { ...validated, source } : validated
|
||||
this.tools.set(id, storedTool)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a tool by ID.
|
||||
*/
|
||||
unregister(id: string): boolean {
|
||||
return this.tools.delete(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a tool by ID.
|
||||
*/
|
||||
get(id: string): CustomToolDefinition | undefined {
|
||||
return this.tools.get(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tool exists.
|
||||
*/
|
||||
has(id: string): boolean {
|
||||
return this.tools.has(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tool IDs.
|
||||
*/
|
||||
list(): string[] {
|
||||
return Array.from(this.tools.keys())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tools.
|
||||
*/
|
||||
getAll(): CustomToolDefinition[] {
|
||||
return Array.from(this.tools.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered tools in the serialized format.
|
||||
*/
|
||||
getAllSerialized(): SerializedCustomToolDefinition[] {
|
||||
return this.getAll().map(serializeCustomTool)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of registered tools.
|
||||
*/
|
||||
get size(): number {
|
||||
return this.tools.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered tools.
|
||||
*/
|
||||
clear(): void {
|
||||
this.tools.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the extension path for finding bundled esbuild binary.
|
||||
* This should be called with context.extensionPath when the extension activates.
|
||||
*/
|
||||
setExtensionPath(extensionPath: string): void {
|
||||
this.extensionPath = extensionPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current extension path.
|
||||
*/
|
||||
getExtensionPath(): string | undefined {
|
||||
return this.extensionPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the TypeScript compilation cache (both in-memory and on disk).
|
||||
* This removes all tool-specific subdirectories and their contents.
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.tsCache.clear()
|
||||
|
||||
if (fs.existsSync(this.cacheDir)) {
|
||||
try {
|
||||
const entries = fs.readdirSync(this.cacheDir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(this.cacheDir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
// Remove tool-specific subdirectory and all its contents.
|
||||
fs.rmSync(entryPath, { recursive: true, force: true })
|
||||
} else if (entry.name.endsWith(".mjs")) {
|
||||
// Also clean up any legacy flat .mjs files from older cache format.
|
||||
fs.unlinkSync(entryPath)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[CustomToolRegistry] clearCache failed to clean disk cache: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically import a TypeScript or JavaScript file.
|
||||
* TypeScript files are transpiled on-the-fly using esbuild.
|
||||
*
|
||||
* For TypeScript files, esbuild bundles the code with these considerations:
|
||||
* - Node.js built-in modules (fs, path, etc.) are kept external
|
||||
* - npm packages are bundled with a CommonJS shim for require() compatibility
|
||||
* - The tool's local node_modules is included in the resolution path
|
||||
*/
|
||||
private async import(filePath: string): Promise<Record<string, CustomToolDefinition>> {
|
||||
const absolutePath = path.resolve(filePath)
|
||||
const ext = path.extname(absolutePath)
|
||||
|
||||
if (ext === ".js" || ext === ".mjs") {
|
||||
return import(`file://${absolutePath}`)
|
||||
}
|
||||
|
||||
const stat = fs.statSync(absolutePath)
|
||||
const cacheKey = `${absolutePath}:${stat.mtimeMs}`
|
||||
|
||||
// Check if we have a cached version in memory.
|
||||
if (this.tsCache.has(cacheKey)) {
|
||||
const cachedPath = this.tsCache.get(cacheKey)!
|
||||
return import(`file://${cachedPath}`)
|
||||
}
|
||||
|
||||
const hash = createHash("sha256").update(cacheKey).digest("hex").slice(0, 16)
|
||||
|
||||
// Use a tool-specific subdirectory to avoid .env file conflicts between tools.
|
||||
const toolCacheDir = path.join(this.cacheDir, hash)
|
||||
fs.mkdirSync(toolCacheDir, { recursive: true })
|
||||
|
||||
const tempFile = path.join(toolCacheDir, "bundle.mjs")
|
||||
|
||||
// Check if we have a cached version on disk (from a previous run/instance).
|
||||
if (fs.existsSync(tempFile)) {
|
||||
this.tsCache.set(cacheKey, tempFile)
|
||||
return import(`file://${tempFile}`)
|
||||
}
|
||||
|
||||
// Get the tool's directory to include its node_modules in resolution path.
|
||||
const toolDir = path.dirname(absolutePath)
|
||||
const toolNodeModules = path.join(toolDir, "node_modules")
|
||||
|
||||
// Combine default nodePaths with tool-specific node_modules.
|
||||
// Tool's node_modules takes priority (listed first).
|
||||
const nodePaths = fs.existsSync(toolNodeModules) ? [toolNodeModules, ...this.nodePaths] : this.nodePaths
|
||||
|
||||
// Bundle the TypeScript file with dependencies using esbuild CLI.
|
||||
// - Node.js built-ins are external (they can't be bundled and are always available)
|
||||
// - npm packages are bundled with CommonJS require() shim for compatibility
|
||||
await runEsbuild(
|
||||
{
|
||||
entryPoint: absolutePath,
|
||||
outfile: tempFile,
|
||||
format: "esm",
|
||||
platform: "node",
|
||||
target: "node18",
|
||||
bundle: true,
|
||||
sourcemap: "inline",
|
||||
packages: "bundle",
|
||||
nodePaths,
|
||||
external: NODE_BUILTIN_MODULES,
|
||||
banner: COMMONJS_REQUIRE_BANNER,
|
||||
},
|
||||
this.extensionPath,
|
||||
)
|
||||
|
||||
// Copy .env files from the tool's source directory to the tool-specific cache directory.
|
||||
// This allows tools that use dotenv with __dirname to find their .env files,
|
||||
// while ensuring different tools' .env files don't overwrite each other.
|
||||
this.copyEnvFiles(toolDir, toolCacheDir)
|
||||
|
||||
this.tsCache.set(cacheKey, tempFile)
|
||||
return import(`file://${tempFile}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy .env files from the tool's source directory to the tool-specific cache directory.
|
||||
* This allows tools that use dotenv with __dirname to find their .env files,
|
||||
* while ensuring different tools' .env files don't overwrite each other.
|
||||
*
|
||||
* @param toolDir - The directory containing the tool source files
|
||||
* @param destDir - The tool-specific cache directory to copy .env files to
|
||||
*/
|
||||
private copyEnvFiles(toolDir: string, destDir: string): void {
|
||||
try {
|
||||
const files = fs.readdirSync(toolDir)
|
||||
const envFiles = files.filter((f) => f === ".env" || f.startsWith(".env."))
|
||||
|
||||
for (const envFile of envFiles) {
|
||||
const srcPath = path.join(toolDir, envFile)
|
||||
const destPath = path.join(destDir, envFile)
|
||||
|
||||
// Only copy if source is a file (not a directory).
|
||||
const stat = fs.statSync(srcPath)
|
||||
if (stat.isFile()) {
|
||||
fs.copyFileSync(srcPath, destPath)
|
||||
console.log(`[CustomToolRegistry] copied ${envFile} to tool cache directory`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Non-fatal: log but don't fail if we can't copy env files.
|
||||
console.warn(
|
||||
`[CustomToolRegistry] failed to copy .env files: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is a Zod schema by looking for the _def property
|
||||
* which is present on all Zod types.
|
||||
*/
|
||||
private isParametersSchema(value: unknown): value is CustomToolParametersSchema {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === "object" &&
|
||||
"_def" in value &&
|
||||
typeof (value as Record<string, unknown>)._def === "object"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a tool definition and return a typed result.
|
||||
* Returns null for non-tool exports, throws for invalid tools.
|
||||
*/
|
||||
private validate(exportName: string, value: unknown): CustomToolDefinition | null {
|
||||
// Quick pre-check to filter out non-objects.
|
||||
if (!value || typeof value !== "object") {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if it looks like a tool (has execute function).
|
||||
if (!("execute" in value) || typeof (value as Record<string, unknown>).execute !== "function") {
|
||||
return null
|
||||
}
|
||||
|
||||
const obj = value as Record<string, unknown>
|
||||
const errors: string[] = []
|
||||
|
||||
// Validate name.
|
||||
if (typeof obj.name !== "string") {
|
||||
errors.push("name: Expected string")
|
||||
} else if (obj.name.length === 0) {
|
||||
errors.push("name: Tool must have a non-empty name")
|
||||
}
|
||||
|
||||
// Validate description.
|
||||
if (typeof obj.description !== "string") {
|
||||
errors.push("description: Expected string")
|
||||
} else if (obj.description.length === 0) {
|
||||
errors.push("description: Tool must have a non-empty description")
|
||||
}
|
||||
|
||||
// Validate parameters (optional).
|
||||
if (obj.parameters !== undefined && !this.isParametersSchema(obj.parameters)) {
|
||||
errors.push("parameters: parameters must be a Zod schema")
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Invalid tool definition for '${exportName}': ${errors.join(", ")}`)
|
||||
}
|
||||
|
||||
return value as CustomToolDefinition
|
||||
}
|
||||
}
|
||||
|
||||
export const customToolRegistry = new CustomToolRegistry()
|
||||
209
packages/core/src/custom-tools/esbuild-runner.ts
Normal file
209
packages/core/src/custom-tools/esbuild-runner.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
/**
|
||||
* esbuild-runner - Runs esbuild-wasm CLI to transpile TypeScript files.
|
||||
*
|
||||
* This module provides a way to run esbuild as a CLI process instead of using
|
||||
* the JavaScript API. This uses esbuild-wasm which is cross-platform and works
|
||||
* on all operating systems without needing native binaries.
|
||||
*
|
||||
* In production, the esbuild-wasm CLI script is bundled in dist/bin/.
|
||||
* In development, it falls back to using esbuild-wasm from node_modules.
|
||||
*/
|
||||
|
||||
import path from "path"
|
||||
import fs from "fs"
|
||||
import { builtinModules } from "module"
|
||||
import { fileURLToPath } from "url"
|
||||
import { execa } from "execa"
|
||||
|
||||
/**
|
||||
* Node.js built-in modules that should never be bundled.
|
||||
* These are always available in Node.js runtime and bundling them causes issues.
|
||||
*
|
||||
* Uses Node.js's authoritative list from `module.builtinModules` and adds
|
||||
* the `node:` prefixed versions for comprehensive coverage.
|
||||
*/
|
||||
export const NODE_BUILTIN_MODULES: readonly string[] = [...builtinModules, ...builtinModules.map((m) => `node:${m}`)]
|
||||
|
||||
/**
|
||||
* Banner code to add to bundled output.
|
||||
* This provides a CommonJS-compatible `require` function for ESM bundles,
|
||||
* which is needed when bundled npm packages use `require()` internally.
|
||||
*/
|
||||
export const COMMONJS_REQUIRE_BANNER = `import { createRequire as __roo_createRequire } from 'module';
|
||||
var require = __roo_createRequire(import.meta.url);`
|
||||
|
||||
// Get the directory where this module is located.
|
||||
function getModuleDir(): string | undefined {
|
||||
try {
|
||||
// In ESM context, import.meta.url is available.
|
||||
// In bundled CJS, this will throw or be undefined.
|
||||
if (typeof import.meta !== "undefined" && import.meta.url) {
|
||||
return path.dirname(fileURLToPath(import.meta.url))
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors, fall through to undefined.
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const moduleDir = getModuleDir()
|
||||
|
||||
export interface EsbuildOptions {
|
||||
/** Entry point file path (absolute) */
|
||||
entryPoint: string
|
||||
/** Output file path (absolute) */
|
||||
outfile: string
|
||||
/** Output format */
|
||||
format?: "esm" | "cjs" | "iife"
|
||||
/** Target platform */
|
||||
platform?: "node" | "browser" | "neutral"
|
||||
/** Target environment (e.g., "node18") */
|
||||
target?: string
|
||||
/** Bundle dependencies */
|
||||
bundle?: boolean
|
||||
/** Generate source maps */
|
||||
sourcemap?: boolean | "inline" | "external"
|
||||
/** How to handle packages: "bundle" includes them, "external" leaves them */
|
||||
packages?: "bundle" | "external"
|
||||
/** Additional paths for module resolution */
|
||||
nodePaths?: string[]
|
||||
/** Modules to exclude from bundling (resolved at runtime) */
|
||||
external?: readonly string[]
|
||||
/** JavaScript code to prepend to the output bundle */
|
||||
banner?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the esbuild-wasm CLI script by walking up the directory tree.
|
||||
* In pnpm monorepos, node_modules/esbuild-wasm is a symlink to the actual package,
|
||||
* so we don't need special pnpm handling.
|
||||
*/
|
||||
function findEsbuildWasmScript(startDir: string): string | null {
|
||||
const maxDepth = 10
|
||||
let currentDir = path.resolve(startDir)
|
||||
const root = path.parse(currentDir).root
|
||||
|
||||
for (let i = 0; i < maxDepth && currentDir !== root; i++) {
|
||||
// Check node_modules/esbuild-wasm/bin/esbuild at this level.
|
||||
const scriptPath = path.join(currentDir, "node_modules", "esbuild-wasm", "bin", "esbuild")
|
||||
|
||||
if (fs.existsSync(scriptPath)) {
|
||||
return scriptPath
|
||||
}
|
||||
|
||||
// Also check src/node_modules for monorepo where src is a workspace.
|
||||
const srcScriptPath = path.join(currentDir, "src", "node_modules", "esbuild-wasm", "bin", "esbuild")
|
||||
|
||||
if (fs.existsSync(srcScriptPath)) {
|
||||
return srcScriptPath
|
||||
}
|
||||
|
||||
currentDir = path.dirname(currentDir)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the esbuild CLI script.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. Production: Look in extension's dist/bin directory for bundled script.
|
||||
* 2. Development: Use esbuild-wasm from node_modules (relative to this module).
|
||||
* 3. Fallback: Try process.cwd() as last resort.
|
||||
*
|
||||
* @param extensionPath - Path to the extension's root directory (production)
|
||||
* @returns Path to the esbuild CLI script
|
||||
*/
|
||||
export function getEsbuildScriptPath(extensionPath?: string): string {
|
||||
// Production: look in extension's dist/bin directory.
|
||||
if (extensionPath) {
|
||||
const prodPath = path.join(extensionPath, "dist", "bin", "esbuild")
|
||||
|
||||
if (fs.existsSync(prodPath)) {
|
||||
return prodPath
|
||||
}
|
||||
}
|
||||
|
||||
// Development: use esbuild-wasm from node_modules relative to this module.
|
||||
// This works when running the extension in debug mode (if moduleDir is available).
|
||||
if (moduleDir) {
|
||||
const devPath = findEsbuildWasmScript(moduleDir)
|
||||
|
||||
if (devPath) {
|
||||
return devPath
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try from cwd (for tests and other contexts).
|
||||
const cwdPath = findEsbuildWasmScript(process.cwd())
|
||||
|
||||
if (cwdPath) {
|
||||
return cwdPath
|
||||
}
|
||||
|
||||
throw new Error("esbuild-wasm CLI not found. Ensure esbuild-wasm is installed.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Run esbuild CLI to bundle a TypeScript file.
|
||||
*
|
||||
* Uses esbuild-wasm which is cross-platform and runs via Node.js.
|
||||
*
|
||||
* @param options - Build options
|
||||
* @param extensionPath - Path to extension root (for finding bundled script)
|
||||
* @returns Promise that resolves when build completes
|
||||
* @throws Error if the build fails
|
||||
*/
|
||||
export async function runEsbuild(options: EsbuildOptions, extensionPath?: string): Promise<void> {
|
||||
const scriptPath = getEsbuildScriptPath(extensionPath)
|
||||
|
||||
const args: string[] = [
|
||||
scriptPath,
|
||||
options.entryPoint,
|
||||
`--outfile=${options.outfile}`,
|
||||
`--format=${options.format ?? "esm"}`,
|
||||
`--platform=${options.platform ?? "node"}`,
|
||||
`--target=${options.target ?? "node18"}`,
|
||||
]
|
||||
|
||||
if (options.bundle !== false) {
|
||||
args.push("--bundle")
|
||||
}
|
||||
|
||||
if (options.sourcemap) {
|
||||
args.push(options.sourcemap === true ? "--sourcemap" : `--sourcemap=${options.sourcemap}`)
|
||||
}
|
||||
|
||||
if (options.packages) {
|
||||
args.push(`--packages=${options.packages}`)
|
||||
}
|
||||
|
||||
// Add external modules - these won't be bundled and will be resolved at runtime.
|
||||
if (options.external && options.external.length > 0) {
|
||||
for (const ext of options.external) {
|
||||
args.push(`--external:${ext}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Add banner code (e.g., for CommonJS require shim in ESM bundles).
|
||||
if (options.banner) {
|
||||
args.push(`--banner:js=${options.banner}`)
|
||||
}
|
||||
|
||||
// Build environment with NODE_PATH for module resolution.
|
||||
const env: NodeJS.ProcessEnv = { ...process.env }
|
||||
|
||||
if (options.nodePaths && options.nodePaths.length > 0) {
|
||||
env.NODE_PATH = options.nodePaths.join(path.delimiter)
|
||||
}
|
||||
|
||||
try {
|
||||
await execa(process.execPath, args, { env, stdin: "ignore" })
|
||||
} catch (error) {
|
||||
const execaError = error as { stderr?: string; stdout?: string; exitCode?: number; message: string }
|
||||
const errorMessage = execaError.stderr || execaError.stdout || `esbuild exited with code ${execaError.exitCode}`
|
||||
throw new Error(`esbuild failed: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
23
packages/core/src/custom-tools/format-native.ts
Normal file
23
packages/core/src/custom-tools/format-native.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { OpenAI } from "openai"
|
||||
|
||||
import type { SerializedCustomToolDefinition } from "@roo-code/types"
|
||||
|
||||
export function formatNative(tool: SerializedCustomToolDefinition): OpenAI.Chat.ChatCompletionFunctionTool {
|
||||
// Create a shallow copy to avoid mutating the input object
|
||||
let parameters = tool.parameters
|
||||
|
||||
if (parameters) {
|
||||
// Create a new object with the modifications instead of mutating the original
|
||||
parameters = { ...parameters }
|
||||
|
||||
// We don't need the $schema property; none of the other tools specify it.
|
||||
delete parameters["$schema"]
|
||||
|
||||
// https://community.openai.com/t/on-the-function-calling-what-about-if-i-have-no-parameter-to-call/516876
|
||||
if (!parameters.required) {
|
||||
parameters.required = []
|
||||
}
|
||||
}
|
||||
|
||||
return { type: "function", function: { ...tool, strict: true, parameters } }
|
||||
}
|
||||
89
packages/core/src/custom-tools/format-xml.ts
Normal file
89
packages/core/src/custom-tools/format-xml.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import type { SerializedCustomToolDefinition, SerializedCustomToolParameters } from "@roo-code/types"
|
||||
|
||||
/**
|
||||
* Extract the type string from a parameter schema.
|
||||
* Handles both direct `type` property and `anyOf` schemas (used for nullable types).
|
||||
*/
|
||||
function getParameterType(parameter: SerializedCustomToolParameters): string {
|
||||
// Direct type property
|
||||
if (parameter.type) {
|
||||
return String(parameter.type)
|
||||
}
|
||||
|
||||
// Handle anyOf schema (used for nullable types like `string | null`)
|
||||
if (parameter.anyOf && Array.isArray(parameter.anyOf)) {
|
||||
const types = parameter.anyOf
|
||||
.map((schema) => (typeof schema === "object" && schema.type ? String(schema.type) : null))
|
||||
.filter((t): t is string => t !== null && t !== "null")
|
||||
|
||||
if (types.length > 0) {
|
||||
return types.join(" | ")
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
function getParameterDescription(name: string, parameter: SerializedCustomToolParameters, required: string[]): string {
|
||||
const requiredText = required.includes(name) ? "(required)" : "(optional)"
|
||||
const typeText = getParameterType(parameter)
|
||||
return `- ${name}: ${requiredText} ${parameter.description ?? ""} (type: ${typeText})`
|
||||
}
|
||||
|
||||
function getUsage(tool: SerializedCustomToolDefinition): string {
|
||||
const lines: string[] = [`<${tool.name}>`]
|
||||
|
||||
if (tool.parameters) {
|
||||
const required = tool.parameters.required ?? []
|
||||
|
||||
for (const [argName, _argType] of Object.entries(tool.parameters.properties ?? {})) {
|
||||
const placeholder = required.includes(argName) ? `${argName} value here` : `optional ${argName} value`
|
||||
lines.push(`<${argName}>${placeholder}</${argName}>`)
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(`</${tool.name}>`)
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
function getDescription(tool: SerializedCustomToolDefinition): string {
|
||||
const parts: string[] = []
|
||||
|
||||
parts.push(`## ${tool.name}`)
|
||||
parts.push(`Description: ${tool.description}`)
|
||||
|
||||
if (tool.parameters?.properties) {
|
||||
const required = tool.parameters?.required ?? []
|
||||
parts.push("Parameters:")
|
||||
|
||||
for (const [name, parameter] of Object.entries(tool.parameters.properties)) {
|
||||
// What should we do with `boolean` values for `parameter`?
|
||||
if (typeof parameter !== "object") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts.push(getParameterDescription(name, parameter, required))
|
||||
}
|
||||
} else {
|
||||
parts.push("Parameters: None")
|
||||
}
|
||||
|
||||
parts.push("Usage:")
|
||||
parts.push(getUsage(tool))
|
||||
|
||||
return parts.join("\n")
|
||||
}
|
||||
|
||||
export function formatXml(tools: SerializedCustomToolDefinition[]): string {
|
||||
if (tools.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const descriptions = tools.map((tool) => getDescription(tool))
|
||||
|
||||
return `# Custom Tools
|
||||
|
||||
The following custom tools are available for this mode. Use them in the same way as built-in tools.
|
||||
|
||||
${descriptions.join("\n\n")}`
|
||||
}
|
||||
4
packages/core/src/custom-tools/index.ts
Normal file
4
packages/core/src/custom-tools/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./custom-tool-registry.js"
|
||||
export * from "./serialize.js"
|
||||
export * from "./format-xml.js"
|
||||
export * from "./format-native.js"
|
||||
21
packages/core/src/custom-tools/serialize.ts
Normal file
21
packages/core/src/custom-tools/serialize.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { type SerializedCustomToolDefinition, parametersSchema } from "@roo-code/types"
|
||||
|
||||
import type { StoredCustomTool } from "./types.js"
|
||||
|
||||
export function serializeCustomTool({
|
||||
name,
|
||||
description,
|
||||
parameters,
|
||||
source,
|
||||
}: StoredCustomTool): SerializedCustomToolDefinition {
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
parameters: parameters ? parametersSchema.toJSONSchema(parameters) : undefined,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeCustomTools(tools: StoredCustomTool[]): SerializedCustomToolDefinition[] {
|
||||
return tools.map(serializeCustomTool)
|
||||
}
|
||||
8
packages/core/src/custom-tools/types.ts
Normal file
8
packages/core/src/custom-tools/types.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { type CustomToolDefinition } from "@roo-code/types"
|
||||
|
||||
export type StoredCustomTool = CustomToolDefinition & { source?: string }
|
||||
|
||||
export interface LoadResult {
|
||||
loaded: string[]
|
||||
failed: Array<{ file: string; error: string }>
|
||||
}
|
||||
1
packages/core/src/index.ts
Normal file
1
packages/core/src/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./custom-tools/index.js"
|
||||
9
packages/core/tsconfig.json
Normal file
9
packages/core/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "@roo-code/config-typescript/base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"],
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src", "scripts", "*.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
9
packages/core/vitest.config.ts
Normal file
9
packages/core/vitest.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
watch: false,
|
||||
},
|
||||
})
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
FROM node:20-slim AS base
|
||||
# Build with:
|
||||
# docker compose -f packages/evals/docker-compose.yml build runner
|
||||
|
||||
# Install pnpm
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
RUN npm install -g npm@latest npm-run-all
|
||||
# Test with:
|
||||
# docker compose -f packages/evals/docker-compose.yml run --rm runner bash
|
||||
|
||||
# Install system packages
|
||||
RUN apt update && \
|
||||
apt install -y \
|
||||
FROM debian:bookworm-slim AS base
|
||||
|
||||
# Install system packages (excluding language runtimes - those come from mise)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
vim \
|
||||
|
|
@ -22,18 +22,13 @@ RUN apt update && \
|
|||
gpg \
|
||||
xvfb \
|
||||
cmake \
|
||||
golang-go \
|
||||
default-jre \
|
||||
python3 \
|
||||
python3-venv \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Docker cli
|
||||
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
|
||||
&& apt update && apt install -y docker-ce-cli \
|
||||
&& apt-get update && apt-get install -y docker-ce-cli \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install VS Code
|
||||
|
|
@ -41,15 +36,43 @@ RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor
|
|||
&& install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg \
|
||||
&& echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" | tee /etc/apt/sources.list.d/vscode.list > /dev/null \
|
||||
&& rm -f packages.microsoft.gpg \
|
||||
&& apt update && apt install -y code \
|
||||
&& apt-get update && apt-get install -y code \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /roo
|
||||
|
||||
# Install rust
|
||||
ARG RUST_VERSION=1.87.0
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain ${RUST_VERSION} \
|
||||
&& echo 'source $HOME/.cargo/env' >> $HOME/.bashrc
|
||||
# Install mise (https://mise.jdx.dev) for language runtime management
|
||||
RUN curl https://mise.run | sh \
|
||||
&& /root/.local/bin/mise --version
|
||||
|
||||
# Set up mise environment
|
||||
ENV MISE_DATA_DIR="/root/.local/share/mise"
|
||||
ENV PATH="/root/.local/share/mise/shims:/root/.local/bin:$PATH"
|
||||
|
||||
# Define language runtime versions (matching setup.sh)
|
||||
ARG NODE_VERSION=20.19.2
|
||||
ARG PYTHON_VERSION=3.13.2
|
||||
ARG GO_VERSION=1.24.2
|
||||
ARG RUST_VERSION=1.85.1
|
||||
ARG JAVA_VERSION=openjdk-17
|
||||
ARG UV_VERSION=0.7.11
|
||||
|
||||
# Install language runtimes via mise
|
||||
RUN mise use --global node@${NODE_VERSION} \
|
||||
&& mise use --global python@${PYTHON_VERSION} \
|
||||
&& mise use --global go@${GO_VERSION} \
|
||||
&& mise use --global rust@${RUST_VERSION} \
|
||||
&& mise use --global java@${JAVA_VERSION} \
|
||||
&& mise use --global uv@${UV_VERSION} \
|
||||
&& mise reshim
|
||||
|
||||
# Verify installations
|
||||
RUN node --version && python --version && go version && rustc --version && java --version && uv --version
|
||||
|
||||
# Install pnpm (after node is available from mise)
|
||||
ENV PNPM_HOME="/root/.local/share/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN npm install -g pnpm npm-run-all
|
||||
|
||||
# Install VS Code extensions
|
||||
ARG GOLANG_EXT_VERSION=0.46.1
|
||||
|
|
@ -72,25 +95,30 @@ RUN git clone ${EVALS_REPO_URL} evals \
|
|||
&& cd evals \
|
||||
&& git checkout ${EVALS_COMMIT}
|
||||
|
||||
# Install uv and sync python dependencies
|
||||
ARG UV_VERSION=0.7.11
|
||||
# Pre-warm Gradle wrapper cache (./gradlew downloads its own Gradle regardless of system install).
|
||||
# Find a Java project with gradlew and run it to cache the distribution.
|
||||
RUN find /roo/evals -name "gradlew" -type f | head -1 | xargs -I {} sh -c 'cd $(dirname {}) && ./gradlew --version'
|
||||
|
||||
# Sync python dependencies for evals
|
||||
WORKDIR /roo/evals/python
|
||||
RUN curl -LsSf https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-installer.sh | sh \
|
||||
&& /root/.local/bin/uv sync
|
||||
RUN uv sync
|
||||
|
||||
WORKDIR /roo/repo
|
||||
|
||||
# Install npm packages
|
||||
RUN mkdir -p \
|
||||
scripts \
|
||||
apps/cli \
|
||||
packages/build \
|
||||
packages/config-eslint \
|
||||
packages/config-typescript \
|
||||
packages/core \
|
||||
packages/evals \
|
||||
packages/ipc \
|
||||
packages/telemetry \
|
||||
packages/types \
|
||||
packages/cloud \
|
||||
packages/vscode-shim \
|
||||
src \
|
||||
webview-ui
|
||||
|
||||
|
|
@ -98,14 +126,17 @@ COPY ./package.json ./
|
|||
COPY ./pnpm-lock.yaml ./
|
||||
COPY ./pnpm-workspace.yaml ./
|
||||
COPY ./scripts/bootstrap.mjs ./scripts/
|
||||
COPY ./apps/cli/package.json ./apps/cli/
|
||||
COPY ./packages/build/package.json ./packages/build/
|
||||
COPY ./packages/config-eslint/package.json ./packages/config-eslint/
|
||||
COPY ./packages/config-typescript/package.json ./packages/config-typescript/
|
||||
COPY ./packages/core/package.json ./packages/core/
|
||||
COPY ./packages/evals/package.json ./packages/evals/
|
||||
COPY ./packages/ipc/package.json ./packages/ipc/
|
||||
COPY ./packages/telemetry/package.json ./packages/telemetry/
|
||||
COPY ./packages/types/package.json ./packages/types/
|
||||
COPY ./packages/cloud/package.json ./packages/cloud/
|
||||
COPY ./packages/vscode-shim/package.json ./packages/vscode-shim/
|
||||
COPY ./src/package.json ./src/
|
||||
COPY ./webview-ui/package.json ./webview-ui/
|
||||
|
||||
|
|
@ -126,10 +157,15 @@ COPY packages/evals/.env.local ./packages/evals/
|
|||
# Copy the pre-installed VS Code extensions
|
||||
RUN cp -r /roo/.vscode-template /roo/.vscode
|
||||
|
||||
# Build the Roo Code extension
|
||||
# Build the Roo Code extension (for VSCode execution method)
|
||||
RUN pnpm vsix -- --out ../bin/roo-code.vsix \
|
||||
&& yes | code --no-sandbox --user-data-dir /roo/.vscode --install-extension bin/roo-code.vsix
|
||||
|
||||
# Build the extension bundle and CLI (for CLI execution method)
|
||||
# The CLI requires the extension bundle (src/dist/extension.js) and the CLI build (apps/cli/dist/index.js)
|
||||
RUN pnpm --filter roo-cline bundle \
|
||||
&& pnpm --filter @roo-code/cli build
|
||||
|
||||
# Copy entrypoint script
|
||||
COPY packages/evals/.docker/entrypoints/runner.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
|
|
|||
35
packages/evals/src/cli/__tests__/messageLogDeduper.test.ts
Normal file
35
packages/evals/src/cli/__tests__/messageLogDeduper.test.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { MessageLogDeduper } from "../messageLogDeduper.js"
|
||||
|
||||
describe("MessageLogDeduper", () => {
|
||||
it("dedupes identical messages for same action+ts", () => {
|
||||
const d = new MessageLogDeduper()
|
||||
const msg = { ts: 123, type: "say", say: "reasoning", text: "hello", partial: false }
|
||||
|
||||
expect(d.shouldLog("updated", msg)).toBe(true)
|
||||
expect(d.shouldLog("updated", msg)).toBe(false)
|
||||
})
|
||||
|
||||
it("logs again if payload changes for same action+ts", () => {
|
||||
const d = new MessageLogDeduper()
|
||||
expect(d.shouldLog("updated", { ts: 123, text: "a" })).toBe(true)
|
||||
expect(d.shouldLog("updated", { ts: 123, text: "b" })).toBe(true)
|
||||
})
|
||||
|
||||
it("does not dedupe across different actions", () => {
|
||||
const d = new MessageLogDeduper()
|
||||
const msg = { ts: 123, text: "same" }
|
||||
expect(d.shouldLog("created", msg)).toBe(true)
|
||||
expect(d.shouldLog("updated", msg)).toBe(true)
|
||||
})
|
||||
|
||||
it("evicts oldest entries", () => {
|
||||
const d = new MessageLogDeduper(2)
|
||||
|
||||
expect(d.shouldLog("updated", { ts: 1, text: "a" })).toBe(true)
|
||||
expect(d.shouldLog("updated", { ts: 2, text: "b" })).toBe(true)
|
||||
// causes eviction of ts:1
|
||||
expect(d.shouldLog("updated", { ts: 3, text: "c" })).toBe(true)
|
||||
// ts:1 was evicted so it should log again
|
||||
expect(d.shouldLog("updated", { ts: 1, text: "a" })).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -6,7 +6,7 @@ import { EVALS_REPO_PATH } from "../exercises/index.js"
|
|||
|
||||
import { runCi } from "./runCi.js"
|
||||
import { runEvals } from "./runEvals.js"
|
||||
import { processTask } from "./runTask.js"
|
||||
import { processTask } from "./processTask.js"
|
||||
|
||||
const main = async () => {
|
||||
await run(
|
||||
|
|
|
|||
50
packages/evals/src/cli/messageLogDeduper.ts
Normal file
50
packages/evals/src/cli/messageLogDeduper.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
export class MessageLogDeduper {
|
||||
private readonly lastLoggedByKey = new Map<string, string>()
|
||||
|
||||
constructor(private readonly maxEntries = 10_000) {}
|
||||
|
||||
/**
|
||||
* Returns true if this message should be logged.
|
||||
* Dedupe key: `${action}:${message.ts}`.
|
||||
* Dedupe rule: skip if payload is identical to the last logged payload for that key.
|
||||
*/
|
||||
public shouldLog(action: string | undefined, message: unknown): boolean {
|
||||
if (!action || !message || typeof message !== "object") {
|
||||
return true
|
||||
}
|
||||
|
||||
const ts = (message as { ts?: unknown }).ts
|
||||
if (typeof ts !== "number") {
|
||||
return true
|
||||
}
|
||||
|
||||
let serialized: string
|
||||
try {
|
||||
serialized = JSON.stringify(message)
|
||||
} catch {
|
||||
// If serialization fails, prefer logging.
|
||||
return true
|
||||
}
|
||||
|
||||
const key = `${action}:${ts}`
|
||||
const prev = this.lastLoggedByKey.get(key)
|
||||
if (prev === serialized) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Refresh insertion order so eviction removes true oldest.
|
||||
if (this.lastLoggedByKey.has(key)) {
|
||||
this.lastLoggedByKey.delete(key)
|
||||
}
|
||||
this.lastLoggedByKey.set(key, serialized)
|
||||
|
||||
if (this.lastLoggedByKey.size > this.maxEntries) {
|
||||
const oldestKey = this.lastLoggedByKey.keys().next().value as string | undefined
|
||||
if (oldestKey) {
|
||||
this.lastLoggedByKey.delete(oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
150
packages/evals/src/cli/processTask.ts
Normal file
150
packages/evals/src/cli/processTask.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
import { execa } from "execa"
|
||||
|
||||
import { type TaskEvent, RooCodeEventName } from "@roo-code/types"
|
||||
|
||||
import { findRun, findTask, updateTask } from "../db/index.js"
|
||||
|
||||
import { Logger, getTag, isDockerContainer } from "./utils.js"
|
||||
import { redisClient, getPubSubKey, registerRunner, deregisterRunner } from "./redis.js"
|
||||
import { runUnitTest } from "./runUnitTest.js"
|
||||
import { runTaskWithCli } from "./runTaskInCli.js"
|
||||
import { runTaskInVscode } from "./runTaskInVscode.js"
|
||||
|
||||
export const processTask = async ({
|
||||
taskId,
|
||||
jobToken,
|
||||
logger,
|
||||
}: {
|
||||
taskId: number
|
||||
jobToken: string | null
|
||||
logger?: Logger
|
||||
}) => {
|
||||
const task = await findTask(taskId)
|
||||
const { language, exercise } = task
|
||||
const run = await findRun(task.runId)
|
||||
await registerRunner({ runId: run.id, taskId, timeoutSeconds: (run.timeout || 5) * 60 })
|
||||
|
||||
const containerized = isDockerContainer()
|
||||
|
||||
logger =
|
||||
logger ||
|
||||
new Logger({
|
||||
logDir: containerized ? `/var/log/evals/runs/${run.id}` : `/tmp/evals/runs/${run.id}`,
|
||||
filename: `${language}-${exercise}.log`,
|
||||
tag: getTag("runTask", { run, task }),
|
||||
})
|
||||
|
||||
try {
|
||||
const publish = async (e: TaskEvent) => {
|
||||
const redis = await redisClient()
|
||||
await redis.publish(getPubSubKey(run.id), JSON.stringify(e))
|
||||
}
|
||||
|
||||
const executionMethod = run.executionMethod || "vscode"
|
||||
logger.info(`running task ${task.id} (${language}/${exercise}) via ${executionMethod}...`)
|
||||
|
||||
if (executionMethod === "cli") {
|
||||
await runTaskWithCli({ run, task, jobToken, publish, logger })
|
||||
} else {
|
||||
await runTaskInVscode({ run, task, jobToken, publish, logger })
|
||||
}
|
||||
|
||||
logger.info(`testing task ${task.id} (${language}/${exercise})...`)
|
||||
const passed = await runUnitTest({ task, logger })
|
||||
|
||||
logger.info(`task ${task.id} (${language}/${exercise}) -> ${passed}`)
|
||||
await updateTask(task.id, { passed })
|
||||
|
||||
await publish({
|
||||
eventName: passed ? RooCodeEventName.EvalPass : RooCodeEventName.EvalFail,
|
||||
taskId: task.id,
|
||||
})
|
||||
} finally {
|
||||
await deregisterRunner({ runId: run.id, taskId })
|
||||
}
|
||||
}
|
||||
|
||||
export const processTaskInContainer = async ({
|
||||
taskId,
|
||||
jobToken,
|
||||
logger,
|
||||
maxRetries = 10,
|
||||
}: {
|
||||
taskId: number
|
||||
jobToken: string | null
|
||||
logger: Logger
|
||||
maxRetries?: number
|
||||
}) => {
|
||||
const baseArgs = [
|
||||
"--rm",
|
||||
"--network evals_default",
|
||||
"-v /var/run/docker.sock:/var/run/docker.sock",
|
||||
"-v /tmp/evals:/var/log/evals",
|
||||
"-e HOST_EXECUTION_METHOD=docker",
|
||||
]
|
||||
|
||||
if (jobToken) {
|
||||
baseArgs.push(`-e ROO_CODE_CLOUD_TOKEN=${jobToken}`)
|
||||
}
|
||||
|
||||
// Pass API keys to the container so the CLI can authenticate
|
||||
const apiKeyEnvVars = [
|
||||
"OPENROUTER_API_KEY",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"GOOGLE_API_KEY",
|
||||
"DEEPSEEK_API_KEY",
|
||||
"MISTRAL_API_KEY",
|
||||
]
|
||||
|
||||
for (const envVar of apiKeyEnvVars) {
|
||||
if (process.env[envVar]) {
|
||||
baseArgs.push(`-e ${envVar}=${process.env[envVar]}`)
|
||||
}
|
||||
}
|
||||
|
||||
const command = `pnpm --filter @roo-code/evals cli --taskId ${taskId}`
|
||||
logger.info(command)
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const containerName = `evals-task-${taskId}.${attempt}`
|
||||
const args = [`--name ${containerName}`, `-e EVALS_ATTEMPT=${attempt}`, ...baseArgs]
|
||||
const isRetry = attempt > 0
|
||||
|
||||
if (isRetry) {
|
||||
const delayMs = Math.pow(2, attempt - 1) * 1000 * (0.5 + Math.random())
|
||||
logger.info(`retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries + 1})`)
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`${isRetry ? "retrying" : "executing"} container command (attempt ${attempt + 1}/${maxRetries + 1})`,
|
||||
)
|
||||
|
||||
const subprocess = execa(`docker run ${args.join(" ")} evals-runner sh -c "${command}"`, { shell: true })
|
||||
// subprocess.stdout?.on("data", (data) => console.log(data.toString()))
|
||||
// subprocess.stderr?.on("data", (data) => console.error(data.toString()))
|
||||
|
||||
try {
|
||||
const result = await subprocess
|
||||
logger.info(`container process completed with exit code: ${result.exitCode}`)
|
||||
return
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "exitCode" in error) {
|
||||
logger.error(
|
||||
`container process failed with exit code: ${error.exitCode} (attempt ${attempt + 1}/${maxRetries + 1})`,
|
||||
)
|
||||
} else {
|
||||
logger.error(`container process failed with error: ${error} (attempt ${attempt + 1}/${maxRetries + 1})`)
|
||||
}
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(`all ${maxRetries + 1} attempts failed, giving up`)
|
||||
|
||||
// TODO: Mark task as failed.
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import { EVALS_REPO_PATH } from "../exercises/index.js"
|
|||
|
||||
import { Logger, getTag, isDockerContainer, resetEvalsRepo, commitEvalsRepoChanges } from "./utils.js"
|
||||
import { startHeartbeat, stopHeartbeat } from "./redis.js"
|
||||
import { processTask, processTaskInContainer } from "./runTask.js"
|
||||
import { processTask, processTaskInContainer } from "./processTask.js"
|
||||
|
||||
export const runEvals = async (runId: number) => {
|
||||
const run = await findRun(runId)
|
||||
|
|
@ -53,13 +53,18 @@ export const runEvals = async (runId: number) => {
|
|||
}
|
||||
|
||||
try {
|
||||
// Add tasks with staggered start times when concurrency > 1
|
||||
// Add tasks with staggered start times when concurrency > 1.
|
||||
for (let i = 0; i < filteredTasks.length; i++) {
|
||||
const task = filteredTasks[i]
|
||||
if (!task) continue
|
||||
|
||||
if (!task) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (run.concurrency > 1 && i > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, STAGGER_DELAY_MS))
|
||||
}
|
||||
|
||||
queue.add(createTaskRunner(task))
|
||||
}
|
||||
|
||||
|
|
|
|||
313
packages/evals/src/cli/runTaskInCli.ts
Normal file
313
packages/evals/src/cli/runTaskInCli.ts
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as os from "node:os"
|
||||
|
||||
import pWaitFor from "p-wait-for"
|
||||
import { execa } from "execa"
|
||||
|
||||
import { type ToolUsage, TaskCommandName, RooCodeEventName, IpcMessageType } from "@roo-code/types"
|
||||
import { IpcClient } from "@roo-code/ipc"
|
||||
|
||||
import { updateTask, createTaskMetrics, updateTaskMetrics, createToolError } from "../db/index.js"
|
||||
import { EVALS_REPO_PATH } from "../exercises/index.js"
|
||||
|
||||
import { type RunTaskOptions } from "./types.js"
|
||||
import { mergeToolUsage, waitForSubprocessWithTimeout } from "./utils.js"
|
||||
|
||||
/**
|
||||
* Run a task using the Roo Code CLI (headless mode).
|
||||
* Uses the same IPC protocol as VSCode since the CLI loads the same extension bundle.
|
||||
*/
|
||||
export const runTaskWithCli = async ({ run, task, publish, logger, jobToken }: RunTaskOptions) => {
|
||||
const { language, exercise } = task
|
||||
const prompt = fs.readFileSync(path.resolve(EVALS_REPO_PATH, `prompts/${language}.md`), "utf-8")
|
||||
const workspacePath = path.resolve(EVALS_REPO_PATH, language, exercise)
|
||||
const ipcSocketPath = path.resolve(os.tmpdir(), `evals-cli-${run.id}-${task.id}.sock`)
|
||||
|
||||
const env: Record<string, string> = {
|
||||
...(process.env as Record<string, string>),
|
||||
ROO_CODE_IPC_SOCKET_PATH: ipcSocketPath,
|
||||
}
|
||||
|
||||
if (jobToken) {
|
||||
env.ROO_CODE_CLOUD_TOKEN = jobToken
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const cancelSignal = controller.signal
|
||||
|
||||
const cliArgs = [
|
||||
"--filter",
|
||||
"@roo-code/cli",
|
||||
"start",
|
||||
"--yes",
|
||||
"--exit-on-complete",
|
||||
"--reasoning-effort",
|
||||
"disabled",
|
||||
"--workspace",
|
||||
workspacePath,
|
||||
]
|
||||
|
||||
if (run.settings?.mode) {
|
||||
cliArgs.push("-M", run.settings.mode)
|
||||
}
|
||||
|
||||
if (run.settings?.apiProvider) {
|
||||
cliArgs.push("-p", run.settings.apiProvider)
|
||||
}
|
||||
|
||||
const modelId = run.settings?.apiModelId || run.settings?.openRouterModelId
|
||||
|
||||
if (modelId) {
|
||||
cliArgs.push("-m", modelId)
|
||||
}
|
||||
|
||||
cliArgs.push(prompt)
|
||||
|
||||
logger.info(`CLI command: pnpm ${cliArgs.join(" ")}`)
|
||||
|
||||
const subprocess = execa("pnpm", cliArgs, { env, cancelSignal, cwd: process.cwd() })
|
||||
|
||||
// Buffer for accumulating streaming output until we have complete lines.
|
||||
let stdoutBuffer = ""
|
||||
let stderrBuffer = ""
|
||||
|
||||
// Track subprocess exit code - with -x flag the CLI exits immediately after task completion.
|
||||
let subprocessExitCode: number | null = null
|
||||
|
||||
// Pipe CLI stdout/stderr to the logger for easier debugging.
|
||||
// Buffer output and only log complete lines to avoid fragmented token-by-token logging.
|
||||
// Use logger.raw() to output without the verbose prefix (timestamp, tag, etc).
|
||||
subprocess.stdout?.on("data", (data: Buffer) => {
|
||||
stdoutBuffer += data.toString()
|
||||
const lines = stdoutBuffer.split("\n")
|
||||
|
||||
// Keep the last incomplete line in the buffer.
|
||||
stdoutBuffer = lines.pop() || ""
|
||||
|
||||
// Log all complete lines without the verbose prefix.
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
logger.raw(line)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
subprocess.stderr?.on("data", (data: Buffer) => {
|
||||
stderrBuffer += data.toString()
|
||||
const lines = stderrBuffer.split("\n")
|
||||
|
||||
// Keep the last incomplete line in the buffer.
|
||||
stderrBuffer = lines.pop() || ""
|
||||
|
||||
// Log all complete lines without the verbose prefix.
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
logger.raw(line)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Log any remaining buffered output when the subprocess exits.
|
||||
subprocess.on("exit", (code) => {
|
||||
subprocessExitCode = code
|
||||
|
||||
if (stdoutBuffer.trim()) {
|
||||
logger.raw(stdoutBuffer)
|
||||
}
|
||||
|
||||
if (stderrBuffer.trim()) {
|
||||
logger.raw(stderrBuffer)
|
||||
}
|
||||
})
|
||||
|
||||
// Give CLI some time to start and create IPC server.
|
||||
await new Promise((resolve) => setTimeout(resolve, 5_000))
|
||||
|
||||
let client: IpcClient | undefined = undefined
|
||||
let attempts = 10 // More attempts for CLI startup.
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
client = new IpcClient(ipcSocketPath)
|
||||
await pWaitFor(() => client!.isReady, { interval: 500, timeout: 2_000 })
|
||||
break
|
||||
} catch (_error) {
|
||||
client?.disconnect()
|
||||
attempts--
|
||||
|
||||
if (attempts <= 0) {
|
||||
logger.error(`unable to connect to IPC socket -> ${ipcSocketPath}`)
|
||||
throw new Error("Unable to connect to CLI IPC socket.")
|
||||
}
|
||||
|
||||
// Wait a bit before retrying.
|
||||
await new Promise((resolve) => setTimeout(resolve, 1_000))
|
||||
}
|
||||
}
|
||||
|
||||
// For CLI mode, we need to create taskMetrics immediately because the CLI starts
|
||||
// the task right away (from command line args). By the time we connect to IPC,
|
||||
// the TaskStarted event may have already been sent and missed.
|
||||
// This is different from VSCode mode where we send StartNewTask via IPC and can
|
||||
// reliably receive TaskStarted.
|
||||
const taskMetrics = await createTaskMetrics({
|
||||
cost: 0,
|
||||
tokensIn: 0,
|
||||
tokensOut: 0,
|
||||
tokensContext: 0,
|
||||
duration: 0,
|
||||
cacheWrites: 0,
|
||||
cacheReads: 0,
|
||||
})
|
||||
|
||||
await updateTask(task.id, { taskMetricsId: taskMetrics.id, startedAt: new Date() })
|
||||
logger.info(`created taskMetrics with id ${taskMetrics.id}`)
|
||||
|
||||
// The rest of the logic handles IPC events for metrics updates.
|
||||
let taskStartedAt = Date.now()
|
||||
let taskFinishedAt: number | undefined
|
||||
let taskAbortedAt: number | undefined
|
||||
let taskTimedOut: boolean = false
|
||||
const taskMetricsId = taskMetrics.id // Already set, no need to wait for TaskStarted.
|
||||
let rooTaskId: string | undefined
|
||||
let isClientDisconnected = false
|
||||
const accumulatedToolUsage: ToolUsage = {}
|
||||
|
||||
// For CLI mode, we don't need verbose IPC message logging since we're logging stdout instead.
|
||||
// We only track what's needed for metrics and task state management.
|
||||
const ignoreEventsForBroadcast = [RooCodeEventName.Message]
|
||||
let isApiUnstable = false
|
||||
|
||||
client.on(IpcMessageType.TaskEvent, async (taskEvent) => {
|
||||
const { eventName, payload } = taskEvent
|
||||
|
||||
// Track API instability for retry logic.
|
||||
if (
|
||||
eventName === RooCodeEventName.Message &&
|
||||
payload[0].message.say &&
|
||||
["api_req_retry_delayed", "api_req_retried"].includes(payload[0].message.say)
|
||||
) {
|
||||
isApiUnstable = true
|
||||
}
|
||||
|
||||
// Publish events to Redis (except Message events) for the web UI.
|
||||
if (!ignoreEventsForBroadcast.includes(eventName)) {
|
||||
await publish({ ...taskEvent, taskId: task.id })
|
||||
}
|
||||
|
||||
// Handle task lifecycle events.
|
||||
// For CLI mode, we already created taskMetrics before connecting to IPC,
|
||||
// but we still want to capture the rooTaskId from TaskStarted if we receive it.
|
||||
if (eventName === RooCodeEventName.TaskStarted) {
|
||||
taskStartedAt = Date.now()
|
||||
rooTaskId = payload[0]
|
||||
logger.info(`received TaskStarted event, rooTaskId: ${rooTaskId}`)
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskToolFailed) {
|
||||
const [_taskId, toolName, error] = payload
|
||||
await createToolError({ taskId: task.id, toolName, error })
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskTokenUsageUpdated || eventName === RooCodeEventName.TaskCompleted) {
|
||||
// In CLI mode, taskMetricsId is always set before we register event handlers.
|
||||
const duration = Date.now() - taskStartedAt
|
||||
|
||||
const { totalCost, totalTokensIn, totalTokensOut, contextTokens, totalCacheWrites, totalCacheReads } =
|
||||
payload[1]
|
||||
|
||||
const incomingToolUsage: ToolUsage = payload[2] ?? {}
|
||||
mergeToolUsage(accumulatedToolUsage, incomingToolUsage)
|
||||
|
||||
await updateTaskMetrics(taskMetricsId, {
|
||||
cost: totalCost,
|
||||
tokensIn: totalTokensIn,
|
||||
tokensOut: totalTokensOut,
|
||||
tokensContext: contextTokens,
|
||||
duration,
|
||||
cacheWrites: totalCacheWrites ?? 0,
|
||||
cacheReads: totalCacheReads ?? 0,
|
||||
toolUsage: accumulatedToolUsage,
|
||||
})
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskAborted) {
|
||||
taskAbortedAt = Date.now()
|
||||
}
|
||||
|
||||
if (eventName === RooCodeEventName.TaskCompleted) {
|
||||
taskFinishedAt = Date.now()
|
||||
}
|
||||
})
|
||||
|
||||
client.on(IpcMessageType.Disconnect, async () => {
|
||||
logger.info(`disconnected from IPC socket -> ${ipcSocketPath}`)
|
||||
isClientDisconnected = true
|
||||
// Note: In CLI mode, we don't need to resolve taskMetricsReady since
|
||||
// taskMetrics is created synchronously before event handlers are registered.
|
||||
})
|
||||
|
||||
// Note: We do NOT send StartNewTask via IPC here because the CLI already
|
||||
// starts the task from its command line arguments. The IPC connection is
|
||||
// only used to receive events (TaskStarted, TaskCompleted, etc.) and metrics.
|
||||
// Sending StartNewTask here would start a SECOND task.
|
||||
|
||||
try {
|
||||
const timeoutMs = (run.timeout || 5) * 60 * 1_000
|
||||
|
||||
await pWaitFor(() => !!taskFinishedAt || !!taskAbortedAt || isClientDisconnected, {
|
||||
interval: 1_000,
|
||||
timeout: timeoutMs,
|
||||
})
|
||||
} catch (_error) {
|
||||
taskTimedOut = true
|
||||
logger.error("time limit reached")
|
||||
|
||||
if (rooTaskId && !isClientDisconnected) {
|
||||
logger.info("cancelling task")
|
||||
client.sendCommand({ commandName: TaskCommandName.CancelTask, data: rooTaskId })
|
||||
await new Promise((resolve) => setTimeout(resolve, 5_000))
|
||||
}
|
||||
|
||||
taskFinishedAt = Date.now()
|
||||
}
|
||||
|
||||
if (!taskFinishedAt && !taskTimedOut) {
|
||||
// With -x flag, CLI exits immediately after task completion, which can cause
|
||||
// IPC disconnection before we receive the TaskCompleted event.
|
||||
// If subprocess exited cleanly (code 0), treat as successful completion.
|
||||
if (subprocessExitCode === 0) {
|
||||
taskFinishedAt = Date.now()
|
||||
logger.info("subprocess exited cleanly (code 0), treating as task completion")
|
||||
} else {
|
||||
logger.error(`client disconnected before task finished (subprocess exit code: ${subprocessExitCode})`)
|
||||
throw new Error("Client disconnected before task completion.")
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("setting task finished at")
|
||||
await updateTask(task.id, { finishedAt: new Date() })
|
||||
|
||||
if (rooTaskId && !isClientDisconnected) {
|
||||
logger.info("closing task")
|
||||
client.sendCommand({ commandName: TaskCommandName.CloseTask, data: rooTaskId })
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000))
|
||||
}
|
||||
|
||||
if (!isClientDisconnected) {
|
||||
logger.info("disconnecting client")
|
||||
client.disconnect()
|
||||
}
|
||||
|
||||
logger.info("waiting for subprocess to finish")
|
||||
controller.abort()
|
||||
|
||||
await waitForSubprocessWithTimeout({ subprocess, logger })
|
||||
|
||||
logger.close()
|
||||
|
||||
if (isApiUnstable && !taskFinishedAt) {
|
||||
throw new Error("API is unstable, throwing to trigger a retry.")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import * as fs from "fs"
|
||||
import * as fsp from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as os from "node:os"
|
||||
|
||||
|
|
@ -7,217 +6,23 @@ import pWaitFor from "p-wait-for"
|
|||
import { execa } from "execa"
|
||||
|
||||
import {
|
||||
type TaskEvent,
|
||||
type ClineSay,
|
||||
type ToolUsage,
|
||||
TaskCommandName,
|
||||
RooCodeEventName,
|
||||
IpcMessageType,
|
||||
EVALS_SETTINGS,
|
||||
type ToolUsage,
|
||||
} from "@roo-code/types"
|
||||
import { IpcClient } from "@roo-code/ipc"
|
||||
|
||||
import {
|
||||
type Run,
|
||||
type Task,
|
||||
findRun,
|
||||
findTask,
|
||||
updateTask,
|
||||
createTaskMetrics,
|
||||
updateTaskMetrics,
|
||||
createToolError,
|
||||
} from "../db/index.js"
|
||||
import { updateTask, createTaskMetrics, updateTaskMetrics, createToolError } from "../db/index.js"
|
||||
import { EVALS_REPO_PATH } from "../exercises/index.js"
|
||||
|
||||
import { Logger, getTag, isDockerContainer } from "./utils.js"
|
||||
import { redisClient, getPubSubKey, registerRunner, deregisterRunner } from "./redis.js"
|
||||
import { runUnitTest } from "./runUnitTest.js"
|
||||
import { type RunTaskOptions } from "./types.js"
|
||||
import { isDockerContainer, copyConversationHistory, mergeToolUsage, waitForSubprocessWithTimeout } from "./utils.js"
|
||||
import { MessageLogDeduper } from "./messageLogDeduper.js"
|
||||
|
||||
class SubprocessTimeoutError extends Error {
|
||||
constructor(timeout: number) {
|
||||
super(`Subprocess timeout after ${timeout}ms`)
|
||||
this.name = "SubprocessTimeoutError"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy conversation history files from VS Code extension storage to the log directory.
|
||||
* This allows us to preserve the api_conversation_history.json and ui_messages.json
|
||||
* files for post-mortem analysis alongside the log files.
|
||||
*/
|
||||
async function copyConversationHistory({
|
||||
rooTaskId,
|
||||
logDir,
|
||||
language,
|
||||
exercise,
|
||||
iteration,
|
||||
logger,
|
||||
}: {
|
||||
rooTaskId: string
|
||||
logDir: string
|
||||
language: string
|
||||
exercise: string
|
||||
iteration: number
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
// VS Code extension global storage path within the container
|
||||
const extensionStoragePath = "/roo/.vscode/User/globalStorage/rooveterinaryinc.roo-cline"
|
||||
const taskStoragePath = path.join(extensionStoragePath, "tasks", rooTaskId)
|
||||
|
||||
const filesToCopy = ["api_conversation_history.json", "ui_messages.json"]
|
||||
|
||||
for (const filename of filesToCopy) {
|
||||
const sourcePath = path.join(taskStoragePath, filename)
|
||||
// Use sanitized exercise name (replace slashes with dashes) for the destination filename
|
||||
// Include iteration number to handle multiple attempts at the same exercise
|
||||
const sanitizedExercise = exercise.replace(/\//g, "-")
|
||||
const destFilename = `${language}-${sanitizedExercise}.${iteration}_${filename}`
|
||||
const destPath = path.join(logDir, destFilename)
|
||||
|
||||
try {
|
||||
// Check if source file exists
|
||||
await fsp.access(sourcePath)
|
||||
|
||||
// Copy the file
|
||||
await fsp.copyFile(sourcePath, destPath)
|
||||
logger.info(`copied ${filename} to ${destPath}`)
|
||||
} catch (error) {
|
||||
// File may not exist if task didn't complete properly - this is not fatal
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
logger.info(`${filename} not found at ${sourcePath} - skipping`)
|
||||
} else {
|
||||
logger.error(`failed to copy ${filename}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const processTask = async ({
|
||||
taskId,
|
||||
jobToken,
|
||||
logger,
|
||||
}: {
|
||||
taskId: number
|
||||
jobToken: string | null
|
||||
logger?: Logger
|
||||
}) => {
|
||||
const task = await findTask(taskId)
|
||||
const { language, exercise } = task
|
||||
const run = await findRun(task.runId)
|
||||
await registerRunner({ runId: run.id, taskId, timeoutSeconds: (run.timeout || 5) * 60 })
|
||||
|
||||
const containerized = isDockerContainer()
|
||||
|
||||
logger =
|
||||
logger ||
|
||||
new Logger({
|
||||
logDir: containerized ? `/var/log/evals/runs/${run.id}` : `/tmp/evals/runs/${run.id}`,
|
||||
filename: `${language}-${exercise}.log`,
|
||||
tag: getTag("runTask", { run, task }),
|
||||
})
|
||||
|
||||
try {
|
||||
const publish = async (e: TaskEvent) => {
|
||||
const redis = await redisClient()
|
||||
await redis.publish(getPubSubKey(run.id), JSON.stringify(e))
|
||||
}
|
||||
|
||||
logger.info(`running task ${task.id} (${language}/${exercise})...`)
|
||||
await runTask({ run, task, jobToken, publish, logger })
|
||||
|
||||
logger.info(`testing task ${task.id} (${language}/${exercise})...`)
|
||||
const passed = await runUnitTest({ task, logger })
|
||||
|
||||
logger.info(`task ${task.id} (${language}/${exercise}) -> ${passed}`)
|
||||
await updateTask(task.id, { passed })
|
||||
|
||||
await publish({
|
||||
eventName: passed ? RooCodeEventName.EvalPass : RooCodeEventName.EvalFail,
|
||||
taskId: task.id,
|
||||
})
|
||||
} finally {
|
||||
await deregisterRunner({ runId: run.id, taskId })
|
||||
}
|
||||
}
|
||||
|
||||
export const processTaskInContainer = async ({
|
||||
taskId,
|
||||
jobToken,
|
||||
logger,
|
||||
maxRetries = 10,
|
||||
}: {
|
||||
taskId: number
|
||||
jobToken: string | null
|
||||
logger: Logger
|
||||
maxRetries?: number
|
||||
}) => {
|
||||
const baseArgs = [
|
||||
"--rm",
|
||||
"--network evals_default",
|
||||
"-v /var/run/docker.sock:/var/run/docker.sock",
|
||||
"-v /tmp/evals:/var/log/evals",
|
||||
"-e HOST_EXECUTION_METHOD=docker",
|
||||
]
|
||||
|
||||
if (jobToken) {
|
||||
baseArgs.push(`-e ROO_CODE_CLOUD_TOKEN=${jobToken}`)
|
||||
}
|
||||
|
||||
const command = `pnpm --filter @roo-code/evals cli --taskId ${taskId}`
|
||||
logger.info(command)
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
const containerName = `evals-task-${taskId}.${attempt}`
|
||||
const args = [`--name ${containerName}`, `-e EVALS_ATTEMPT=${attempt}`, ...baseArgs]
|
||||
const isRetry = attempt > 0
|
||||
|
||||
if (isRetry) {
|
||||
const delayMs = Math.pow(2, attempt - 1) * 1000 * (0.5 + Math.random())
|
||||
logger.info(`retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries + 1})`)
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs))
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`${isRetry ? "retrying" : "executing"} container command (attempt ${attempt + 1}/${maxRetries + 1})`,
|
||||
)
|
||||
|
||||
const subprocess = execa(`docker run ${args.join(" ")} evals-runner sh -c "${command}"`, { shell: true })
|
||||
// subprocess.stdout?.on("data", (data) => console.log(data.toString()))
|
||||
// subprocess.stderr?.on("data", (data) => console.error(data.toString()))
|
||||
|
||||
try {
|
||||
const result = await subprocess
|
||||
logger.info(`container process completed with exit code: ${result.exitCode}`)
|
||||
return
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "exitCode" in error) {
|
||||
logger.error(
|
||||
`container process failed with exit code: ${error.exitCode} (attempt ${attempt + 1}/${maxRetries + 1})`,
|
||||
)
|
||||
} else {
|
||||
logger.error(`container process failed with error: ${error} (attempt ${attempt + 1}/${maxRetries + 1})`)
|
||||
}
|
||||
|
||||
if (attempt === maxRetries) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(`all ${maxRetries + 1} attempts failed, giving up`)
|
||||
|
||||
// TODO: Mark task as failed.
|
||||
}
|
||||
|
||||
type RunTaskOptions = {
|
||||
run: Run
|
||||
task: Task
|
||||
jobToken: string | null
|
||||
publish: (taskEvent: TaskEvent) => Promise<void>
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskOptions) => {
|
||||
export const runTaskInVscode = async ({ run, task, publish, logger, jobToken }: RunTaskOptions) => {
|
||||
const { language, exercise } = task
|
||||
const prompt = fs.readFileSync(path.resolve(EVALS_REPO_PATH, `prompts/${language}.md`), "utf-8")
|
||||
const workspacePath = path.resolve(EVALS_REPO_PATH, language, exercise)
|
||||
|
|
@ -300,11 +105,13 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
"diff_error",
|
||||
"condense_context",
|
||||
"condense_context_error",
|
||||
"api_req_rate_limit_wait",
|
||||
"api_req_retry_delayed",
|
||||
"api_req_retried",
|
||||
]
|
||||
|
||||
let isApiUnstable = false
|
||||
const messageLogDeduper = new MessageLogDeduper()
|
||||
|
||||
client.on(IpcMessageType.TaskEvent, async (taskEvent) => {
|
||||
const { eventName, payload } = taskEvent
|
||||
|
|
@ -330,6 +137,15 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
(payload[0].message.say && loggableSays.includes(payload[0].message.say)) ||
|
||||
payload[0].message.partial !== true)
|
||||
) {
|
||||
// Dedupe identical repeated message events (same message.ts + same payload)
|
||||
if (eventName === RooCodeEventName.Message) {
|
||||
const action = payload[0]?.action as string | undefined
|
||||
const message = payload[0]?.message
|
||||
if (!messageLogDeduper.shouldLog(action, message)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Extract tool name for tool-related messages for clearer logging
|
||||
let logEventName: string = eventName
|
||||
if (eventName === RooCodeEventName.Message && payload[0]?.message?.ask === "tool") {
|
||||
|
|
@ -398,24 +214,7 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
|
||||
// For both TaskTokenUsageUpdated and TaskCompleted: toolUsage is payload[2]
|
||||
const incomingToolUsage: ToolUsage = payload[2] ?? {}
|
||||
|
||||
// Merge incoming tool usage with accumulated data using MAX strategy.
|
||||
// This handles the case where a task is rehydrated after abort:
|
||||
// - Empty rehydrated data won't overwrite existing: max(5, 0) = 5
|
||||
// - Legitimate restart with additional work is captured: max(5, 8) = 8
|
||||
// Each task instance tracks its own cumulative values, so we take the max
|
||||
// to preserve the highest values seen across all instances.
|
||||
for (const [toolName, usage] of Object.entries(incomingToolUsage)) {
|
||||
const existing = accumulatedToolUsage[toolName as keyof ToolUsage]
|
||||
if (existing) {
|
||||
accumulatedToolUsage[toolName as keyof ToolUsage] = {
|
||||
attempts: Math.max(existing.attempts, usage.attempts),
|
||||
failures: Math.max(existing.failures, usage.failures),
|
||||
}
|
||||
} else {
|
||||
accumulatedToolUsage[toolName as keyof ToolUsage] = { ...usage }
|
||||
}
|
||||
}
|
||||
mergeToolUsage(accumulatedToolUsage, incomingToolUsage)
|
||||
|
||||
await updateTaskMetrics(taskMetricsId, {
|
||||
cost: totalCost,
|
||||
|
|
@ -502,35 +301,7 @@ export const runTask = async ({ run, task, publish, logger, jobToken }: RunTaskO
|
|||
logger.info("waiting for subprocess to finish")
|
||||
controller.abort()
|
||||
|
||||
// Wait for subprocess to finish gracefully, with a timeout.
|
||||
const SUBPROCESS_TIMEOUT = 10_000
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
subprocess,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new SubprocessTimeoutError(SUBPROCESS_TIMEOUT)), SUBPROCESS_TIMEOUT),
|
||||
),
|
||||
])
|
||||
|
||||
logger.info("subprocess finished gracefully")
|
||||
} catch (error) {
|
||||
if (error instanceof SubprocessTimeoutError) {
|
||||
logger.error("subprocess did not finish within timeout, force killing")
|
||||
|
||||
try {
|
||||
if (subprocess.kill("SIGKILL")) {
|
||||
logger.info("SIGKILL sent to subprocess")
|
||||
} else {
|
||||
logger.error("failed to send SIGKILL to subprocess")
|
||||
}
|
||||
} catch (killError) {
|
||||
logger.error("subprocess.kill(SIGKILL) failed:", killError)
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
await waitForSubprocessWithTimeout({ subprocess, logger })
|
||||
|
||||
// Copy conversation history files from VS Code extension storage to the log directory
|
||||
// for post-mortem analysis. Only do this in containerized mode where we have a known path.
|
||||
19
packages/evals/src/cli/types.ts
Normal file
19
packages/evals/src/cli/types.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { type TaskEvent } from "@roo-code/types"
|
||||
|
||||
import type { Run, Task } from "../db/index.js"
|
||||
import { Logger } from "./utils.js"
|
||||
|
||||
export class SubprocessTimeoutError extends Error {
|
||||
constructor(timeout: number) {
|
||||
super(`Subprocess timeout after ${timeout}ms`)
|
||||
this.name = "SubprocessTimeoutError"
|
||||
}
|
||||
}
|
||||
|
||||
export type RunTaskOptions = {
|
||||
run: Run
|
||||
task: Task
|
||||
jobToken: string | null
|
||||
publish: (taskEvent: TaskEvent) => Promise<void>
|
||||
logger: Logger
|
||||
}
|
||||
|
|
@ -1,10 +1,15 @@
|
|||
import * as fs from "fs"
|
||||
import * as fsp from "fs/promises"
|
||||
import * as path from "path"
|
||||
|
||||
import { execa } from "execa"
|
||||
import { execa, type ResultPromise } from "execa"
|
||||
|
||||
import type { ToolUsage } from "@roo-code/types"
|
||||
|
||||
import type { Run, Task } from "../db/index.js"
|
||||
|
||||
import { SubprocessTimeoutError } from "./types.js"
|
||||
|
||||
export const getTag = (caller: string, { run, task }: { run: Run; task?: Task }) =>
|
||||
task
|
||||
? `${caller} | pid:${process.pid} | run:${run.id} | task:${task.id} | ${task.language}/${task.exercise}`
|
||||
|
|
@ -107,6 +112,22 @@ export class Logger {
|
|||
this.info(message, ...args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Write raw output without any prefix (timestamp, level, tag).
|
||||
* Useful for streaming CLI output where the prefix would be noise.
|
||||
*/
|
||||
public raw(message: string): void {
|
||||
try {
|
||||
console.log(message)
|
||||
|
||||
if (this.logStream) {
|
||||
this.logStream.write(message + "\n")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to write to log file ${this.logFilePath}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
if (this.logStream) {
|
||||
this.logStream.end()
|
||||
|
|
@ -114,3 +135,117 @@ export class Logger {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy conversation history files from VS Code extension storage to the log directory.
|
||||
* This allows us to preserve the api_conversation_history.json and ui_messages.json
|
||||
* files for post-mortem analysis alongside the log files.
|
||||
*/
|
||||
export async function copyConversationHistory({
|
||||
rooTaskId,
|
||||
logDir,
|
||||
language,
|
||||
exercise,
|
||||
iteration,
|
||||
logger,
|
||||
}: {
|
||||
rooTaskId: string
|
||||
logDir: string
|
||||
language: string
|
||||
exercise: string
|
||||
iteration: number
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
// VS Code extension global storage path within the container
|
||||
const extensionStoragePath = "/roo/.vscode/User/globalStorage/rooveterinaryinc.roo-cline"
|
||||
const taskStoragePath = path.join(extensionStoragePath, "tasks", rooTaskId)
|
||||
|
||||
const filesToCopy = ["api_conversation_history.json", "ui_messages.json"]
|
||||
|
||||
for (const filename of filesToCopy) {
|
||||
const sourcePath = path.join(taskStoragePath, filename)
|
||||
// Use sanitized exercise name (replace slashes with dashes) for the destination filename
|
||||
// Include iteration number to handle multiple attempts at the same exercise
|
||||
const sanitizedExercise = exercise.replace(/\//g, "-")
|
||||
const destFilename = `${language}-${sanitizedExercise}.${iteration}_${filename}`
|
||||
const destPath = path.join(logDir, destFilename)
|
||||
|
||||
try {
|
||||
// Check if source file exists
|
||||
await fsp.access(sourcePath)
|
||||
|
||||
// Copy the file
|
||||
await fsp.copyFile(sourcePath, destPath)
|
||||
logger.info(`copied ${filename} to ${destPath}`)
|
||||
} catch (error) {
|
||||
// File may not exist if task didn't complete properly - this is not fatal
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
logger.info(`${filename} not found at ${sourcePath} - skipping`)
|
||||
} else {
|
||||
logger.error(`failed to copy ${filename}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge incoming tool usage with accumulated data using MAX strategy.
|
||||
* This handles the case where a task is rehydrated after abort:
|
||||
* - Empty rehydrated data won't overwrite existing: max(5, 0) = 5
|
||||
* - Legitimate restart with additional work is captured: max(5, 8) = 8
|
||||
* Each task instance tracks its own cumulative values, so we take the max
|
||||
* to preserve the highest values seen across all instances.
|
||||
*/
|
||||
export function mergeToolUsage(accumulated: ToolUsage, incoming: ToolUsage): void {
|
||||
for (const [toolName, usage] of Object.entries(incoming)) {
|
||||
const existing = accumulated[toolName as keyof ToolUsage]
|
||||
|
||||
if (existing) {
|
||||
accumulated[toolName as keyof ToolUsage] = {
|
||||
attempts: Math.max(existing.attempts, usage.attempts),
|
||||
failures: Math.max(existing.failures, usage.failures),
|
||||
}
|
||||
} else {
|
||||
accumulated[toolName as keyof ToolUsage] = { ...usage }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a subprocess to finish gracefully, with a timeout.
|
||||
* If the subprocess doesn't finish within the timeout, force kill it with SIGKILL.
|
||||
*/
|
||||
export async function waitForSubprocessWithTimeout({
|
||||
subprocess,
|
||||
timeoutMs = 10_000,
|
||||
logger,
|
||||
}: {
|
||||
subprocess: ResultPromise
|
||||
timeoutMs?: number
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
try {
|
||||
await Promise.race([
|
||||
subprocess,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new SubprocessTimeoutError(timeoutMs)), timeoutMs)),
|
||||
])
|
||||
|
||||
logger.info("subprocess finished gracefully")
|
||||
} catch (error) {
|
||||
if (error instanceof SubprocessTimeoutError) {
|
||||
logger.error("subprocess did not finish within timeout, force killing")
|
||||
|
||||
try {
|
||||
if (subprocess.kill("SIGKILL")) {
|
||||
logger.info("SIGKILL sent to subprocess")
|
||||
} else {
|
||||
logger.error("failed to send SIGKILL to subprocess")
|
||||
}
|
||||
} catch (killError) {
|
||||
logger.error("subprocess.kill(SIGKILL) failed:", killError)
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "runs" ADD COLUMN "execution_method" text DEFAULT 'vscode' NOT NULL;
|
||||
479
packages/evals/src/db/migrations/meta/0006_snapshot.json
Normal file
479
packages/evals/src/db/migrations/meta/0006_snapshot.json
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
{
|
||||
"id": "ae1ebc36-8f5b-43e1-8e47-5a63d72ed05f",
|
||||
"prevId": "71b54967-86df-42ec-a200-bfd8dad85069",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.runs": {
|
||||
"name": "runs",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "runs_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"task_metrics_id": {
|
||||
"name": "task_metrics_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"model": {
|
||||
"name": "model",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"contextWindow": {
|
||||
"name": "contextWindow",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"inputPrice": {
|
||||
"name": "inputPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"outputPrice": {
|
||||
"name": "outputPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cacheWritesPrice": {
|
||||
"name": "cacheWritesPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cacheReadsPrice": {
|
||||
"name": "cacheReadsPrice",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"settings": {
|
||||
"name": "settings",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"jobToken": {
|
||||
"name": "jobToken",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"pid": {
|
||||
"name": "pid",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"socket_path": {
|
||||
"name": "socket_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"execution_method": {
|
||||
"name": "execution_method",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'vscode'"
|
||||
},
|
||||
"concurrency": {
|
||||
"name": "concurrency",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 2
|
||||
},
|
||||
"timeout": {
|
||||
"name": "timeout",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 5
|
||||
},
|
||||
"passed": {
|
||||
"name": "passed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"failed": {
|
||||
"name": "failed",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"runs_task_metrics_id_taskMetrics_id_fk": {
|
||||
"name": "runs_task_metrics_id_taskMetrics_id_fk",
|
||||
"tableFrom": "runs",
|
||||
"tableTo": "taskMetrics",
|
||||
"columnsFrom": ["task_metrics_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.taskMetrics": {
|
||||
"name": "taskMetrics",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "taskMetrics_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"tokens_in": {
|
||||
"name": "tokens_in",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tokens_out": {
|
||||
"name": "tokens_out",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tokens_context": {
|
||||
"name": "tokens_context",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cache_writes": {
|
||||
"name": "cache_writes",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cache_reads": {
|
||||
"name": "cache_reads",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"cost": {
|
||||
"name": "cost",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"tool_usage": {
|
||||
"name": "tool_usage",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.tasks": {
|
||||
"name": "tasks",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "tasks_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"run_id": {
|
||||
"name": "run_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"task_metrics_id": {
|
||||
"name": "task_metrics_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"language": {
|
||||
"name": "language",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"exercise": {
|
||||
"name": "exercise",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"iteration": {
|
||||
"name": "iteration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"passed": {
|
||||
"name": "passed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"finished_at": {
|
||||
"name": "finished_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"tasks_language_exercise_iteration_idx": {
|
||||
"name": "tasks_language_exercise_iteration_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "run_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "language",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "exercise",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "iteration",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"tasks_run_id_runs_id_fk": {
|
||||
"name": "tasks_run_id_runs_id_fk",
|
||||
"tableFrom": "tasks",
|
||||
"tableTo": "runs",
|
||||
"columnsFrom": ["run_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"tasks_task_metrics_id_taskMetrics_id_fk": {
|
||||
"name": "tasks_task_metrics_id_taskMetrics_id_fk",
|
||||
"tableFrom": "tasks",
|
||||
"tableTo": "taskMetrics",
|
||||
"columnsFrom": ["task_metrics_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.toolErrors": {
|
||||
"name": "toolErrors",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"identity": {
|
||||
"type": "always",
|
||||
"name": "toolErrors_id_seq",
|
||||
"schema": "public",
|
||||
"increment": "1",
|
||||
"startWith": "1",
|
||||
"minValue": "1",
|
||||
"maxValue": "2147483647",
|
||||
"cache": "1",
|
||||
"cycle": false
|
||||
}
|
||||
},
|
||||
"run_id": {
|
||||
"name": "run_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"task_id": {
|
||||
"name": "task_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"tool_name": {
|
||||
"name": "tool_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"toolErrors_run_id_runs_id_fk": {
|
||||
"name": "toolErrors_run_id_runs_id_fk",
|
||||
"tableFrom": "toolErrors",
|
||||
"tableTo": "runs",
|
||||
"columnsFrom": ["run_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"toolErrors_task_id_tasks_id_fk": {
|
||||
"name": "toolErrors_task_id_tasks_id_fk",
|
||||
"tableFrom": "toolErrors",
|
||||
"tableTo": "tasks",
|
||||
"columnsFrom": ["task_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,13 @@
|
|||
"when": 1765167049182,
|
||||
"tag": "0005_strong_skrulls",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "7",
|
||||
"when": 1767550126096,
|
||||
"tag": "0006_worried_spectrum",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ import type { RooCodeSettings, ToolName, ToolUsage } from "@roo-code/types"
|
|||
|
||||
import type { ExerciseLanguage } from "../exercises/index.js"
|
||||
|
||||
/**
|
||||
* ExecutionMethod
|
||||
*/
|
||||
|
||||
export type ExecutionMethod = "vscode" | "cli"
|
||||
|
||||
/**
|
||||
* runs
|
||||
*/
|
||||
|
|
@ -24,6 +30,7 @@ export const runs = pgTable("runs", {
|
|||
jobToken: text(),
|
||||
pid: integer(),
|
||||
socketPath: text("socket_path").notNull(),
|
||||
executionMethod: text("execution_method").default("vscode").notNull().$type<ExecutionMethod>(),
|
||||
concurrency: integer().default(2).notNull(),
|
||||
timeout: integer().default(5).notNull(),
|
||||
passed: integer().default(0).notNull(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@roo-code/types",
|
||||
"version": "1.93.0",
|
||||
"version": "1.99.0",
|
||||
"description": "TypeScript type definitions for Roo Code.",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
|
|
|||
89
packages/types/src/__tests__/custom-tool.spec.ts
Normal file
89
packages/types/src/__tests__/custom-tool.spec.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
type CustomToolDefinition,
|
||||
type CustomToolContext,
|
||||
defineCustomTool,
|
||||
parametersSchema as z,
|
||||
} from "../custom-tool.js"
|
||||
import type { TaskLike } from "../task.js"
|
||||
|
||||
describe("custom-tool utilities", () => {
|
||||
describe("z (Zod re-export)", () => {
|
||||
it("should export z from zod", () => {
|
||||
expect(z).toBeDefined()
|
||||
expect(z.string).toBeInstanceOf(Function)
|
||||
expect(z.object).toBeInstanceOf(Function)
|
||||
expect(z.number).toBeInstanceOf(Function)
|
||||
})
|
||||
|
||||
it("should allow creating schemas", () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
count: z.number().optional(),
|
||||
})
|
||||
|
||||
const result = schema.parse({ name: "test" })
|
||||
expect(result).toEqual({ name: "test" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("defineCustomTool", () => {
|
||||
it("should return the same definition object", () => {
|
||||
const definition = {
|
||||
name: "test-tool",
|
||||
description: "Test tool",
|
||||
parameters: z.object({ input: z.string() }),
|
||||
execute: async (args: { input: string }) => `Result: ${args.input}`,
|
||||
}
|
||||
|
||||
const result = defineCustomTool(definition)
|
||||
expect(result).toBe(definition)
|
||||
})
|
||||
|
||||
it("should work without parameters", () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "no-params-tool",
|
||||
description: "No params tool",
|
||||
execute: async () => "done",
|
||||
})
|
||||
|
||||
expect(tool.description).toBe("No params tool")
|
||||
expect(tool.parameters).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should preserve type inference for execute args", async () => {
|
||||
const tool = defineCustomTool({
|
||||
name: "typed-tool",
|
||||
description: "Typed tool",
|
||||
parameters: z.object({
|
||||
name: z.string(),
|
||||
count: z.number(),
|
||||
}),
|
||||
execute: async (args) => {
|
||||
// TypeScript should infer args as { name: string, count: number }.
|
||||
return `Hello ${args.name}, count is ${args.count}`
|
||||
},
|
||||
})
|
||||
|
||||
const context: CustomToolContext = {
|
||||
mode: "code",
|
||||
task: { taskId: "test-task-id" } as unknown as TaskLike,
|
||||
}
|
||||
|
||||
const result = await tool.execute({ name: "World", count: 42 }, context)
|
||||
expect(result).toBe("Hello World, count is 42")
|
||||
})
|
||||
})
|
||||
|
||||
describe("CustomToolDefinition type", () => {
|
||||
it("should accept valid definitions", () => {
|
||||
const def: CustomToolDefinition = {
|
||||
name: "valid-tool",
|
||||
description: "A valid tool",
|
||||
parameters: z.object({}),
|
||||
execute: async () => "result",
|
||||
}
|
||||
|
||||
expect(def.description).toBe("A valid tool")
|
||||
})
|
||||
})
|
||||
})
|
||||
104
packages/types/src/custom-tool.ts
Normal file
104
packages/types/src/custom-tool.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import type { ZodType, z } from "zod/v4"
|
||||
|
||||
import { TaskLike } from "./task.js"
|
||||
|
||||
// Re-export from Zod for convenience.
|
||||
|
||||
export { z as parametersSchema } from "zod/v4"
|
||||
|
||||
export type CustomToolParametersSchema = ZodType
|
||||
|
||||
export type SerializedCustomToolParameters = z.core.JSONSchema.JSONSchema
|
||||
|
||||
/**
|
||||
* Context provided to tool execute functions.
|
||||
*/
|
||||
export interface CustomToolContext {
|
||||
mode: string
|
||||
task: TaskLike
|
||||
}
|
||||
|
||||
/**
|
||||
* Definition structure for a custom tool.
|
||||
*
|
||||
* Note: This interface uses simple types to avoid TypeScript performance issues
|
||||
* with Zod's complex type inference. For type-safe parameter inference, use
|
||||
* the `defineCustomTool` helper function instead of annotating with this interface.
|
||||
*/
|
||||
export interface CustomToolDefinition {
|
||||
/**
|
||||
* The name of the tool.
|
||||
* This is used to identify the tool in the prompt and in the tool registry.
|
||||
*/
|
||||
name: string
|
||||
|
||||
/**
|
||||
* A description of what the tool does.
|
||||
* This is shown to the AI model to help it decide when to use the tool.
|
||||
*/
|
||||
description: string
|
||||
|
||||
/**
|
||||
* Optional Zod schema defining the tool's parameters.
|
||||
* Use `z.object({})` to define the shape of arguments.
|
||||
*/
|
||||
parameters?: CustomToolParametersSchema
|
||||
|
||||
/**
|
||||
* The function that executes the tool.
|
||||
*
|
||||
* @param args - The validated arguments
|
||||
* @param context - Execution context with session and message info
|
||||
* @returns A string result to return to the AI
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
execute: (args: any, context: CustomToolContext) => Promise<string>
|
||||
}
|
||||
|
||||
export interface SerializedCustomToolDefinition {
|
||||
name: string
|
||||
description: string
|
||||
parameters?: SerializedCustomToolParameters
|
||||
source?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe definition structure for a custom tool with inferred parameter types.
|
||||
* Use this with `defineCustomTool` for full type inference.
|
||||
*
|
||||
* @template T - The Zod schema type for parameters
|
||||
*/
|
||||
export interface TypedCustomToolDefinition<T extends CustomToolParametersSchema>
|
||||
extends Omit<CustomToolDefinition, "execute" | "parameters"> {
|
||||
parameters?: T
|
||||
execute: (args: z.infer<T>, context: CustomToolContext) => Promise<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to define a custom tool with proper type inference.
|
||||
*
|
||||
* This is optional - you can also just export a plain object that matches
|
||||
* the CustomToolDefinition interface.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { parametersSchema as z, defineCustomTool } from "@roo-code/types"
|
||||
*
|
||||
* export default defineCustomTool({
|
||||
* name: "add_numbers",
|
||||
* description: "Add two numbers",
|
||||
* parameters: z.object({
|
||||
* a: z.number().describe("First number"),
|
||||
* b: z.number().describe("Second number"),
|
||||
* }),
|
||||
* async execute({ a, b }) {
|
||||
* return `The sum is ${a + b}`
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function defineCustomTool<T extends CustomToolParametersSchema>(
|
||||
definition: TypedCustomToolDefinition<T>,
|
||||
): TypedCustomToolDefinition<T> {
|
||||
return definition
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ export const experimentIds = [
|
|||
"imageGeneration",
|
||||
"runSlashCommand",
|
||||
"multipleNativeToolCalls",
|
||||
"customTools",
|
||||
] as const
|
||||
|
||||
export const experimentIdsSchema = z.enum(experimentIds)
|
||||
|
|
@ -30,6 +31,7 @@ export const experimentsSchema = z.object({
|
|||
imageGeneration: z.boolean().optional(),
|
||||
runSlashCommand: z.boolean().optional(),
|
||||
multipleNativeToolCalls: z.boolean().optional(),
|
||||
customTools: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type Experiments = z.infer<typeof experimentsSchema>
|
||||
|
|
|
|||
|
|
@ -143,6 +143,7 @@ export const globalSettingsSchema = z.object({
|
|||
maxOpenTabsContext: z.number().optional(),
|
||||
maxWorkspaceFiles: z.number().optional(),
|
||||
showRooIgnoredFiles: z.boolean().optional(),
|
||||
enableSubfolderRules: z.boolean().optional(),
|
||||
maxReadFileLine: z.number().optional(),
|
||||
maxImageFileSize: z.number().optional(),
|
||||
maxTotalImageSize: z.number().optional(),
|
||||
|
|
@ -291,7 +292,6 @@ export const isGlobalStateKey = (key: string): key is Keys<GlobalState> =>
|
|||
// Default settings when running evals (unless overridden).
|
||||
export const EVALS_SETTINGS: RooCodeSettings = {
|
||||
apiProvider: "openrouter",
|
||||
openRouterUseMiddleOutTransform: false,
|
||||
|
||||
lastShownAnnouncementId: "jul-09-2025-3-23-0",
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export * from "./cloud.js"
|
|||
export * from "./codebase-index.js"
|
||||
export * from "./context-management.js"
|
||||
export * from "./cookie-consent.js"
|
||||
export * from "./custom-tool.js"
|
||||
export * from "./events.js"
|
||||
export * from "./experiment.js"
|
||||
export * from "./followup.js"
|
||||
|
|
@ -16,7 +17,6 @@ export * from "./message.js"
|
|||
export * from "./mode.js"
|
||||
export * from "./model.js"
|
||||
export * from "./provider-settings.js"
|
||||
export * from "./single-file-read-models.js"
|
||||
export * from "./task.js"
|
||||
export * from "./todo.js"
|
||||
export * from "./telemetry.js"
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
|
|||
* - `api_req_finished`: Indicates an API request has completed successfully
|
||||
* - `api_req_retried`: Indicates an API request is being retried after a failure
|
||||
* - `api_req_retry_delayed`: Indicates an API request retry has been delayed
|
||||
* - `api_req_rate_limit_wait`: Indicates a configured rate-limit wait (not an error)
|
||||
* - `api_req_deleted`: Indicates an API request has been deleted/cancelled
|
||||
* - `text`: General text message or assistant response
|
||||
* - `reasoning`: Assistant's reasoning or thought process (often hidden from user)
|
||||
|
|
@ -155,6 +156,7 @@ export const clineSays = [
|
|||
"api_req_finished",
|
||||
"api_req_retried",
|
||||
"api_req_retry_delayed",
|
||||
"api_req_rate_limit_wait",
|
||||
"api_req_deleted",
|
||||
"text",
|
||||
"image",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue