mirror of
https://github.com/cline/cline.git
synced 2026-09-07 04:44:58 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4f6783140 | |||
| 174c098cdf |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Fix issue with Cline accounts not showing user info in popout tabs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
add coverage tests to github workflows
|
||||
@@ -11,12 +11,10 @@ graph TB
|
||||
subgraph VSCode Extension Host
|
||||
subgraph Core Extension
|
||||
ExtensionEntry[Extension Entry<br/>src/extension.ts]
|
||||
WebviewProvider[WebviewProvider<br/>src/core/webview/index.ts]
|
||||
Controller[Controller<br/>src/core/controller/index.ts]
|
||||
Task[Task<br/>src/core/task/index.ts]
|
||||
ClineProvider[ClineProvider<br/>src/core/webview/ClineProvider.ts]
|
||||
ClineClass[Cline Class<br/>src/core/Cline.ts]
|
||||
GlobalState[VSCode Global State]
|
||||
SecretsStorage[VSCode Secrets Storage]
|
||||
McpHub[McpHub<br/>src/services/mcp/McpHub.ts]
|
||||
end
|
||||
|
||||
subgraph Webview UI
|
||||
@@ -29,101 +27,45 @@ graph TB
|
||||
TaskStorage[Task Storage<br/>Per-Task Files & History]
|
||||
CheckpointSystem[Git-based Checkpoints]
|
||||
end
|
||||
|
||||
subgraph API Providers
|
||||
AnthropicAPI[Anthropic]
|
||||
OpenRouterAPI[OpenRouter]
|
||||
BedrockAPI[AWS Bedrock]
|
||||
OtherAPIs[Other Providers]
|
||||
end
|
||||
|
||||
subgraph MCP Servers
|
||||
ExternalMcpServers[External MCP Servers]
|
||||
end
|
||||
end
|
||||
|
||||
%% Core Extension Data Flow
|
||||
ExtensionEntry --> WebviewProvider
|
||||
WebviewProvider --> Controller
|
||||
Controller --> Task
|
||||
Controller --> McpHub
|
||||
Task --> GlobalState
|
||||
Task --> SecretsStorage
|
||||
Task --> TaskStorage
|
||||
Task --> CheckpointSystem
|
||||
Task --> |API Requests| API Providers
|
||||
McpHub --> |Connects to| ExternalMcpServers
|
||||
Task --> |Uses| McpHub
|
||||
ExtensionEntry --> ClineProvider
|
||||
ClineProvider --> ClineClass
|
||||
ClineClass --> GlobalState
|
||||
ClineClass --> SecretsStorage
|
||||
ClineClass --> TaskStorage
|
||||
ClineClass --> CheckpointSystem
|
||||
|
||||
%% Webview Data Flow
|
||||
WebviewApp --> ExtStateContext
|
||||
ExtStateContext --> ReactComponents
|
||||
|
||||
%% Bidirectional Communication
|
||||
WebviewProvider <-->|postMessage| ExtStateContext
|
||||
ClineProvider <-->|postMessage| ExtStateContext
|
||||
|
||||
style GlobalState fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style SecretsStorage fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style ExtStateContext fill:#bbf,stroke:#333,stroke-width:2px
|
||||
style WebviewProvider fill:#bfb,stroke:#333,stroke-width:2px
|
||||
style McpHub fill:#bfb,stroke:#333,stroke-width:2px
|
||||
style API Providers fill:#fdb,stroke:#333,stroke-width:2px
|
||||
style ClineProvider fill:#bfb,stroke:#333,stroke-width:2px
|
||||
```
|
||||
|
||||
## Definitions
|
||||
|
||||
- **Core Extension**: Anything inside the src folder, organized into modular components
|
||||
- **Core Extension State**: Managed by the Controller class in src/core/controller/index.ts, which serves as the single source of truth for the extension's state. It manages multiple types of persistent storage (global state, workspace state, and secrets), handles state distribution to both the core extension and webview components, and coordinates state across multiple extension instances. This includes managing API configurations, task history, settings, and MCP configurations.
|
||||
- **Webview**: Anything inside the webview-ui. All the react or view's seen by the user and user interaction components
|
||||
- **Webview State**: Managed by ExtensionStateContext in webview-ui/src/context/ExtensionStateContext.tsx, which provides React components with access to the extension's state through a context provider pattern. It maintains local state for UI components, handles real-time updates through message events, manages partial message updates, and provides methods for state modifications. The context includes extension version, messages, task history, theme, API configurations, MCP servers, marketplace catalog, and workspace file paths. It synchronizes with the core extension through VSCode's message passing system and provides type-safe access to state through a custom hook (useExtensionState).
|
||||
|
||||
### Core Extension Architecture
|
||||
|
||||
The core extension follows a clear hierarchical structure:
|
||||
|
||||
1. **WebviewProvider** (src/core/webview/index.ts): Manages the webview lifecycle and communication
|
||||
2. **Controller** (src/core/controller/index.ts): Handles webview messages and task management
|
||||
3. **Task** (src/core/task/index.ts): Executes API requests and tool operations
|
||||
|
||||
This architecture provides clear separation of concerns:
|
||||
- WebviewProvider focuses on VSCode webview integration
|
||||
- Controller manages state and coordinates tasks
|
||||
- Task handles the execution of AI requests and tool operations
|
||||
|
||||
### WebviewProvider Implementation
|
||||
|
||||
The WebviewProvider class in `src/core/webview/index.ts` is responsible for:
|
||||
|
||||
- Managing multiple active instances through a static set (`activeInstances`)
|
||||
- Handling webview lifecycle events (creation, visibility changes, disposal)
|
||||
- Implementing HTML content generation with proper CSP headers
|
||||
- Supporting Hot Module Replacement (HMR) for development
|
||||
- Setting up message listeners between the webview and extension
|
||||
|
||||
The WebviewProvider maintains a reference to the Controller and delegates message handling to it. It also handles the creation of both sidebar and tab panel webviews, allowing Cline to be used in different contexts within VSCode.
|
||||
- core extension: Anything inside the src folder starting with the Cline.ts file
|
||||
- core extension state: Managed by the ClineProvider class in src/core/webview/ClineProvider.ts, which serves as the single source of truth for the extension's state. It manages multiple types of persistent storage (global state, workspace state, and secrets), handles state distribution to both the core extension and webview components, and coordinates state across multiple extension instances. This includes managing API configurations, task history, settings, and MCP configurations.
|
||||
- webview: Anything inside the webview-ui. All the react or view's seen by the user and user interaction compone
|
||||
- webview state: Managed by ExtensionStateContext in webview-ui/src/context/ExtensionStateContext.tsx, which provides React components with access to the extension's state through a context provider pattern. It maintains local state for UI components, handles real-time updates through message events, manages partial message updates, and provides methods for state modifications. The context includes extension version, messages, task history, theme, API configurations, MCP servers, marketplace catalog, and workspace file paths. It synchronizes with the core extension through VSCode's message passing system and provides type-safe access to state through a custom hook (useExtensionState).
|
||||
|
||||
### Core Extension State
|
||||
|
||||
The `Controller` class manages multiple types of persistent storage:
|
||||
The `ClineProvider` class manages multiple types of persistent storage:
|
||||
|
||||
- **Global State:** Stored across all VSCode instances. Used for settings and data that should persist globally.
|
||||
- **Workspace State:** Specific to the current workspace. Used for task-specific data and settings.
|
||||
- **Secrets:** Secure storage for sensitive information like API keys.
|
||||
|
||||
The `Controller` handles the distribution of state to both the core extension and webview components. It also coordinates state across multiple extension instances, ensuring consistency.
|
||||
|
||||
State synchronization between instances is handled through:
|
||||
- File-based storage for task history and conversation data
|
||||
- VSCode's global state API for settings and configuration
|
||||
- Secrets storage for sensitive information
|
||||
- Event listeners for file changes and configuration updates
|
||||
|
||||
The Controller implements methods for:
|
||||
- Saving and loading task state
|
||||
- Managing API configurations
|
||||
- Handling user authentication
|
||||
- Coordinating MCP server connections
|
||||
- Managing task history and checkpoints
|
||||
The `ClineProvider` handles the distribution of state to both the core extension and webview components. It also coordinates state across multiple extension instances, ensuring consistency.
|
||||
|
||||
### Webview State
|
||||
|
||||
@@ -140,65 +82,16 @@ The `ExtensionStateContext` in `webview-ui/src/context/ExtensionStateContext.tsx
|
||||
|
||||
It synchronizes with the core extension through VSCode's message passing system and provides type-safe access to the state via a custom hook (`useExtensionState`).
|
||||
|
||||
The ExtensionStateContext handles:
|
||||
- Real-time updates through message events
|
||||
- Partial message updates for streaming content
|
||||
- State modifications through setter methods
|
||||
- Type-safe access to state through a custom hook
|
||||
## Core Extension (Cline.ts)
|
||||
|
||||
## API Provider System
|
||||
|
||||
Cline supports multiple AI providers through a modular API provider system. Each provider is implemented as a separate module in the `src/api/providers/` directory and follows a common interface.
|
||||
|
||||
### API Provider Architecture
|
||||
|
||||
The API system consists of:
|
||||
|
||||
1. **API Handlers**: Provider-specific implementations in `src/api/providers/`
|
||||
2. **API Transformers**: Stream transformation utilities in `src/api/transform/`
|
||||
3. **API Configuration**: User settings for API keys and endpoints
|
||||
4. **API Factory**: Builder function to create the appropriate handler
|
||||
|
||||
Key providers include:
|
||||
- **Anthropic**: Direct integration with Claude models
|
||||
- **OpenRouter**: Meta-provider supporting multiple model providers
|
||||
- **AWS Bedrock**: Integration with Amazon's AI services
|
||||
- **Gemini**: Google's AI models
|
||||
- **Ollama**: Local model hosting
|
||||
- **LM Studio**: Local model hosting
|
||||
- **VSCode LM**: VSCode's built-in language models
|
||||
|
||||
### API Configuration Management
|
||||
|
||||
API configurations are stored securely:
|
||||
- API keys are stored in VSCode's secrets storage
|
||||
- Model selections and non-sensitive settings are stored in global state
|
||||
- The Controller manages switching between providers and updating configurations
|
||||
|
||||
The system supports:
|
||||
- Secure storage of API keys
|
||||
- Model selection and configuration
|
||||
- Automatic retry and error handling
|
||||
- Token usage tracking and cost calculation
|
||||
- Context window management
|
||||
|
||||
### Plan/Act Mode API Configuration
|
||||
|
||||
Cline supports separate model configurations for Plan and Act modes:
|
||||
- Different models can be used for planning vs. execution
|
||||
- The system preserves model selections when switching modes
|
||||
- The Controller handles the transition between modes and updates the API configuration accordingly
|
||||
|
||||
## Task Execution System
|
||||
|
||||
The Task class is responsible for executing AI requests and tool operations. Each task runs in its own instance of the Task class, ensuring isolation and proper state management.
|
||||
The Cline class is the heart of the extension, managing task execution, state persistence, and tool coordination. Each task runs in its own instance of the Cline class, ensuring isolation and proper state management.
|
||||
|
||||
### Task Execution Loop
|
||||
|
||||
The core task execution loop follows this pattern:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async initiateTaskLoop(userContent: UserContent, isNewTask: boolean) {
|
||||
while (!this.abort) {
|
||||
// 1. Make API request and stream response
|
||||
@@ -233,7 +126,7 @@ class Task {
|
||||
The streaming system handles real-time updates and partial content:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async presentAssistantMessage() {
|
||||
// Handle streaming locks to prevent race conditions
|
||||
if (this.presentAssistantMessageLocked) {
|
||||
@@ -268,7 +161,7 @@ class Task {
|
||||
Tools follow a strict execution pattern:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async executeToolWithApproval(block: ToolBlock) {
|
||||
// 1. Check auto-approval settings
|
||||
if (this.shouldAutoApproveTool(block.name)) {
|
||||
@@ -300,7 +193,7 @@ class Task {
|
||||
The system includes robust error handling:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async handleError(action: string, error: Error) {
|
||||
// 1. Check if task was abandoned
|
||||
if (this.abandoned) return
|
||||
@@ -323,23 +216,23 @@ class Task {
|
||||
|
||||
### API Request & Token Management
|
||||
|
||||
The Task class handles API requests with built-in retry, streaming, and token management:
|
||||
The Cline class handles API requests with built-in retry, streaming, and token management:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
|
||||
// 1. Wait for MCP servers to connect
|
||||
await pWaitFor(() => this.controllerRef.deref()?.mcpHub?.isConnecting !== true)
|
||||
await pWaitFor(() => this.providerRef.deref()?.mcpHub?.isConnecting !== true)
|
||||
|
||||
// 2. Manage context window
|
||||
const previousRequest = this.clineMessages[previousApiReqIndex]
|
||||
if (previousRequest?.text) {
|
||||
const { tokensIn, tokensOut } = JSON.parse(previousRequest.text || "{}")
|
||||
const { tokensIn, tokensOut } = JSON.parse(previousRequest.text)
|
||||
const totalTokens = (tokensIn || 0) + (tokensOut || 0)
|
||||
|
||||
// Truncate conversation if approaching context limit
|
||||
if (totalTokens >= maxAllowedSize) {
|
||||
this.conversationHistoryDeletedRange = this.contextManager.getNextTruncationRange(
|
||||
this.conversationHistoryDeletedRange = getNextTruncationRange(
|
||||
this.apiConversationHistory,
|
||||
this.conversationHistoryDeletedRange,
|
||||
totalTokens / 2 > maxAllowedSize ? "quarter" : "half"
|
||||
@@ -406,32 +299,16 @@ Key features:
|
||||
- Cost calculation
|
||||
- Cache hit monitoring
|
||||
|
||||
### Context Management System
|
||||
|
||||
The Context Management System handles conversation history truncation to prevent context window overflow errors. Implemented in the `ContextManager` class, it ensures long-running conversations remain within model context limits while preserving critical context.
|
||||
|
||||
Key features:
|
||||
|
||||
1. **Model-Aware Sizing**: Dynamically adjusts based on different model context windows (64K for DeepSeek, 128K for most models, 200K for Claude).
|
||||
|
||||
2. **Proactive Truncation**: Monitors token usage and preemptively truncates conversations when approaching limits, maintaining buffers of 27K-40K tokens depending on the model.
|
||||
|
||||
3. **Intelligent Preservation**: Always preserves the original task message and maintains the user-assistant conversation structure when truncating.
|
||||
|
||||
4. **Adaptive Strategies**: Uses different truncation strategies based on context pressure - removing half of the conversation for moderate pressure or three-quarters for severe pressure.
|
||||
|
||||
5. **Error Recovery**: Includes specialized detection for context window errors from different providers with automatic retry and more aggressive truncation when needed.
|
||||
|
||||
### Task State & Resumption
|
||||
|
||||
The Task class provides robust task state management and resumption capabilities:
|
||||
The Cline class provides robust task state management and resumption capabilities:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async resumeTaskFromHistory() {
|
||||
// 1. Load saved state
|
||||
this.clineMessages = await getSavedClineMessages(this.getContext(), this.taskId)
|
||||
this.apiConversationHistory = await getSavedApiConversationHistory(this.getContext(), this.taskId)
|
||||
this.clineMessages = await this.getSavedClineMessages()
|
||||
this.apiConversationHistory = await this.getSavedApiConversationHistory()
|
||||
|
||||
// 2. Handle interrupted tool executions
|
||||
const lastMessage = this.apiConversationHistory[this.apiConversationHistory.length - 1]
|
||||
@@ -461,14 +338,14 @@ class Task {
|
||||
|
||||
private async saveTaskState() {
|
||||
// Save conversation history
|
||||
await saveApiConversationHistory(this.getContext(), this.taskId, this.apiConversationHistory)
|
||||
await saveClineMessages(this.getContext(), this.taskId, this.clineMessages)
|
||||
await this.saveApiConversationHistory()
|
||||
await this.saveClineMessages()
|
||||
|
||||
// Create checkpoint
|
||||
const commitHash = await this.checkpointTracker?.commit()
|
||||
|
||||
// Update task history
|
||||
await this.controllerRef.deref()?.updateTaskHistory({
|
||||
await this.providerRef.deref()?.updateTaskHistory({
|
||||
id: this.taskId,
|
||||
ts: lastMessage.ts,
|
||||
task: taskMessage.text,
|
||||
@@ -504,54 +381,11 @@ Key aspects of task state management:
|
||||
- Resources are cleaned up properly
|
||||
- User is notified of state changes
|
||||
|
||||
## Plan/Act Mode System
|
||||
|
||||
Cline implements a dual-mode system that separates planning from execution:
|
||||
|
||||
### Mode Architecture
|
||||
|
||||
The Plan/Act mode system consists of:
|
||||
|
||||
1. **Mode State**: Stored in `chatSettings.mode` in the Controller's state
|
||||
2. **Mode Switching**: Handled by `togglePlanActModeWithChatSettings` in the Controller
|
||||
3. **Mode-specific Models**: Optional configuration to use different models for each mode
|
||||
4. **Mode-specific Prompting**: Different system prompts for planning vs. execution
|
||||
|
||||
### Mode Switching Process
|
||||
|
||||
When switching between modes:
|
||||
|
||||
1. The current model configuration is saved to mode-specific state
|
||||
2. The previous mode's model configuration is restored
|
||||
3. The Task instance is updated with the new mode
|
||||
4. The webview is notified of the mode change
|
||||
5. Telemetry events are captured for analytics
|
||||
|
||||
### Plan Mode
|
||||
|
||||
Plan mode is designed for:
|
||||
- Information gathering and context building
|
||||
- Asking clarifying questions
|
||||
- Creating detailed execution plans
|
||||
- Discussing approaches with the user
|
||||
|
||||
In Plan mode, the AI uses the `plan_mode_respond` tool to engage in conversational planning without executing actions.
|
||||
|
||||
### Act Mode
|
||||
|
||||
Act mode is designed for:
|
||||
- Executing the planned actions
|
||||
- Using tools to modify files, run commands, etc.
|
||||
- Implementing the solution
|
||||
- Providing results and completion feedback
|
||||
|
||||
In Act mode, the AI has access to all tools except `plan_mode_respond` and focuses on implementation rather than discussion.
|
||||
|
||||
## Data Flow & State Management
|
||||
|
||||
### Core Extension Role
|
||||
|
||||
The Controller acts as the single source of truth for all persistent state. It:
|
||||
The core extension (ClineProvider) acts as the single source of truth for all persistent state. It:
|
||||
- Manages VSCode global state and secrets storage
|
||||
- Coordinates state updates between components
|
||||
- Ensures state consistency across webview reloads
|
||||
@@ -560,10 +394,10 @@ The Controller acts as the single source of truth for all persistent state. It:
|
||||
|
||||
### Terminal Management
|
||||
|
||||
The Task class manages terminal instances and command execution:
|
||||
The Cline class manages terminal instances and command execution:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async executeCommandTool(command: string): Promise<[boolean, ToolResponse]> {
|
||||
// 1. Get or create terminal
|
||||
const terminalInfo = await this.terminalManager.getOrCreateTerminal(cwd)
|
||||
@@ -619,10 +453,10 @@ Key features:
|
||||
|
||||
### Browser Session Management
|
||||
|
||||
The Task class handles browser automation through Puppeteer:
|
||||
The Cline class handles browser automation through Puppeteer:
|
||||
|
||||
```typescript
|
||||
class Task {
|
||||
class Cline {
|
||||
async executeBrowserAction(action: BrowserAction): Promise<BrowserActionResult> {
|
||||
switch (action) {
|
||||
case "launch":
|
||||
@@ -659,93 +493,13 @@ Key aspects:
|
||||
- Screenshot capture
|
||||
- Error recovery
|
||||
|
||||
## MCP (Model Context Protocol) Integration
|
||||
|
||||
### MCP Architecture
|
||||
|
||||
The MCP system consists of:
|
||||
|
||||
1. **McpHub Class**: Central manager in `src/services/mcp/McpHub.ts`
|
||||
2. **MCP Connections**: Manages connections to external MCP servers
|
||||
3. **MCP Settings**: Configuration stored in a JSON file
|
||||
4. **MCP Marketplace**: Online catalog of available MCP servers
|
||||
5. **MCP Tools & Resources**: Capabilities exposed by connected servers
|
||||
|
||||
The McpHub class:
|
||||
- Manages the lifecycle of MCP server connections
|
||||
- Handles server configuration through a settings file
|
||||
- Provides methods for calling tools and accessing resources
|
||||
- Implements auto-approval settings for MCP tools
|
||||
- Monitors server health and handles reconnection
|
||||
|
||||
### MCP Server Types
|
||||
|
||||
Cline supports two types of MCP server connections:
|
||||
- **Stdio**: Command-line based servers that communicate via standard I/O
|
||||
- **SSE**: HTTP-based servers that communicate via Server-Sent Events
|
||||
|
||||
### MCP Server Management
|
||||
|
||||
The McpHub class provides methods for:
|
||||
- Discovering and connecting to MCP servers
|
||||
- Monitoring server health and status
|
||||
- Restarting servers when needed
|
||||
- Managing server configurations
|
||||
- Setting timeouts and auto-approval rules
|
||||
|
||||
### MCP Tool Integration
|
||||
|
||||
MCP tools are integrated into the Task execution system:
|
||||
- Tools are discovered and registered at connection time
|
||||
- The Task class can call MCP tools through the McpHub
|
||||
- Tool results are streamed back to the AI
|
||||
- Auto-approval settings can be configured per tool
|
||||
|
||||
### MCP Marketplace
|
||||
|
||||
The MCP Marketplace provides:
|
||||
- A catalog of available MCP servers
|
||||
- One-click installation
|
||||
- README previews
|
||||
- Server status monitoring
|
||||
|
||||
The Controller class manages MCP servers through the McpHub service:
|
||||
|
||||
```typescript
|
||||
class Controller {
|
||||
mcpHub?: McpHub
|
||||
|
||||
constructor(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, webviewProvider: WebviewProvider) {
|
||||
this.mcpHub = new McpHub(this)
|
||||
}
|
||||
|
||||
async downloadMcp(mcpId: string) {
|
||||
// Fetch server details from marketplace
|
||||
const response = await axios.post<McpDownloadResponse>(
|
||||
"https://api.cline.bot/v1/mcp/download",
|
||||
{ mcpId },
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
timeout: 10000,
|
||||
}
|
||||
)
|
||||
|
||||
// Create task with context from README
|
||||
const task = `Set up the MCP server from ${mcpDetails.githubUrl}...`
|
||||
|
||||
// Initialize task and show chat view
|
||||
await this.initClineWithTask(task)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
This guide provides a comprehensive overview of the Cline extension architecture, with special focus on state management, data persistence, and code organization. Following these patterns ensures robust feature implementation with proper state handling across the extension's components.
|
||||
|
||||
Remember:
|
||||
- Always persist important state in the extension
|
||||
- The core extension follows a WebviewProvider -> Controller -> Task flow
|
||||
- The core extension exists in the src/ folder
|
||||
- Use proper typing for all state and messages
|
||||
- Handle errors and edge cases
|
||||
- Test state persistence across webview reloads
|
||||
@@ -31,14 +31,6 @@ body:
|
||||
label: Relevant API REQUEST output
|
||||
description: Please copy and paste any relevant output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: input
|
||||
id: provider-model
|
||||
attributes:
|
||||
label: Provider/Model
|
||||
description: What provider and model were you using when the issue occurred?
|
||||
placeholder: "e.g., cline:anthropic/claude-3.7-sonnet, gemini:gemini-2.5-pro-exp-03-25"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: operating-system
|
||||
attributes:
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
- [ ] 🐛 Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] ✨ New feature (non-breaking change which adds functionality)
|
||||
- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] ♻️ Refactor Changes
|
||||
- [ ] 💅 Cosmetic Changes
|
||||
- [ ] 📚 Documentation update
|
||||
- [ ] 🏃 Workflow Changes
|
||||
|
||||
### Pre-flight Checklist
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ name: Changeset Converter
|
||||
run-name: Changeset Conversion
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
@@ -14,36 +13,16 @@ env:
|
||||
jobs:
|
||||
# Job 1: Create version bump PR when changesets are merged to main
|
||||
changeset-pr-version-bump:
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.base.ref == 'main' &&
|
||||
github.actor != 'github-actions'
|
||||
)
|
||||
if: >
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.base.ref == 'main' &&
|
||||
github.actor != 'github-actions'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check user for team affiliation
|
||||
id: team_check
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
uses: morfien101/actions-authorized-user@4a3cfbf0bcb3cafe4a71710a278920c5d94bb38b
|
||||
with:
|
||||
username: ${{ github.actor }}
|
||||
team: "deployer"
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check if user is authorized
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
if [ "${{ steps.team_check.outputs.authorized }}" != "true" ]; then
|
||||
echo "User is not authorized to run this workflow."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Git Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -73,10 +73,6 @@ jobs:
|
||||
- name: Build Extension
|
||||
run: npm run compile
|
||||
|
||||
# Disabling due to compatability with test framework and ESM modules
|
||||
# - name: Unit Tests
|
||||
# run: npm run test:unit
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Tests with Coverage
|
||||
id: extension_coverage
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "src/**/__tests__/*.ts",
|
||||
"require": ["ts-node/register", "source-map-support/register"],
|
||||
"recursive": true
|
||||
}
|
||||
Vendored
-13
@@ -16,19 +16,6 @@
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Run Extension (Test Mode)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"IS_TEST": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
-51
@@ -14,14 +14,6 @@
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "watch:test",
|
||||
"dependsOn": ["npm: build:webview:test", "npm: dev:webview", "npm: watch:tsc", "npm: watch:esbuild:test"],
|
||||
"presentation": {
|
||||
"reveal": "never"
|
||||
},
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview",
|
||||
@@ -40,25 +32,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build:webview:test",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
"label": "npm: build:webview:test",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"IS_TEST": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "dev:webview",
|
||||
@@ -104,30 +77,6 @@
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
"IS_DEV": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch:esbuild:test",
|
||||
"group": "build",
|
||||
"problemMatcher": "$esbuild-watch",
|
||||
"isBackground": true,
|
||||
"label": "npm: watch:esbuild:test",
|
||||
"presentation": {
|
||||
"group": "watch",
|
||||
"reveal": "never",
|
||||
"close": true
|
||||
},
|
||||
"options": {
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"IS_TEST": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+6
-87
@@ -1,86 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [3.12.3]
|
||||
|
||||
- Add copy button to MermaidBlock component (Thanks @cacosub7!)
|
||||
- Add the ability to fetch from global cline rules files
|
||||
- Add icon to indicate when a file outside of the users workspace is edited
|
||||
|
||||
## [3.12.2]
|
||||
|
||||
- Add gpt-4.1
|
||||
|
||||
## [3.12.1]
|
||||
|
||||
- Use visual checkpoint indicator to make it clear when checkpoints are created
|
||||
- Big shoutout to @samuel871211 for numerous code quality improvements, refactoring contributions, and webview performance improvements!
|
||||
- Use improved context manager
|
||||
|
||||
## [3.12.0]
|
||||
|
||||
- Add favorite toggles for models when using the Cline & OpenRouter providers
|
||||
- Add auto-approve options for edits/reads outside of the workspace
|
||||
- Improve diff editing animation for large files
|
||||
- Add indicator showing number of diff edits when Cline edits a file
|
||||
- Add streaming support and reasoning effort option to xAI's Grok 3 Mini
|
||||
- Add settings button to MCP popover to easily modify installed servers
|
||||
- Fix bug where browser tool actions would show unparsed results in the chat view
|
||||
- Fix issue with new checkpoints popover hiding too quickly
|
||||
- Fix duplicate checkpoints bug
|
||||
- Improve Ollama provider with retry mechanism, timeout handling, and improved error handling (thanks suvarchal!)
|
||||
|
||||
## [3.11.0]
|
||||
|
||||
- Redesign checkpoint UI to declutter chat view by using a subtle indicator line that expands to a popover on hover, with a new date indicator for when it was created
|
||||
- Add support for xAI's provider's Grok 3 models
|
||||
- Add more robust error tracking for users opted in to telemetry (thank you for helping us make Cline better!)
|
||||
|
||||
## [3.10.1]
|
||||
|
||||
- Add CMD+' keyboard shortcut to add selected text to Cline
|
||||
- Cline now auto focuses the text field when using 'Add to Cline' shortcut
|
||||
- Add new 'Create New Task' tool to let Cline start a new task autonomously!
|
||||
- Fix Mermaid diagram issues
|
||||
- Fix Gemini provider cost calculation to take new tiered pricing structure into account
|
||||
|
||||
## [3.10.0]
|
||||
|
||||
- Add setting to let browser tool use local Chrome via remote debugging, enabling session-based browsing. Replaces sessionless Chromium, unlocking debugging and productivity workflows tied to your real browser state.
|
||||
- Add new auto-approve option to approve _ALL_ commands (use at your own risk!)
|
||||
- Add modal in the chat area to more easily enable or disable MCP servers
|
||||
- Add drag and drop of file/folders into cline chat (Thanks eljapi!)
|
||||
- Add prompt caching for LiteLLM + Claude (Thanks sammcj!)
|
||||
- Add Improved context management
|
||||
- Fix MCP auto approve toggle issues being out of sync with settings
|
||||
|
||||
## [3.9.2]
|
||||
|
||||
- Add recommended models for Cline provider
|
||||
- Add ability to detect when user edits files manually so Cline knows to re-read, leading to reduced diff edit errors
|
||||
- Add improvements to file mention searching for faster searching
|
||||
- Add scoring logic to file mentions to sort and exlcude results based on relevance
|
||||
- Add Support for Bytedance Doubao (Thanks Tunixer!)
|
||||
- Fix to prevent duplicate BOM (Thanks bamps53!)
|
||||
|
||||
## [3.9.1]
|
||||
|
||||
- Add Gemini 2.5 Pro Preview 03-25 to Google Provider
|
||||
|
||||
## [3.9.0]
|
||||
|
||||
- Add Enable extended thinking for LiteLLM provider (Thanks @jorgegarciarey!)
|
||||
- Add a tab for configuring local MCP Servers
|
||||
- Fix issue with DeepSeek API provider token counting + context management
|
||||
- Fix issues with checkpoints hanging under certain conditions
|
||||
|
||||
## [3.8.6]
|
||||
|
||||
- Add UI for adding remote servers
|
||||
- Add Mentions Feature Guide and update related documentation
|
||||
- Fix bug where menu would open in sidebar and open tab
|
||||
- Fix issue with Cline accounts not showing user info in popout tabs
|
||||
- Fix bug where menu buttons wouldn't open view in sidebar
|
||||
|
||||
## [3.8.5]
|
||||
|
||||
- Add support for remote MCP Servers using SSE
|
||||
@@ -303,8 +222,8 @@
|
||||
## [3.1.0]
|
||||
|
||||
- Added checkpoints: Snapshots of workspace are automatically created whenever Cline uses a tool
|
||||
- Compare changes: Hover over any tool use to see a diff between the snapshot and current workspace state
|
||||
- Restore options: Choose to restore just the task state, just the workspace files, or both
|
||||
- Compare changes: Hover over any tool use to see a diff between the snapshot and current workspace state
|
||||
- Restore options: Choose to restore just the task state, just the workspace files, or both
|
||||
- New 'See new changes' button appears after task completion, providing an overview of all workspace changes
|
||||
- Task header now shows disk space usage with a delete button to help manage snapshot storage
|
||||
|
||||
@@ -486,10 +405,10 @@
|
||||
## [1.8.0]
|
||||
|
||||
- You can now use '@' in the textarea to add context!
|
||||
- @url: Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Claude the latest docs!
|
||||
- @problems: Add workspace errors and warnings for Claude to fix, no more back-and-forth about debugging
|
||||
- @file: Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
|
||||
- @folder: Adds folder's files all at once to speed up your workflow even more
|
||||
- @url: Paste in a URL for the extension to fetch and convert to markdown, useful when you want to give Claude the latest docs!
|
||||
- @problems: Add workspace errors and warnings for Claude to fix, no more back-and-forth about debugging
|
||||
- @file: Adds a file's contents so you don't have to waste API requests approving read file (+ type to search files)
|
||||
- @folder: Adds folder's files all at once to speed up your workflow even more
|
||||
|
||||
## [1.7.0]
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">繁體中文</a> | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
|
||||
</sub></div>
|
||||
|
||||
# Cline
|
||||
# Cline – \#1 on OpenRouter
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
@@ -24,7 +24,7 @@ English | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md
|
||||
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>Feature Requests</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>Getting Started</strong></a>
|
||||
<a href="https://docs.cline.bot/getting-started/getting-started-new-coders" target="_blank"><strong>Getting Started</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -18,7 +18,6 @@ Welcome to the Cline documentation - your comprehensive guide to using and exten
|
||||
- **Understand Cline's capabilities:**
|
||||
|
||||
- [Cline Tools Guide](tools/cline-tools-guide.md)
|
||||
- [Mentions Feature Guide](tools/mentions-guide.md)
|
||||
|
||||
- **Extend Cline with MCP Servers:**
|
||||
- [MCP Overview](mcp/README.md)
|
||||
|
||||
@@ -1,67 +1,41 @@
|
||||
flowchart TB
|
||||
subgraph "VSCode Extension Host"
|
||||
subgraph "Core Extension"
|
||||
ExtensionEntry["Extension Entry<br/>src/extension.ts"]
|
||||
WebviewProvider["WebviewProvider<br/>src/core/webview/index.ts"]
|
||||
Controller["Controller<br/>src/core/controller/index.ts"]
|
||||
Task["Task<br/>src/core/task/index.ts"]
|
||||
GlobalState["VSCode Global State"]
|
||||
SecretsStorage["VSCode Secrets Storage"]
|
||||
McpHub["McpHub<br/>src/services/mcp/McpHub.ts"]
|
||||
graph TB
|
||||
subgraph VSCode Extension Host
|
||||
subgraph Core Extension
|
||||
ExtensionEntry[Extension Entry<br/>src/extension.ts]
|
||||
ClineProvider[ClineProvider<br/>src/core/webview/ClineProvider.ts]
|
||||
ClineClass[Cline Class<br/>src/core/Cline.ts]
|
||||
GlobalState[VSCode Global State]
|
||||
SecretsStorage[VSCode Secrets Storage]
|
||||
end
|
||||
|
||||
subgraph "Webview UI"
|
||||
WebviewApp["React App<br/>webview-ui/src/App.tsx"]
|
||||
ExtStateContext["ExtensionStateContext<br/>webview-ui/src/context/ExtensionStateContext.tsx"]
|
||||
ReactComponents["React Components"]
|
||||
subgraph Webview UI
|
||||
WebviewApp[React App<br/>webview-ui/src/App.tsx]
|
||||
ExtStateContext[ExtensionStateContext<br/>webview-ui/src/context/ExtensionStateContext.tsx]
|
||||
ReactComponents[React Components]
|
||||
end
|
||||
|
||||
subgraph "Storage"
|
||||
TaskStorage["Task Storage<br/>Per-Task Files & History"]
|
||||
CheckpointSystem["Git-based Checkpoints"]
|
||||
end
|
||||
|
||||
subgraph "API Providers"
|
||||
AnthropicAPI["Anthropic"]
|
||||
OpenRouterAPI["OpenRouter"]
|
||||
BedrockAPI["AWS Bedrock"]
|
||||
OtherAPIs["Other Providers"]
|
||||
end
|
||||
|
||||
subgraph "MCP Servers"
|
||||
ExternalMcpServers["External MCP Servers"]
|
||||
subgraph Storage
|
||||
TaskStorage[Task Storage<br/>Per-Task Files & History]
|
||||
CheckpointSystem[Git-based Checkpoints]
|
||||
end
|
||||
end
|
||||
|
||||
%% Core Extension Data Flow
|
||||
ExtensionEntry --> WebviewProvider
|
||||
WebviewProvider --> Controller
|
||||
Controller --> Task
|
||||
Controller --> McpHub
|
||||
Task --> GlobalState
|
||||
Task --> SecretsStorage
|
||||
Task --> TaskStorage
|
||||
Task --> CheckpointSystem
|
||||
Task --> |"API Requests"| AnthropicAPI
|
||||
Task --> |"API Requests"| OpenRouterAPI
|
||||
Task --> |"API Requests"| BedrockAPI
|
||||
Task --> |"API Requests"| OtherAPIs
|
||||
McpHub --> |"Connects to"| ExternalMcpServers
|
||||
Task --> |"Uses"| McpHub
|
||||
ExtensionEntry --> ClineProvider
|
||||
ClineProvider --> ClineClass
|
||||
ClineClass --> GlobalState
|
||||
ClineClass --> SecretsStorage
|
||||
ClineClass --> TaskStorage
|
||||
ClineClass --> CheckpointSystem
|
||||
|
||||
%% Webview Data Flow
|
||||
WebviewApp --> ExtStateContext
|
||||
ExtStateContext --> ReactComponents
|
||||
|
||||
%% Bidirectional Communication
|
||||
WebviewProvider <--> |"postMessage"| ExtStateContext
|
||||
ClineProvider <-->|postMessage| ExtStateContext
|
||||
|
||||
classDef vscodeState fill:#f9f,stroke:#333,stroke-width:2px
|
||||
classDef contextClass fill:#bbf,stroke:#333,stroke-width:2px
|
||||
classDef providerClass fill:#bfb,stroke:#333,stroke-width:2px
|
||||
classDef apiClass fill:#fdb,stroke:#333,stroke-width:2px
|
||||
|
||||
class GlobalState,SecretsStorage vscodeState
|
||||
class ExtStateContext contextClass
|
||||
class WebviewProvider,McpHub providerClass
|
||||
class AnthropicAPI,OpenRouterAPI,BedrockAPI,OtherAPIs apiClass
|
||||
style GlobalState fill:#ff0066,stroke:#333,stroke-width:2px,color:#ffffff
|
||||
style SecretsStorage fill:#ff0066,stroke:#333,stroke-width:2px,color:#ffffff
|
||||
style ExtStateContext fill:#0066ff,stroke:#333,stroke-width:2px,color:#ffffff
|
||||
style ClineProvider fill:#00cc66,stroke:#333,stroke-width:2px,color:#ffffff
|
||||
|
||||
@@ -20,9 +20,8 @@ Cline is your AI assistant that can:
|
||||
|
||||
2. **Provide Context**
|
||||
|
||||
- Use @ mentions to add files, folders, URLs, diagnostics, terminal output, and more
|
||||
- Example: "@/src/components/App.tsx"
|
||||
- See the [Mentions Feature Guide](./mentions-guide.md) for details
|
||||
- Use @ mentions to add files, folders, or URLs
|
||||
- Example: "@file:src/components/App.tsx"
|
||||
|
||||
3. **Review Changes**
|
||||
- Cline will show diffs before making changes
|
||||
@@ -55,7 +54,7 @@ Cline is your AI assistant that can:
|
||||
|
||||
## Available Tools
|
||||
|
||||
For the most up-to-date implementation details, you can view the full source code in the [Cline repository](https://github.com/cline/cline/blob/main/src/core/task/index.ts).
|
||||
For the most up-to-date implementation details, you can view the full source code in the [Cline repository](https://github.com/cline/cline/blob/main/src/core/Cline.ts).
|
||||
|
||||
Cline has access to the following tools for various tasks:
|
||||
|
||||
@@ -82,7 +81,6 @@ Cline has access to the following tools for various tasks:
|
||||
4. **Interaction Tools**
|
||||
- `ask_followup_question`: Ask user for clarification
|
||||
- `attempt_completion`: Present final results
|
||||
- `new_task`: Start a new task with preloaded context
|
||||
|
||||
Each tool has specific parameters and usage patterns. Here are some examples:
|
||||
|
||||
@@ -115,21 +113,6 @@ Each tool has specific parameters and usage patterns. Here are some examples:
|
||||
</execute_command>
|
||||
```
|
||||
|
||||
- Start a new task with context (new_task):
|
||||
```xml
|
||||
<new_task>
|
||||
<context>
|
||||
We've completed the backend API with these endpoints:
|
||||
- GET /api/tasks
|
||||
- POST /api/tasks
|
||||
- PUT /api/tasks/:id
|
||||
- DELETE /api/tasks/:id
|
||||
|
||||
Now we need to implement the React frontend.
|
||||
</context>
|
||||
</new_task>
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
1. **Create a New Component**
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
# Cline Mentions Feature Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The mentions feature is a powerful capability that allows you to reference various resources in your conversations with Cline using the "@" symbol. This includes file contents, directory structures, webpage URLs, VSCode diagnostic information, terminal output, Git change status, and more - all easily incorporated into your conversations.
|
||||
|
||||
By using this feature, Cline can gain more accurate context and provide more relevant assistance for your tasks.
|
||||
|
||||
## Basic Syntax
|
||||
|
||||
Mentions always start with the "@" symbol, followed by the path or identifier of the resource you want to reference:
|
||||
|
||||
```
|
||||
@resource_identifier
|
||||
```
|
||||
|
||||
You can place mentions anywhere in your user messages, and Cline will automatically retrieve the referenced content.
|
||||
|
||||
## Supported Mention Types
|
||||
|
||||
### 1. File References
|
||||
|
||||
To reference file contents, use `@/` followed by the relative path within your project:
|
||||
|
||||
```
|
||||
@/path/to/file.js
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Please analyze the implementation in @/src/components/Button.tsx
|
||||
```
|
||||
|
||||
In this example, Cline automatically retrieves the contents of Button.tsx and uses it to perform the analysis.
|
||||
|
||||
### 2. Directory References
|
||||
|
||||
To reference directory contents, use `@/` followed by the relative path of the directory, ending with a trailing `/`:
|
||||
|
||||
```
|
||||
@/path/to/directory/
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
What components are available in the @/src/components/ directory?
|
||||
```
|
||||
|
||||
In this example, Cline retrieves a listing of the components directory and its contents.
|
||||
|
||||
### 3. URL References
|
||||
|
||||
To reference web page contents, use `@` followed by the URL:
|
||||
|
||||
```
|
||||
@https://example.com
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Please parse the JSON response from @https://api.github.com/users/octocat
|
||||
```
|
||||
|
||||
In this example, Cline fetches the response from the GitHub API and analyzes the JSON.
|
||||
|
||||
### 4. Diagnostic References
|
||||
|
||||
To reference VSCode diagnostic information (errors and warnings) in the current workspace, use `@problems`:
|
||||
|
||||
```
|
||||
@problems
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Check @problems and tell me which errors I should prioritize fixing
|
||||
```
|
||||
|
||||
In this example, Cline retrieves the current errors and warnings from your workspace and identifies high-priority issues.
|
||||
|
||||
### 5. Terminal Output References
|
||||
|
||||
To reference the latest terminal output, use `@terminal`:
|
||||
|
||||
```
|
||||
@terminal
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Please identify the cause of the error in the @terminal output
|
||||
```
|
||||
|
||||
In this example, Cline examines the latest terminal output and analyzes the error's cause.
|
||||
|
||||
### 6. Git Working Directory References
|
||||
|
||||
To reference the current Git working directory change status, use `@git-changes`:
|
||||
|
||||
```
|
||||
@git-changes
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Review the @git-changes and summarize the important changes that should be committed
|
||||
```
|
||||
|
||||
In this example, Cline retrieves the list of changed files in the current Git working directory and identifies candidates for commit.
|
||||
|
||||
### 7. Git Commit References
|
||||
|
||||
To reference information about a specific Git commit, use `@` followed by the commit hash:
|
||||
|
||||
```
|
||||
@commit_hash
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Analyze the commit @abcd123 and explain what changes were made
|
||||
```
|
||||
|
||||
In this example, Cline retrieves information about the specified commit hash and analyzes the changes made in that commit.
|
||||
|
||||
## Usage Scenarios
|
||||
|
||||
### Code Review
|
||||
|
||||
```
|
||||
Check @/src/components/Form.jsx and suggest improvements from a performance perspective. Also, if there are any @problems, please suggest how to fix them.
|
||||
```
|
||||
|
||||
### Debugging Assistance
|
||||
|
||||
```
|
||||
My npm install failed. Please examine the @terminal output and suggest a solution to the problem.
|
||||
```
|
||||
|
||||
### Project Analysis
|
||||
|
||||
```
|
||||
Analyze the code in the @/src/models/ directory and explain the relationships between the data models. Also, tell me how the utility functions in @/src/utils/ are used with these models.
|
||||
```
|
||||
|
||||
### Code Generation
|
||||
|
||||
```
|
||||
Create a new Input.tsx component using the same design language as @/src/components/Button.tsx
|
||||
```
|
||||
|
||||
### Version Control Integration
|
||||
|
||||
```
|
||||
Review the @git-changes and suggest a commit message for the feature I'm working on.
|
||||
```
|
||||
|
||||
## Combining Multiple Mentions
|
||||
|
||||
You can combine multiple mentions to provide more complex context:
|
||||
|
||||
```
|
||||
There seems to be a bug in @/src/api/users.js. Please check @problems and @terminal to identify and fix the issue.
|
||||
```
|
||||
|
||||
## Limitations and Considerations
|
||||
|
||||
1. **Large Files**: Referencing very large files may take time to process and could consume a significant amount of tokens.
|
||||
|
||||
2. **Binary Files**: Binary files (such as images) will not be properly processed and will show a "Binary file" message.
|
||||
|
||||
3. **Directory Structure**: Directory references will only show top-level files and directories, not recursively showing the contents of subdirectories.
|
||||
|
||||
4. **URL Limitations**: Some websites may block automated crawling, which could prevent accurate content retrieval.
|
||||
|
||||
5. **Path Syntax**: File paths or URLs with special characters (such as spaces) may not be recognized correctly.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Mentions Not Recognized
|
||||
|
||||
If your mentions aren't being recognized correctly, check that:
|
||||
|
||||
- There's no space after the `@` symbol
|
||||
- File paths are accurate (case-sensitive)
|
||||
- URLs include the full format (with `https://`)
|
||||
|
||||
### Content Not Retrieved
|
||||
|
||||
If the content of referenced resources can't be retrieved:
|
||||
|
||||
- Verify the file exists
|
||||
- Ensure you have access permissions for the file
|
||||
- Check that the file isn't too large or the URL too complex
|
||||
|
||||
### Performance Issues
|
||||
|
||||
If mention processing is slow:
|
||||
|
||||
- Reference smaller files or specific file sections
|
||||
- Reduce the number of mentions used at once
|
||||
|
||||
## Conclusion
|
||||
|
||||
Mastering the mentions feature makes your communication with Cline more efficient. By providing appropriate context, Cline can deliver more accurate assistance, significantly improving your development workflow.
|
||||
@@ -4,7 +4,6 @@ const path = require("path")
|
||||
|
||||
const production = process.argv.includes("--production")
|
||||
const watch = process.argv.includes("--watch")
|
||||
const test = process.env.IS_TEST === "true"
|
||||
|
||||
/**
|
||||
* @type {import('esbuild').Plugin}
|
||||
@@ -69,10 +68,6 @@ const extensionConfig = {
|
||||
minify: production,
|
||||
sourcemap: !production,
|
||||
logLevel: "silent",
|
||||
define: {
|
||||
"process.env.IS_DEV": JSON.stringify(!production),
|
||||
"process.env.IS_TEST": JSON.stringify(test),
|
||||
},
|
||||
plugins: [
|
||||
copyWasmFiles,
|
||||
/* add to the end of plugins array */
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
repositories
|
||||
|
||||
results/evals.db
|
||||
-162
@@ -1,162 +0,0 @@
|
||||
# Cline Evaluation System
|
||||
|
||||
This directory contains the evaluation system for benchmarking Cline against various coding evaluation frameworks.
|
||||
|
||||
## Overview
|
||||
|
||||
The Cline Evaluation System allows you to:
|
||||
|
||||
1. Run Cline against standardized coding benchmarks
|
||||
2. Collect comprehensive metrics on performance
|
||||
3. Generate detailed reports on evaluation results
|
||||
4. Compare performance across different models and benchmarks
|
||||
|
||||
## Architecture
|
||||
|
||||
The evaluation system consists of two main components:
|
||||
|
||||
1. **Test Server**: Enhanced HTTP server in `src/services/test/TestServer.ts` that provides detailed task results
|
||||
2. **CLI Tool**: Command-line interface in `evals/cli/` for orchestrating evaluations
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
cline-repo/
|
||||
├── src/
|
||||
│ ├── services/
|
||||
│ │ ├── test/
|
||||
│ │ │ ├── TestServer.ts # Enhanced HTTP server for task execution
|
||||
│ │ │ ├── GitHelper.ts # Git utilities for file tracking
|
||||
│ │ │ └── ...
|
||||
│ │ └── ...
|
||||
│ └── ...
|
||||
├── evals/ # Main directory for evaluation system
|
||||
│ ├── cli/ # CLI tool for orchestrating evaluations
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── index.ts # CLI entry point
|
||||
│ │ │ ├── commands/ # CLI commands (setup, run, report)
|
||||
│ │ │ ├── adapters/ # Benchmark adapters
|
||||
│ │ │ ├── db/ # Database management
|
||||
│ │ │ └── utils/ # Utility functions
|
||||
│ │ ├── package.json
|
||||
│ │ └── tsconfig.json
|
||||
│ ├── repositories/ # Cloned benchmark repositories
|
||||
│ │ ├── exercism/ # Modified Exercism (from pashpashpash/evals)
|
||||
│ │ ├── swe-bench/ # SWE-Bench repository
|
||||
│ │ ├── swelancer/ # SWELancer repository
|
||||
│ │ └── multi-swe/ # Multi-SWE-Bench repository
|
||||
│ ├── results/ # Evaluation results storage
|
||||
│ │ ├── runs/ # Individual run results
|
||||
│ │ └── reports/ # Generated reports
|
||||
│ └── README.md # This file
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 16+
|
||||
- VSCode with Cline extension installed
|
||||
- Git
|
||||
|
||||
### Installation
|
||||
|
||||
1. Build the CLI tool:
|
||||
|
||||
```bash
|
||||
cd evals/cli
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
#### Setting Up Benchmarks
|
||||
|
||||
```bash
|
||||
cd evals/cli
|
||||
node dist/index.js setup
|
||||
```
|
||||
|
||||
This will clone and set up all benchmark repositories. You can specify specific benchmarks:
|
||||
|
||||
```bash
|
||||
node dist/index.js setup --benchmarks exercism
|
||||
```
|
||||
|
||||
#### Running Evaluations
|
||||
|
||||
```bash
|
||||
node dist/index.js run --model claude-3-opus-20240229 --benchmark exercism
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--model`: The model to evaluate (default: claude-3-opus-20240229)
|
||||
- `--benchmark`: Specific benchmark to run (default: all)
|
||||
- `--count`: Number of tasks to run (default: all)
|
||||
|
||||
#### Generating Reports
|
||||
|
||||
```bash
|
||||
node dist/index.js report
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--format`: Report format (json, markdown) (default: markdown)
|
||||
- `--output`: Output path for the report
|
||||
|
||||
## Benchmarks
|
||||
|
||||
### Exercism
|
||||
|
||||
Modified Exercism exercises from the [pashpashpash/evals](https://github.com/pashpashpash/evals) repository. These are small, focused programming exercises in various languages.
|
||||
|
||||
### SWE-Bench (Coming Soon)
|
||||
|
||||
Real-world software engineering tasks from the [SWE-bench](https://github.com/SWE-bench/SWE-bench) repository.
|
||||
|
||||
### SWELancer (Coming Soon)
|
||||
|
||||
Freelance-style programming tasks from the SWELancer benchmark.
|
||||
|
||||
### Multi-SWE-Bench (Coming Soon)
|
||||
|
||||
Multi-file software engineering tasks from the Multi-SWE-Bench repository.
|
||||
|
||||
## Metrics
|
||||
|
||||
The evaluation system collects the following metrics:
|
||||
|
||||
- **Token Usage**: Input and output tokens
|
||||
- **Cost**: Estimated cost of API calls
|
||||
- **Duration**: Time taken to complete tasks
|
||||
- **Tool Usage**: Number of tool calls and failures
|
||||
- **Success Rate**: Percentage of tasks completed successfully
|
||||
- **Functional Correctness**: Percentage of tests passed
|
||||
|
||||
## Reports
|
||||
|
||||
Reports are generated in Markdown or JSON format and include:
|
||||
|
||||
- Overall summary
|
||||
- Benchmark-specific results
|
||||
- Model-specific results
|
||||
- Tool usage statistics
|
||||
- Charts and visualizations
|
||||
|
||||
## Development
|
||||
|
||||
### Adding a New Benchmark
|
||||
|
||||
1. Create a new adapter in `evals/cli/src/adapters/`
|
||||
2. Implement the `BenchmarkAdapter` interface
|
||||
3. Register the adapter in `evals/cli/src/adapters/index.ts`
|
||||
|
||||
### Extending Metrics
|
||||
|
||||
To add new metrics:
|
||||
|
||||
1. Update the database schema in `evals/cli/src/db/schema.ts`
|
||||
2. Add collection logic in `evals/cli/src/utils/results.ts`
|
||||
3. Update report generation in `evals/cli/src/commands/report.ts`
|
||||
Generated
-2455
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"name": "cline-evaluation-cli",
|
||||
"version": "0.1.0",
|
||||
"description": "CLI tool for orchestrating Cline evaluations across multiple benchmarks",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"cline",
|
||||
"evaluation",
|
||||
"benchmark"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.0.0",
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^9.4.1",
|
||||
"execa": "^5.1.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"ora": "^5.4.1",
|
||||
"sqlite": "^4.1.2",
|
||||
"uuid": "^9.0.0",
|
||||
"yargs": "^17.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.3",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/yargs": "^17.0.19",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import execa from "execa"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
|
||||
/**
|
||||
* Adapter for the modified Exercism benchmark
|
||||
*/
|
||||
export class ExercismAdapter implements BenchmarkAdapter {
|
||||
name = "exercism"
|
||||
|
||||
/**
|
||||
* Set up the Exercism benchmark repository
|
||||
*/
|
||||
async setup(): Promise<void> {
|
||||
// Clone repository if needed
|
||||
const exercismDir = path.join(EVALS_DIR, "repositories", "exercism")
|
||||
|
||||
if (!fs.existsSync(exercismDir)) {
|
||||
console.log(`Cloning Exercism repository to ${exercismDir}...`)
|
||||
await execa("git", ["clone", "https://github.com/pashpashpash/evals.git", exercismDir])
|
||||
console.log("Exercism repository cloned successfully")
|
||||
} else {
|
||||
console.log(`Exercism repository already exists at ${exercismDir}`)
|
||||
|
||||
// Pull latest changes
|
||||
console.log("Pulling latest changes...")
|
||||
await execa("git", ["pull"], { cwd: exercismDir })
|
||||
console.log("Repository updated successfully")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available tasks in the Exercism benchmark
|
||||
*/
|
||||
async listTasks(): Promise<Task[]> {
|
||||
const tasks: Task[] = []
|
||||
const exercisesDir = path.join(EVALS_DIR, "repositories", "exercism")
|
||||
|
||||
// Ensure the repository exists
|
||||
if (!fs.existsSync(exercisesDir)) {
|
||||
throw new Error(`Exercism repository not found at ${exercisesDir}. Run setup first.`)
|
||||
}
|
||||
|
||||
// Read language directories
|
||||
const languages = fs
|
||||
.readdirSync(exercisesDir)
|
||||
.filter((dir) => fs.statSync(path.join(exercisesDir, dir)).isDirectory())
|
||||
.filter((dir) => !dir.startsWith(".") && !["node_modules", ".git"].includes(dir))
|
||||
|
||||
for (const language of languages) {
|
||||
const languageDir = path.join(exercisesDir, language)
|
||||
|
||||
// Read exercise directories
|
||||
const exercises = fs.readdirSync(languageDir).filter((dir) => fs.statSync(path.join(languageDir, dir)).isDirectory())
|
||||
|
||||
for (const exercise of exercises) {
|
||||
const exerciseDir = path.join(languageDir, exercise)
|
||||
|
||||
// Read instructions
|
||||
let description = ""
|
||||
const instructionsPath = path.join(exerciseDir, "docs", "instructions.md")
|
||||
if (fs.existsSync(instructionsPath)) {
|
||||
description = fs.readFileSync(instructionsPath, "utf-8")
|
||||
}
|
||||
|
||||
// Determine test commands based on language
|
||||
let testCommands: string[] = []
|
||||
switch (language) {
|
||||
case "javascript":
|
||||
testCommands = ["npm install", "npm test"]
|
||||
break
|
||||
case "python":
|
||||
testCommands = ["python -m pytest -o markers=task *_test.py"]
|
||||
break
|
||||
case "go":
|
||||
testCommands = ["go test"]
|
||||
break
|
||||
case "java":
|
||||
testCommands = ["./gradlew test"]
|
||||
break
|
||||
case "rust":
|
||||
testCommands = ["cargo test"]
|
||||
break
|
||||
default:
|
||||
testCommands = []
|
||||
}
|
||||
|
||||
tasks.push({
|
||||
id: `exercism-${language}-${exercise}`,
|
||||
name: exercise,
|
||||
description,
|
||||
workspacePath: exerciseDir,
|
||||
setupCommands: [],
|
||||
verificationCommands: testCommands,
|
||||
metadata: {
|
||||
language,
|
||||
type: "exercism",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a specific task for execution
|
||||
* @param taskId The ID of the task to prepare
|
||||
*/
|
||||
async prepareTask(taskId: string): Promise<Task> {
|
||||
const tasks = await this.listTasks()
|
||||
const task = tasks.find((t) => t.id === taskId)
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Check if Git repository is already initialized
|
||||
const gitDirExists = fs.existsSync(path.join(task.workspacePath, ".git"))
|
||||
|
||||
try {
|
||||
// Initialize Git repository if needed
|
||||
if (!gitDirExists) {
|
||||
await execa("git", ["init"], { cwd: task.workspacePath })
|
||||
}
|
||||
|
||||
// Create a dummy file to ensure there's something to commit
|
||||
const dummyFilePath = path.join(task.workspacePath, ".eval-timestamp")
|
||||
fs.writeFileSync(dummyFilePath, new Date().toISOString())
|
||||
|
||||
// Add all files and commit
|
||||
await execa("git", ["add", "."], { cwd: task.workspacePath })
|
||||
|
||||
try {
|
||||
await execa("git", ["commit", "-m", "Initial commit"], { cwd: task.workspacePath })
|
||||
} catch (error: any) {
|
||||
// If commit fails because there are no changes, that's okay
|
||||
if (!error.stderr?.includes("nothing to commit")) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn(`Warning: Git operations failed: ${error.message}`)
|
||||
console.warn("Continuing without Git initialization")
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
// Run verification commands
|
||||
let success = true
|
||||
let output = ""
|
||||
|
||||
for (const command of task.verificationCommands) {
|
||||
try {
|
||||
const [cmd, ...args] = command.split(" ")
|
||||
const { stdout } = await execa(cmd, args, { cwd: task.workspacePath })
|
||||
output += stdout + "\n"
|
||||
} catch (error: any) {
|
||||
success = false
|
||||
if (error.stdout) output += error.stdout + "\n"
|
||||
if (error.stderr) output += error.stderr + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
// Parse test results
|
||||
const testsPassed = (output.match(/PASS/g) || []).length
|
||||
const testsFailed = (output.match(/FAIL/g) || []).length
|
||||
const testsTotal = testsPassed + testsFailed
|
||||
|
||||
return {
|
||||
success,
|
||||
metrics: {
|
||||
testsPassed,
|
||||
testsFailed,
|
||||
testsTotal,
|
||||
functionalCorrectness: testsTotal > 0 ? testsPassed / testsTotal : 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { BenchmarkAdapter } from "./types"
|
||||
import { ExercismAdapter } from "./exercism"
|
||||
import { SWEBenchAdapter } from "./swe-bench"
|
||||
import { SWELancerAdapter } from "./swelancer"
|
||||
import { MultiSWEAdapter } from "./multi-swe"
|
||||
|
||||
// Registry of all available adapters
|
||||
const adapters: Record<string, BenchmarkAdapter> = {
|
||||
// Exercism is the primary adapter with real implementation
|
||||
exercism: new ExercismAdapter(),
|
||||
|
||||
// Dummy adapters for testing
|
||||
"swe-bench": new SWEBenchAdapter(),
|
||||
swelancer: new SWELancerAdapter(),
|
||||
"multi-swe": new MultiSWEAdapter(),
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific adapter by name
|
||||
* @param name The name of the adapter to get
|
||||
* @returns The requested adapter
|
||||
* @throws Error if the adapter is not found
|
||||
*/
|
||||
export function getAdapter(name: string): BenchmarkAdapter {
|
||||
const adapter = adapters[name]
|
||||
if (!adapter) {
|
||||
throw new Error(`Adapter for benchmark '${name}' not found`)
|
||||
}
|
||||
return adapter
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available adapters
|
||||
* @returns Array of all registered adapters
|
||||
*/
|
||||
export function getAllAdapters(): BenchmarkAdapter[] {
|
||||
return Object.values(adapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new adapter
|
||||
* @param name The name to register the adapter under
|
||||
* @param adapter The adapter to register
|
||||
*/
|
||||
export function registerAdapter(name: string, adapter: BenchmarkAdapter): void {
|
||||
adapters[name] = adapter
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import execa from "execa"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
|
||||
/**
|
||||
* Dummy adapter for the Multi-SWE-Bench benchmark
|
||||
*/
|
||||
export class MultiSWEAdapter implements BenchmarkAdapter {
|
||||
name = "multi-swe"
|
||||
|
||||
/**
|
||||
* Set up the Multi-SWE-Bench benchmark repository (dummy implementation)
|
||||
*/
|
||||
async setup(): Promise<void> {
|
||||
console.log("Multi-SWE-Bench dummy setup completed")
|
||||
|
||||
// Create repositories directory if it doesn't exist
|
||||
const repoDir = path.join(EVALS_DIR, "repositories", "multi-swe")
|
||||
if (!fs.existsSync(repoDir)) {
|
||||
fs.mkdirSync(repoDir, { recursive: true })
|
||||
console.log(`Created dummy Multi-SWE-Bench directory at ${repoDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available tasks in the Multi-SWE-Bench benchmark (dummy implementation)
|
||||
*/
|
||||
async listTasks(): Promise<Task[]> {
|
||||
return [
|
||||
{
|
||||
id: "multi-swe-task-1",
|
||||
name: "Multi-Language API Integration",
|
||||
description:
|
||||
"Implement a system that integrates a Python backend with a TypeScript frontend and a Rust processing service.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
languages: ["python", "typescript", "rust"],
|
||||
complexity: "high",
|
||||
type: "multi-swe",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "multi-swe-task-2",
|
||||
name: "Cross-Platform Mobile App",
|
||||
description: "Create a cross-platform mobile app using React Native with native modules in Swift and Kotlin.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
languages: ["javascript", "swift", "kotlin"],
|
||||
complexity: "medium",
|
||||
type: "multi-swe",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "multi-swe-task-3",
|
||||
name: "Microservice Architecture",
|
||||
description: "Design and implement a microservice architecture with services written in Go, Node.js, and Java.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "multi-swe"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
languages: ["go", "javascript", "java"],
|
||||
complexity: "high",
|
||||
type: "multi-swe",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a specific task for execution (dummy implementation)
|
||||
* @param taskId The ID of the task to prepare
|
||||
*/
|
||||
async prepareTask(taskId: string): Promise<Task> {
|
||||
const tasks = await this.listTasks()
|
||||
const task = tasks.find((t) => t.id === taskId)
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Create a dummy workspace for the task
|
||||
const taskDir = path.join(task.workspacePath, taskId)
|
||||
if (!fs.existsSync(taskDir)) {
|
||||
fs.mkdirSync(taskDir, { recursive: true })
|
||||
|
||||
// Create a dummy file for the task
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "README.md"),
|
||||
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
|
||||
)
|
||||
|
||||
// Create additional dummy files based on task type
|
||||
if (task.id === "multi-swe-task-1") {
|
||||
// Python backend
|
||||
fs.mkdirSync(path.join(taskDir, "backend"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "backend", "app.py"),
|
||||
`# TODO: Implement Python backend\nfrom flask import Flask\n\napp = Flask(__name__)\n\n@app.route('/')\ndef hello():\n return "Hello, World!"\n`,
|
||||
)
|
||||
|
||||
// TypeScript frontend
|
||||
fs.mkdirSync(path.join(taskDir, "frontend"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "frontend", "app.ts"),
|
||||
`// TODO: Implement TypeScript frontend\nconsole.log('Frontend starting...');\n`,
|
||||
)
|
||||
|
||||
// Rust processing service
|
||||
fs.mkdirSync(path.join(taskDir, "processor"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "processor", "main.rs"),
|
||||
`// TODO: Implement Rust processing service\nfn main() {\n println!("Processor starting...");\n}\n`,
|
||||
)
|
||||
} else if (task.id === "multi-swe-task-2") {
|
||||
// React Native app
|
||||
fs.mkdirSync(path.join(taskDir, "app"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "app", "App.js"),
|
||||
`// TODO: Implement React Native app\nimport React from 'react';\nimport { View, Text } from 'react-native';\n\nexport default function App() {\n return (\n <View>\n <Text>Hello, World!</Text>\n </View>\n );\n}\n`,
|
||||
)
|
||||
|
||||
// Swift native module
|
||||
fs.mkdirSync(path.join(taskDir, "ios"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "ios", "NativeModule.swift"),
|
||||
`// TODO: Implement Swift native module\nimport Foundation\n\n@objc(NativeModule)\nclass NativeModule: NSObject {\n @objc\n func hello() -> String {\n return "Hello from Swift"\n }\n}\n`,
|
||||
)
|
||||
|
||||
// Kotlin native module
|
||||
fs.mkdirSync(path.join(taskDir, "android"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "android", "NativeModule.kt"),
|
||||
`// TODO: Implement Kotlin native module\npackage com.example.app\n\nclass NativeModule {\n fun hello(): String {\n return "Hello from Kotlin"\n }\n}\n`,
|
||||
)
|
||||
} else if (task.id === "multi-swe-task-3") {
|
||||
// Go service
|
||||
fs.mkdirSync(path.join(taskDir, "service-go"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "service-go", "main.go"),
|
||||
`// TODO: Implement Go service\npackage main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Go service starting...")\n}\n`,
|
||||
)
|
||||
|
||||
// Node.js service
|
||||
fs.mkdirSync(path.join(taskDir, "service-node"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "service-node", "server.js"),
|
||||
`// TODO: Implement Node.js service\nconsole.log('Node.js service starting...');\n`,
|
||||
)
|
||||
|
||||
// Java service
|
||||
fs.mkdirSync(path.join(taskDir, "service-java"), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "service-java", "Main.java"),
|
||||
`// TODO: Implement Java service\npublic class Main {\n public static void main(String[] args) {\n System.out.println("Java service starting...");\n }\n}\n`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the task's workspace path to the task-specific directory
|
||||
return {
|
||||
...task,
|
||||
workspacePath: taskDir,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution (dummy implementation)
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
// Always return success for dummy implementation
|
||||
return {
|
||||
success: true,
|
||||
metrics: {
|
||||
testsPassed: 1,
|
||||
testsFailed: 0,
|
||||
testsTotal: 1,
|
||||
functionalCorrectness: 1.0,
|
||||
crossLanguageIntegration: 0.9, // Dummy metric specific to Multi-SWE
|
||||
architectureQuality: 0.85, // Dummy metric specific to Multi-SWE
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import execa from "execa"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
|
||||
/**
|
||||
* Dummy adapter for the SWE-Bench benchmark
|
||||
*/
|
||||
export class SWEBenchAdapter implements BenchmarkAdapter {
|
||||
name = "swe-bench"
|
||||
|
||||
/**
|
||||
* Set up the SWE-Bench benchmark repository (dummy implementation)
|
||||
*/
|
||||
async setup(): Promise<void> {
|
||||
console.log("SWE-Bench dummy setup completed")
|
||||
|
||||
// Create repositories directory if it doesn't exist
|
||||
const repoDir = path.join(EVALS_DIR, "repositories", "swe-bench")
|
||||
if (!fs.existsSync(repoDir)) {
|
||||
fs.mkdirSync(repoDir, { recursive: true })
|
||||
console.log(`Created dummy SWE-Bench directory at ${repoDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available tasks in the SWE-Bench benchmark (dummy implementation)
|
||||
*/
|
||||
async listTasks(): Promise<Task[]> {
|
||||
return [
|
||||
{
|
||||
id: "swe-bench-task-1",
|
||||
name: "Fix React Component Bug",
|
||||
description: "Fix a bug in a React component where the state is not properly updated.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
repository: "facebook/react",
|
||||
issue: "#12345",
|
||||
type: "swe-bench",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swe-bench-task-2",
|
||||
name: "Optimize Database Query",
|
||||
description: "Optimize a slow database query in a Django application.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
repository: "django/django",
|
||||
issue: "#6789",
|
||||
type: "swe-bench",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swe-bench-task-3",
|
||||
name: "Fix Memory Leak",
|
||||
description: "Fix a memory leak in a Node.js application.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swe-bench"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
repository: "nodejs/node",
|
||||
issue: "#9876",
|
||||
type: "swe-bench",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a specific task for execution (dummy implementation)
|
||||
* @param taskId The ID of the task to prepare
|
||||
*/
|
||||
async prepareTask(taskId: string): Promise<Task> {
|
||||
const tasks = await this.listTasks()
|
||||
const task = tasks.find((t) => t.id === taskId)
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Create a dummy workspace for the task
|
||||
const taskDir = path.join(task.workspacePath, taskId)
|
||||
if (!fs.existsSync(taskDir)) {
|
||||
fs.mkdirSync(taskDir, { recursive: true })
|
||||
|
||||
// Create a dummy file for the task
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "README.md"),
|
||||
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
|
||||
)
|
||||
}
|
||||
|
||||
// Update the task's workspace path to the task-specific directory
|
||||
return {
|
||||
...task,
|
||||
workspacePath: taskDir,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution (dummy implementation)
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
// Always return success for dummy implementation
|
||||
return {
|
||||
success: true,
|
||||
metrics: {
|
||||
testsPassed: 1,
|
||||
testsFailed: 0,
|
||||
testsTotal: 1,
|
||||
functionalCorrectness: 1.0,
|
||||
performanceImprovement: 0.25, // Dummy metric specific to SWE-Bench
|
||||
codeQuality: 0.9, // Dummy metric specific to SWE-Bench
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import execa from "execa"
|
||||
import { BenchmarkAdapter, Task, VerificationResult } from "./types"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
|
||||
/**
|
||||
* Dummy adapter for the SWELancer benchmark
|
||||
*/
|
||||
export class SWELancerAdapter implements BenchmarkAdapter {
|
||||
name = "swelancer"
|
||||
|
||||
/**
|
||||
* Set up the SWELancer benchmark repository (dummy implementation)
|
||||
*/
|
||||
async setup(): Promise<void> {
|
||||
console.log("SWELancer dummy setup completed")
|
||||
|
||||
// Create repositories directory if it doesn't exist
|
||||
const repoDir = path.join(EVALS_DIR, "repositories", "swelancer")
|
||||
if (!fs.existsSync(repoDir)) {
|
||||
fs.mkdirSync(repoDir, { recursive: true })
|
||||
console.log(`Created dummy SWELancer directory at ${repoDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available tasks in the SWELancer benchmark (dummy implementation)
|
||||
*/
|
||||
async listTasks(): Promise<Task[]> {
|
||||
return [
|
||||
{
|
||||
id: "swelancer-task-1",
|
||||
name: "Create Landing Page",
|
||||
description: "Create a responsive landing page for a new product using HTML, CSS, and JavaScript.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
client: "TechStartup Inc.",
|
||||
difficulty: "medium",
|
||||
type: "swelancer",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swelancer-task-2",
|
||||
name: "Build REST API",
|
||||
description: "Create a RESTful API for a blog application using Node.js and Express.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
client: "BlogCo",
|
||||
difficulty: "hard",
|
||||
type: "swelancer",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "swelancer-task-3",
|
||||
name: "Fix CSS Layout Issues",
|
||||
description: "Fix layout issues in a responsive website across different screen sizes.",
|
||||
workspacePath: path.join(EVALS_DIR, "repositories", "swelancer"),
|
||||
setupCommands: [],
|
||||
verificationCommands: [],
|
||||
metadata: {
|
||||
client: "DesignAgency",
|
||||
difficulty: "easy",
|
||||
type: "swelancer",
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a specific task for execution (dummy implementation)
|
||||
* @param taskId The ID of the task to prepare
|
||||
*/
|
||||
async prepareTask(taskId: string): Promise<Task> {
|
||||
const tasks = await this.listTasks()
|
||||
const task = tasks.find((t) => t.id === taskId)
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Task ${taskId} not found`)
|
||||
}
|
||||
|
||||
// Create a dummy workspace for the task
|
||||
const taskDir = path.join(task.workspacePath, taskId)
|
||||
if (!fs.existsSync(taskDir)) {
|
||||
fs.mkdirSync(taskDir, { recursive: true })
|
||||
|
||||
// Create a dummy file for the task
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "README.md"),
|
||||
`# ${task.name}\n\n${task.description}\n\nThis is a dummy task for testing purposes.`,
|
||||
)
|
||||
|
||||
// Create additional dummy files based on task type
|
||||
if (task.id === "swelancer-task-1") {
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "index.html"),
|
||||
`<!DOCTYPE html>\n<html>\n<head>\n <title>Landing Page</title>\n</head>\n<body>\n <!-- TODO: Implement landing page -->\n</body>\n</html>`,
|
||||
)
|
||||
} else if (task.id === "swelancer-task-2") {
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "server.js"),
|
||||
`// TODO: Implement REST API\nconsole.log('Server starting...');`,
|
||||
)
|
||||
} else if (task.id === "swelancer-task-3") {
|
||||
fs.writeFileSync(
|
||||
path.join(taskDir, "styles.css"),
|
||||
`/* TODO: Fix layout issues */\nbody {\n margin: 0;\n padding: 0;\n}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the task's workspace path to the task-specific directory
|
||||
return {
|
||||
...task,
|
||||
workspacePath: taskDir,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the result of a task execution (dummy implementation)
|
||||
* @param task The task that was executed
|
||||
* @param result The result of the task execution
|
||||
*/
|
||||
async verifyResult(task: Task, result: any): Promise<VerificationResult> {
|
||||
// Always return success for dummy implementation
|
||||
return {
|
||||
success: true,
|
||||
metrics: {
|
||||
testsPassed: 1,
|
||||
testsFailed: 0,
|
||||
testsTotal: 1,
|
||||
functionalCorrectness: 1.0,
|
||||
clientSatisfaction: 0.95, // Dummy metric specific to SWELancer
|
||||
timeEfficiency: 0.85, // Dummy metric specific to SWELancer
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Represents a task to be executed
|
||||
*/
|
||||
export interface Task {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
workspacePath: string
|
||||
setupCommands: string[]
|
||||
verificationCommands: string[]
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of verifying a task execution
|
||||
*/
|
||||
export interface VerificationResult {
|
||||
success: boolean
|
||||
metrics: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for benchmark adapters
|
||||
*/
|
||||
export interface BenchmarkAdapter {
|
||||
name: string
|
||||
setup(): Promise<void>
|
||||
listTasks(): Promise<Task[]>
|
||||
prepareTask(taskId: string): Promise<Task>
|
||||
verifyResult(task: Task, result: any): Promise<VerificationResult>
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import chalk from "chalk"
|
||||
import ora from "ora"
|
||||
import { ResultsDatabase } from "../db"
|
||||
import { generateMarkdownReport } from "../utils/markdown"
|
||||
|
||||
interface ReportOptions {
|
||||
format?: "json" | "markdown"
|
||||
output?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the report command
|
||||
* @param options Command options
|
||||
*/
|
||||
export async function reportHandler(options: ReportOptions): Promise<void> {
|
||||
const format = options.format || "markdown"
|
||||
const db = new ResultsDatabase()
|
||||
|
||||
try {
|
||||
const spinner = ora("Generating report...").start()
|
||||
|
||||
// Get all runs
|
||||
const runs = db.getRuns()
|
||||
|
||||
console.log(chalk.blue(`Found ${runs.length} evaluation runs`))
|
||||
|
||||
if (runs.length === 0) {
|
||||
spinner.fail("No evaluation runs found")
|
||||
return
|
||||
}
|
||||
|
||||
// Generate summary report
|
||||
const summary = {
|
||||
runs: runs.length,
|
||||
models: [...new Set(runs.map((run) => run.model))],
|
||||
benchmarks: [...new Set(runs.map((run) => run.benchmark))],
|
||||
tasks: 0,
|
||||
successRate: 0,
|
||||
averageTokens: 0,
|
||||
averageCost: 0,
|
||||
averageDuration: 0,
|
||||
totalToolCalls: 0,
|
||||
totalToolFailures: 0,
|
||||
toolSuccessRate: 0,
|
||||
toolUsage: {} as Record<string, { calls: number; failures: number }>,
|
||||
}
|
||||
|
||||
let totalTasks = 0
|
||||
let successfulTasks = 0
|
||||
let totalTokens = 0
|
||||
let totalCost = 0
|
||||
let totalDuration = 0
|
||||
let totalToolCalls = 0
|
||||
let totalToolFailures = 0
|
||||
|
||||
for (const run of runs) {
|
||||
const tasks = db.getRunTasks(run.id)
|
||||
totalTasks += tasks.length
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.success) {
|
||||
successfulTasks++
|
||||
}
|
||||
|
||||
const metrics = db.getTaskMetrics(task.id)
|
||||
|
||||
const tokensIn = metrics.find((m) => m.name === "tokensIn")?.value || 0
|
||||
const tokensOut = metrics.find((m) => m.name === "tokensOut")?.value || 0
|
||||
totalTokens += tokensIn + tokensOut
|
||||
|
||||
totalCost += metrics.find((m) => m.name === "cost")?.value || 0
|
||||
totalDuration += metrics.find((m) => m.name === "duration")?.value || 0
|
||||
|
||||
// Collect tool call metrics
|
||||
totalToolCalls += task.total_tool_calls || 0
|
||||
totalToolFailures += task.total_tool_failures || 0
|
||||
|
||||
// Get detailed tool usage
|
||||
const toolCalls = db.getTaskToolCalls(task.id)
|
||||
|
||||
for (const toolCall of toolCalls) {
|
||||
if (!summary.toolUsage[toolCall.tool_name]) {
|
||||
summary.toolUsage[toolCall.tool_name] = {
|
||||
calls: 0,
|
||||
failures: 0,
|
||||
}
|
||||
}
|
||||
|
||||
summary.toolUsage[toolCall.tool_name].calls += toolCall.call_count
|
||||
summary.toolUsage[toolCall.tool_name].failures += toolCall.failure_count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate tool success rate
|
||||
summary.totalToolCalls = totalToolCalls
|
||||
summary.totalToolFailures = totalToolFailures
|
||||
summary.toolSuccessRate = totalToolCalls > 0 ? 1 - totalToolFailures / totalToolCalls : 1.0
|
||||
|
||||
summary.tasks = totalTasks
|
||||
summary.successRate = totalTasks > 0 ? successfulTasks / totalTasks : 0
|
||||
summary.averageTokens = totalTasks > 0 ? totalTokens / totalTasks : 0
|
||||
summary.averageCost = totalTasks > 0 ? totalCost / totalTasks : 0
|
||||
summary.averageDuration = totalTasks > 0 ? totalDuration / totalTasks : 0
|
||||
|
||||
// Generate benchmark-specific reports
|
||||
const benchmarkReports: Record<string, any> = {}
|
||||
|
||||
for (const benchmark of summary.benchmarks) {
|
||||
const benchmarkRuns = runs.filter((run) => run.benchmark === benchmark)
|
||||
const benchmarkSummary = {
|
||||
runs: benchmarkRuns.length,
|
||||
models: [...new Set(benchmarkRuns.map((run) => run.model))],
|
||||
tasks: 0,
|
||||
successRate: 0,
|
||||
averageTokens: 0,
|
||||
averageCost: 0,
|
||||
averageDuration: 0,
|
||||
}
|
||||
|
||||
let benchmarkTasks = 0
|
||||
let benchmarkSuccessfulTasks = 0
|
||||
let benchmarkTotalTokens = 0
|
||||
let benchmarkTotalCost = 0
|
||||
let benchmarkTotalDuration = 0
|
||||
|
||||
for (const run of benchmarkRuns) {
|
||||
const tasks = db.getRunTasks(run.id)
|
||||
benchmarkTasks += tasks.length
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.success) {
|
||||
benchmarkSuccessfulTasks++
|
||||
}
|
||||
|
||||
const metrics = db.getTaskMetrics(task.id)
|
||||
|
||||
const tokensIn = metrics.find((m) => m.name === "tokensIn")?.value || 0
|
||||
const tokensOut = metrics.find((m) => m.name === "tokensOut")?.value || 0
|
||||
benchmarkTotalTokens += tokensIn + tokensOut
|
||||
|
||||
benchmarkTotalCost += metrics.find((m) => m.name === "cost")?.value || 0
|
||||
benchmarkTotalDuration += metrics.find((m) => m.name === "duration")?.value || 0
|
||||
}
|
||||
}
|
||||
|
||||
benchmarkSummary.tasks = benchmarkTasks
|
||||
benchmarkSummary.successRate = benchmarkTasks > 0 ? benchmarkSuccessfulTasks / benchmarkTasks : 0
|
||||
benchmarkSummary.averageTokens = benchmarkTasks > 0 ? benchmarkTotalTokens / benchmarkTasks : 0
|
||||
benchmarkSummary.averageCost = benchmarkTasks > 0 ? benchmarkTotalCost / benchmarkTasks : 0
|
||||
benchmarkSummary.averageDuration = benchmarkTasks > 0 ? benchmarkTotalDuration / benchmarkTasks : 0
|
||||
|
||||
benchmarkReports[benchmark] = benchmarkSummary
|
||||
}
|
||||
|
||||
// Generate model-specific reports
|
||||
const modelReports: Record<string, any> = {}
|
||||
|
||||
for (const model of summary.models) {
|
||||
const modelRuns = runs.filter((run) => run.model === model)
|
||||
const modelSummary = {
|
||||
runs: modelRuns.length,
|
||||
benchmarks: [...new Set(modelRuns.map((run) => run.benchmark))],
|
||||
tasks: 0,
|
||||
successRate: 0,
|
||||
averageTokens: 0,
|
||||
averageCost: 0,
|
||||
averageDuration: 0,
|
||||
}
|
||||
|
||||
let modelTasks = 0
|
||||
let modelSuccessfulTasks = 0
|
||||
let modelTotalTokens = 0
|
||||
let modelTotalCost = 0
|
||||
let modelTotalDuration = 0
|
||||
|
||||
for (const run of modelRuns) {
|
||||
const tasks = db.getRunTasks(run.id)
|
||||
modelTasks += tasks.length
|
||||
|
||||
for (const task of tasks) {
|
||||
if (task.success) {
|
||||
modelSuccessfulTasks++
|
||||
}
|
||||
|
||||
const metrics = db.getTaskMetrics(task.id)
|
||||
|
||||
const tokensIn = metrics.find((m) => m.name === "tokensIn")?.value || 0
|
||||
const tokensOut = metrics.find((m) => m.name === "tokensOut")?.value || 0
|
||||
modelTotalTokens += tokensIn + tokensOut
|
||||
|
||||
modelTotalCost += metrics.find((m) => m.name === "cost")?.value || 0
|
||||
modelTotalDuration += metrics.find((m) => m.name === "duration")?.value || 0
|
||||
}
|
||||
}
|
||||
|
||||
modelSummary.tasks = modelTasks
|
||||
modelSummary.successRate = modelTasks > 0 ? modelSuccessfulTasks / modelTasks : 0
|
||||
modelSummary.averageTokens = modelTasks > 0 ? modelTotalTokens / modelTasks : 0
|
||||
modelSummary.averageCost = modelTasks > 0 ? modelTotalCost / modelTasks : 0
|
||||
modelSummary.averageDuration = modelTasks > 0 ? modelTotalDuration / modelTasks : 0
|
||||
|
||||
modelReports[model] = modelSummary
|
||||
}
|
||||
|
||||
// Save reports
|
||||
const reportDir = path.join(path.resolve(__dirname, "../../../"), "results", "reports")
|
||||
fs.mkdirSync(reportDir, { recursive: true })
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/:/g, "-")
|
||||
|
||||
if (format === "json") {
|
||||
// Save JSON reports
|
||||
fs.writeFileSync(path.join(reportDir, `summary-${timestamp}.json`), JSON.stringify(summary, null, 2))
|
||||
|
||||
fs.writeFileSync(path.join(reportDir, `benchmarks-${timestamp}.json`), JSON.stringify(benchmarkReports, null, 2))
|
||||
|
||||
fs.writeFileSync(path.join(reportDir, `models-${timestamp}.json`), JSON.stringify(modelReports, null, 2))
|
||||
|
||||
spinner.succeed(`JSON reports generated in ${reportDir}`)
|
||||
} else {
|
||||
// Generate markdown report
|
||||
const outputPath = options.output || path.join(reportDir, `report-${timestamp}.md`)
|
||||
|
||||
generateMarkdownReport(summary, benchmarkReports, modelReports, outputPath)
|
||||
|
||||
spinner.succeed(`Markdown report generated at ${outputPath}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Error generating report: ${error.message}`))
|
||||
console.error(error.stack)
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import * as path from "path"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import chalk from "chalk"
|
||||
import ora from "ora"
|
||||
import { getAdapter } from "../adapters"
|
||||
import { ResultsDatabase } from "../db"
|
||||
import { spawnVSCode, cleanupVSCode } from "../utils/vscode"
|
||||
import { sendTaskToServer } from "../utils/task"
|
||||
import { storeTaskResult } from "../utils/results"
|
||||
|
||||
interface RunOptions {
|
||||
benchmark?: string
|
||||
model: string
|
||||
count?: number
|
||||
apiKey?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the run command
|
||||
* @param options Command options
|
||||
*/
|
||||
export async function runHandler(options: RunOptions): Promise<void> {
|
||||
// Determine which benchmarks to run
|
||||
const benchmarks = options.benchmark ? [options.benchmark] : ["exercism"] // Default to exercism for now
|
||||
const model = options.model
|
||||
const count = options.count || Infinity
|
||||
|
||||
console.log(chalk.blue(`Running evaluations for model: ${model}`))
|
||||
console.log(chalk.blue(`Benchmarks: ${benchmarks.join(", ")}`))
|
||||
|
||||
// Create a run for each benchmark
|
||||
for (const benchmark of benchmarks) {
|
||||
const runId = uuidv4()
|
||||
const db = new ResultsDatabase()
|
||||
|
||||
console.log(chalk.green(`\nStarting run for benchmark: ${benchmark}`))
|
||||
|
||||
// Create run in database
|
||||
db.createRun(runId, model, benchmark)
|
||||
|
||||
// Get adapter for this benchmark
|
||||
try {
|
||||
const adapter = getAdapter(benchmark)
|
||||
|
||||
// List tasks
|
||||
const spinner = ora("Listing tasks...").start()
|
||||
const tasks = await adapter.listTasks()
|
||||
spinner.succeed(`Found ${tasks.length} tasks for ${benchmark}`)
|
||||
|
||||
// Limit number of tasks if specified
|
||||
const tasksToRun = tasks.slice(0, count)
|
||||
|
||||
console.log(chalk.blue(`Running ${tasksToRun.length} tasks...`))
|
||||
|
||||
// Run each task
|
||||
for (let i = 0; i < tasksToRun.length; i++) {
|
||||
const task = tasksToRun[i]
|
||||
|
||||
console.log(chalk.cyan(`\nTask ${i + 1}/${tasksToRun.length}: ${task.name}`))
|
||||
|
||||
// Prepare task
|
||||
const prepareSpinner = ora("Preparing task...").start()
|
||||
const preparedTask = await adapter.prepareTask(task.id)
|
||||
prepareSpinner.succeed("Task prepared")
|
||||
|
||||
// Spawn VSCode
|
||||
console.log("Spawning VSCode...")
|
||||
await spawnVSCode(preparedTask.workspacePath)
|
||||
|
||||
// Send task to server
|
||||
const sendSpinner = ora("Sending task to server...").start()
|
||||
try {
|
||||
const result = await sendTaskToServer(preparedTask.description, options.apiKey)
|
||||
sendSpinner.succeed("Task completed")
|
||||
|
||||
// Verify result
|
||||
const verifySpinner = ora("Verifying result...").start()
|
||||
const verification = await adapter.verifyResult(preparedTask, result)
|
||||
|
||||
if (verification.success) {
|
||||
verifySpinner.succeed(
|
||||
`Verification successful: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal} tests passed`,
|
||||
)
|
||||
} else {
|
||||
verifySpinner.fail(
|
||||
`Verification failed: ${verification.metrics.testsPassed}/${verification.metrics.testsTotal} tests passed`,
|
||||
)
|
||||
}
|
||||
|
||||
// Store result
|
||||
const storeSpinner = ora("Storing result...").start()
|
||||
await storeTaskResult(runId, preparedTask, result, verification)
|
||||
storeSpinner.succeed("Result stored")
|
||||
|
||||
console.log(chalk.green(`Task completed. Success: ${verification.success}`))
|
||||
|
||||
// Clean up VS Code and temporary files
|
||||
const cleanupSpinner = ora("Cleaning up...").start()
|
||||
try {
|
||||
await cleanupVSCode(preparedTask.workspacePath)
|
||||
cleanupSpinner.succeed("Cleanup completed")
|
||||
} catch (cleanupError: any) {
|
||||
cleanupSpinner.fail(`Cleanup failed: ${cleanupError.message}`)
|
||||
console.error(chalk.yellow(cleanupError.stack))
|
||||
}
|
||||
} catch (error: any) {
|
||||
sendSpinner.fail(`Task failed: ${error.message}`)
|
||||
console.error(chalk.red(error.stack))
|
||||
|
||||
// Clean up VS Code and temporary files even if the task failed
|
||||
const cleanupSpinner = ora("Cleaning up...").start()
|
||||
try {
|
||||
await cleanupVSCode(preparedTask.workspacePath)
|
||||
cleanupSpinner.succeed("Cleanup completed")
|
||||
} catch (cleanupError: any) {
|
||||
cleanupSpinner.fail(`Cleanup failed: ${cleanupError.message}`)
|
||||
console.error(chalk.yellow(cleanupError.stack))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark run as complete
|
||||
db.completeRun(runId)
|
||||
|
||||
console.log(chalk.green(`\nRun complete for benchmark: ${benchmark}`))
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red(`Error running benchmark ${benchmark}: ${error.message}`))
|
||||
console.error(error.stack)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.green("\nAll evaluations complete"))
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import execa from "execa"
|
||||
import chalk from "chalk"
|
||||
import ora from "ora"
|
||||
import { getAllAdapters } from "../adapters/index"
|
||||
import { BenchmarkAdapter } from "../adapters/types"
|
||||
|
||||
interface SetupOptions {
|
||||
benchmarks: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for the setup command
|
||||
* @param options Command options
|
||||
*/
|
||||
export async function setupHandler(options: SetupOptions): Promise<void> {
|
||||
const benchmarks = options.benchmarks.split(",")
|
||||
|
||||
console.log(chalk.blue(`Setting up benchmarks: ${benchmarks.join(", ")}`))
|
||||
|
||||
// Create directories
|
||||
const evalsDir = path.resolve(__dirname, "../../../")
|
||||
const reposDir = path.join(evalsDir, "repositories")
|
||||
const resultsDir = path.join(evalsDir, "results")
|
||||
|
||||
const spinner = ora("Creating directory structure").start()
|
||||
|
||||
try {
|
||||
fs.mkdirSync(reposDir, { recursive: true })
|
||||
fs.mkdirSync(resultsDir, { recursive: true })
|
||||
fs.mkdirSync(path.join(resultsDir, "runs"), { recursive: true })
|
||||
fs.mkdirSync(path.join(resultsDir, "reports"), { recursive: true })
|
||||
spinner.succeed("Directory structure created")
|
||||
} catch (error) {
|
||||
spinner.fail(`Failed to create directory structure: ${(error as Error).message}`)
|
||||
throw error
|
||||
}
|
||||
|
||||
// Set up each benchmark
|
||||
try {
|
||||
const adapters = getAllAdapters().filter((adapter: BenchmarkAdapter) => benchmarks.includes(adapter.name))
|
||||
|
||||
if (adapters.length === 0) {
|
||||
console.warn(chalk.yellow("No valid benchmarks specified. Available benchmarks:"))
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
getAllAdapters()
|
||||
.map((a: BenchmarkAdapter) => a.name)
|
||||
.join(", "),
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
for (const adapter of adapters) {
|
||||
const setupSpinner = ora(`Setting up ${adapter.name}...`).start()
|
||||
try {
|
||||
await adapter.setup()
|
||||
setupSpinner.succeed(`${adapter.name} setup complete`)
|
||||
} catch (error) {
|
||||
setupSpinner.fail(`Failed to set up ${adapter.name}: ${(error as Error).message}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.green("Setup complete"))
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Setup failed: ${(error as Error).message}`))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import Database from "better-sqlite3"
|
||||
import { SCHEMA } from "./schema"
|
||||
|
||||
const EVALS_DIR = path.resolve(__dirname, "../../../")
|
||||
|
||||
/**
|
||||
* Database class for storing evaluation results
|
||||
*/
|
||||
export class ResultsDatabase {
|
||||
db: Database.Database
|
||||
|
||||
constructor() {
|
||||
// Ensure results directory exists
|
||||
const resultsDir = path.join(EVALS_DIR, "results")
|
||||
fs.mkdirSync(resultsDir, { recursive: true })
|
||||
|
||||
// Create database file
|
||||
const dbPath = path.join(resultsDir, "evals.db")
|
||||
this.db = new Database(dbPath)
|
||||
|
||||
// Initialize schema
|
||||
this.initSchema()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database schema
|
||||
*/
|
||||
private initSchema(): void {
|
||||
this.db.exec(SCHEMA)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new evaluation run
|
||||
* @param id Run ID
|
||||
* @param model Model name
|
||||
* @param benchmark Benchmark name
|
||||
*/
|
||||
createRun(id: string, model: string, benchmark: string): void {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO runs (id, timestamp, model, benchmark)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
stmt.run(id, Date.now(), model, benchmark)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a run as completed
|
||||
* @param id Run ID
|
||||
*/
|
||||
completeRun(id: string): void {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE runs SET completed = 1 WHERE id = ?
|
||||
`)
|
||||
|
||||
stmt.run(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new task
|
||||
* @param id Task ID
|
||||
* @param runId Run ID
|
||||
* @param taskId Original task ID
|
||||
*/
|
||||
createTask(id: string, runId: string, taskId: string): void {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO tasks (id, run_id, task_id, timestamp)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
stmt.run(id, runId, taskId, Date.now())
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a task as completed
|
||||
* @param id Task ID
|
||||
* @param success Whether the task was successful
|
||||
* @param toolCalls Total tool calls
|
||||
* @param toolFailures Total tool failures
|
||||
*/
|
||||
completeTask(id: string, success: boolean, toolCalls: number = 0, toolFailures: number = 0): void {
|
||||
const stmt = this.db.prepare(`
|
||||
UPDATE tasks
|
||||
SET success = ?, total_tool_calls = ?, total_tool_failures = ?
|
||||
WHERE id = ?
|
||||
`)
|
||||
|
||||
stmt.run(success ? 1 : 0, toolCalls, toolFailures, id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a metric to a task
|
||||
* @param taskId Task ID
|
||||
* @param name Metric name
|
||||
* @param value Metric value
|
||||
*/
|
||||
addMetric(taskId: string, name: string, value: number): void {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO metrics (task_id, name, value)
|
||||
VALUES (?, ?, ?)
|
||||
`)
|
||||
|
||||
stmt.run(taskId, name, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tool call record
|
||||
* @param taskId Task ID
|
||||
* @param toolName Tool name
|
||||
* @param callCount Number of calls
|
||||
* @param failureCount Number of failures
|
||||
*/
|
||||
addToolCall(taskId: string, toolName: string, callCount: number, failureCount: number): void {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO tool_calls (task_id, tool_name, call_count, failure_count)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
stmt.run(taskId, toolName, callCount, failureCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a file record
|
||||
* @param taskId Task ID
|
||||
* @param filePath File path
|
||||
* @param status File status (created, modified, deleted)
|
||||
*/
|
||||
addFile(taskId: string, filePath: string, status: "created" | "modified" | "deleted"): void {
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO files (task_id, path, status)
|
||||
VALUES (?, ?, ?)
|
||||
`)
|
||||
|
||||
stmt.run(taskId, filePath, status)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all runs
|
||||
* @returns Array of runs
|
||||
*/
|
||||
getRuns(): any[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM runs ORDER BY timestamp DESC
|
||||
`)
|
||||
|
||||
return stmt.all()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tasks for a run
|
||||
* @param runId Run ID
|
||||
* @returns Array of tasks
|
||||
*/
|
||||
getRunTasks(runId: string): any[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM tasks WHERE run_id = ? ORDER BY timestamp ASC
|
||||
`)
|
||||
|
||||
return stmt.all(runId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all metrics for a task
|
||||
* @param taskId Task ID
|
||||
* @returns Array of metrics
|
||||
*/
|
||||
getTaskMetrics(taskId: string): any[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT name, value FROM metrics WHERE task_id = ?
|
||||
`)
|
||||
|
||||
return stmt.all(taskId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tool calls for a task
|
||||
* @param taskId Task ID
|
||||
* @returns Array of tool calls
|
||||
*/
|
||||
getTaskToolCalls(taskId: string): any[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT tool_name, call_count, failure_count
|
||||
FROM tool_calls
|
||||
WHERE task_id = ?
|
||||
`)
|
||||
|
||||
return stmt.all(taskId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files for a task
|
||||
* @param taskId Task ID
|
||||
* @returns Array of files
|
||||
*/
|
||||
getTaskFiles(taskId: string): any[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT path, status FROM files WHERE task_id = ?
|
||||
`)
|
||||
|
||||
return stmt.all(taskId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
close(): void {
|
||||
this.db.close()
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* SQL schema for the evaluation database
|
||||
*/
|
||||
export const SCHEMA = `
|
||||
CREATE TABLE IF NOT EXISTS runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
timestamp INTEGER NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
benchmark TEXT NOT NULL,
|
||||
completed INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
run_id TEXT NOT NULL,
|
||||
task_id TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
success INTEGER NOT NULL DEFAULT 0,
|
||||
total_tool_calls INTEGER DEFAULT 0,
|
||||
total_tool_failures INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (run_id) REFERENCES runs(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS metrics (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
value REAL NOT NULL,
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tool_calls (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
tool_name TEXT NOT NULL,
|
||||
call_count INTEGER NOT NULL,
|
||||
failure_count INTEGER NOT NULL,
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
FOREIGN KEY (task_id) REFERENCES tasks(id)
|
||||
);
|
||||
`
|
||||
@@ -1,70 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { Command } from "commander"
|
||||
import chalk from "chalk"
|
||||
import { setupHandler } from "./commands/setup"
|
||||
import { runHandler } from "./commands/run"
|
||||
import { reportHandler } from "./commands/report"
|
||||
|
||||
// Create the CLI program
|
||||
const program = new Command()
|
||||
|
||||
// Set up CLI metadata
|
||||
program.name("cline-eval").description("CLI tool for orchestrating Cline evaluations across multiple benchmarks").version("0.1.0")
|
||||
|
||||
// Setup command
|
||||
program
|
||||
.command("setup")
|
||||
.description("Clone and set up benchmark repositories")
|
||||
.option(
|
||||
"-b, --benchmarks <benchmarks>",
|
||||
"Comma-separated list of benchmarks to set up",
|
||||
"exercism,swe-bench,swelancer,multi-swe",
|
||||
)
|
||||
.action(async (options) => {
|
||||
try {
|
||||
await setupHandler(options)
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error during setup: ${error instanceof Error ? error.message : String(error)}`))
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
// Run command
|
||||
program
|
||||
.command("run")
|
||||
.description("Run evaluations")
|
||||
.option("-b, --benchmark <benchmark>", "Specific benchmark to run")
|
||||
.option("-m, --model <model>", "Model to evaluate", "claude-3-opus-20240229")
|
||||
.option("-c, --count <count>", "Number of tasks to run", parseInt)
|
||||
.option("-k, --api-key <apiKey>", "Cline API key to use for evaluations")
|
||||
.action(async (options) => {
|
||||
try {
|
||||
await runHandler(options)
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error during run: ${error instanceof Error ? error.message : String(error)}`))
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
// Report command
|
||||
program
|
||||
.command("report")
|
||||
.description("Generate reports")
|
||||
.option("-f, --format <format>", "Report format (json, markdown)", "markdown")
|
||||
.option("-o, --output <path>", "Output path for the report")
|
||||
.action(async (options) => {
|
||||
try {
|
||||
await reportHandler(options)
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error generating report: ${error instanceof Error ? error.message : String(error)}`))
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
// Parse command line arguments
|
||||
program.parse(process.argv)
|
||||
|
||||
// If no arguments provided, show help
|
||||
if (process.argv.length === 2) {
|
||||
program.help()
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
import execa from "execa"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
|
||||
/**
|
||||
* List of VSCode extensions to install for evaluation environments
|
||||
* These extensions provide language support and other useful features
|
||||
*/
|
||||
export const REQUIRED_EXTENSIONS = [
|
||||
"golang.go", // Go language support
|
||||
"dbaeumer.vscode-eslint", // ESLint support
|
||||
"redhat.java", // Java support
|
||||
"ms-python.python", // Python support
|
||||
"rust-lang.rust-analyzer", // Rust support
|
||||
"ms-vscode.cpptools", // C/C++ support
|
||||
]
|
||||
|
||||
/**
|
||||
* Install required VSCode extensions in the specified extensions directory
|
||||
* @param extensionsDir The directory where extensions should be installed
|
||||
* @returns Promise that resolves when all extensions are installed
|
||||
*/
|
||||
export async function installRequiredExtensions(extensionsDir: string): Promise<void> {
|
||||
console.log("Installing required VSCode extensions...")
|
||||
|
||||
// Create the extensions directory if it doesn't exist
|
||||
if (!fs.existsSync(extensionsDir)) {
|
||||
fs.mkdirSync(extensionsDir, { recursive: true })
|
||||
}
|
||||
|
||||
// Install each extension
|
||||
for (const extension of REQUIRED_EXTENSIONS) {
|
||||
try {
|
||||
console.log(`Installing extension: ${extension}...`)
|
||||
await execa("code", ["--extensions-dir", extensionsDir, "--install-extension", extension, "--force"])
|
||||
console.log(`✅ Extension ${extension} installed successfully`)
|
||||
} catch (error: any) {
|
||||
console.warn(`⚠️ Failed to install extension ${extension}: ${error.message}`)
|
||||
// Continue with other extensions even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ All required extensions installed")
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a VSCode extension is installed in the specified directory
|
||||
* @param extensionsDir The directory to check for installed extensions
|
||||
* @param extensionId The ID of the extension to check
|
||||
* @returns True if the extension is installed, false otherwise
|
||||
*/
|
||||
export function isExtensionInstalled(extensionsDir: string, extensionId: string): boolean {
|
||||
// Extensions are installed in directories named publisher.name-version
|
||||
// We need to check if any directory starts with the extensionId
|
||||
const extensionPrefix = extensionId.toLowerCase() + "-"
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(extensionsDir)
|
||||
return files.some((file) => {
|
||||
const lowerCaseFile = file.toLowerCase()
|
||||
return lowerCaseFile === extensionId.toLowerCase() || lowerCaseFile.startsWith(extensionPrefix)
|
||||
})
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path to the VSCode settings file in the specified user data directory
|
||||
* @param userDataDir The VSCode user data directory
|
||||
* @returns The path to the settings.json file
|
||||
*/
|
||||
export function getSettingsPath(userDataDir: string): string {
|
||||
const settingsDir = path.join(userDataDir, "User")
|
||||
fs.mkdirSync(settingsDir, { recursive: true })
|
||||
return path.join(settingsDir, "settings.json")
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure extension settings in the VSCode user data directory
|
||||
* @param userDataDir The VSCode user data directory
|
||||
*/
|
||||
export function configureExtensionSettings(userDataDir: string): void {
|
||||
const settingsPath = getSettingsPath(userDataDir)
|
||||
|
||||
// Read existing settings if they exist
|
||||
let settings = {}
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
try {
|
||||
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"))
|
||||
} catch (error) {
|
||||
console.warn(`Error reading settings file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update extension-specific settings
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
// Go extension settings
|
||||
"go.toolsManagement.autoUpdate": false,
|
||||
"go.survey.prompt": false,
|
||||
|
||||
// ESLint settings
|
||||
"eslint.enable": true,
|
||||
"eslint.run": "onSave",
|
||||
|
||||
// Java settings
|
||||
"java.configuration.checkProjectSettingsExclusions": false,
|
||||
"java.configure.checkForOutdatedExtensions": false,
|
||||
"java.help.firstView": false,
|
||||
|
||||
// Python settings
|
||||
"python.experiments.enabled": false,
|
||||
"python.showStartPage": false,
|
||||
|
||||
// Rust settings
|
||||
"rust-analyzer.checkOnSave.command": "check",
|
||||
|
||||
// C/C++ settings
|
||||
"C_Cpp.intelliSenseEngine": "default",
|
||||
|
||||
// General extension settings
|
||||
"extensions.autoUpdate": false,
|
||||
"extensions.ignoreRecommendations": true,
|
||||
}
|
||||
|
||||
// Write updated settings
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(updatedSettings, null, 2))
|
||||
console.log("✅ Extension settings configured")
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
|
||||
/**
|
||||
* Generate a markdown report from evaluation results
|
||||
* @param summary Overall summary
|
||||
* @param benchmarkReports Benchmark-specific reports
|
||||
* @param modelReports Model-specific reports
|
||||
* @param outputPath Output file path
|
||||
*/
|
||||
export function generateMarkdownReport(
|
||||
summary: any,
|
||||
benchmarkReports: Record<string, any>,
|
||||
modelReports: Record<string, any>,
|
||||
outputPath: string,
|
||||
): void {
|
||||
let markdown = `# Cline Evaluation Report\n\n`
|
||||
|
||||
// Generate summary section
|
||||
markdown += `## Summary\n\n`
|
||||
markdown += `- **Total Runs:** ${summary.runs}\n`
|
||||
markdown += `- **Models:** ${summary.models.join(", ")}\n`
|
||||
markdown += `- **Benchmarks:** ${summary.benchmarks.join(", ")}\n`
|
||||
markdown += `- **Total Tasks:** ${summary.tasks}\n`
|
||||
markdown += `- **Success Rate:** ${(summary.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Average Tokens:** ${Math.round(summary.averageTokens)}\n`
|
||||
markdown += `- **Average Cost:** $${summary.averageCost.toFixed(4)}\n`
|
||||
markdown += `- **Average Duration:** ${(summary.averageDuration / 1000).toFixed(2)}s\n`
|
||||
markdown += `- **Total Tool Calls:** ${summary.totalToolCalls}\n`
|
||||
markdown += `- **Tool Success Rate:** ${(summary.toolSuccessRate * 100).toFixed(2)}%\n\n`
|
||||
|
||||
// Generate tool usage section
|
||||
markdown += `## Tool Usage\n\n`
|
||||
markdown += `| Tool | Calls | Failures | Success Rate |\n`
|
||||
markdown += `| ---- | ----- | -------- | ------------ |\n`
|
||||
|
||||
for (const [toolName, metrics] of Object.entries(summary.toolUsage)) {
|
||||
const calls = (metrics as any).calls
|
||||
const failures = (metrics as any).failures
|
||||
const successRate = calls > 0 ? (1 - failures / calls) * 100 : 100
|
||||
|
||||
markdown += `| ${toolName} | ${calls} | ${failures} | ${successRate.toFixed(2)}% |\n`
|
||||
}
|
||||
|
||||
// Generate benchmark results section
|
||||
markdown += `\n## Benchmark Results\n\n`
|
||||
|
||||
for (const [benchmark, report] of Object.entries(benchmarkReports)) {
|
||||
markdown += `### ${benchmark}\n\n`
|
||||
markdown += `- **Runs:** ${report.runs}\n`
|
||||
markdown += `- **Models:** ${report.models.join(", ")}\n`
|
||||
markdown += `- **Tasks:** ${report.tasks}\n`
|
||||
markdown += `- **Success Rate:** ${(report.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Average Tokens:** ${Math.round(report.averageTokens)}\n`
|
||||
markdown += `- **Average Cost:** $${report.averageCost.toFixed(4)}\n`
|
||||
markdown += `- **Average Duration:** ${(report.averageDuration / 1000).toFixed(2)}s\n\n`
|
||||
}
|
||||
|
||||
// Generate model results section
|
||||
markdown += `## Model Results\n\n`
|
||||
|
||||
for (const [model, report] of Object.entries(modelReports)) {
|
||||
markdown += `### ${model}\n\n`
|
||||
markdown += `- **Runs:** ${report.runs}\n`
|
||||
markdown += `- **Benchmarks:** ${report.benchmarks.join(", ")}\n`
|
||||
markdown += `- **Tasks:** ${report.tasks}\n`
|
||||
markdown += `- **Success Rate:** ${(report.successRate * 100).toFixed(2)}%\n`
|
||||
markdown += `- **Average Tokens:** ${Math.round(report.averageTokens)}\n`
|
||||
markdown += `- **Average Cost:** $${report.averageCost.toFixed(4)}\n`
|
||||
markdown += `- **Average Duration:** ${(report.averageDuration / 1000).toFixed(2)}s\n\n`
|
||||
}
|
||||
|
||||
// Add charts using Mermaid
|
||||
markdown += `## Charts\n\n`
|
||||
|
||||
// Success rate by benchmark chart
|
||||
markdown += `### Success Rate by Benchmark\n\n`
|
||||
markdown += "```mermaid\n"
|
||||
markdown += "graph TD\n"
|
||||
markdown += " title[Success Rate by Benchmark]\n"
|
||||
markdown += " style title fill:none,stroke:none\n\n"
|
||||
|
||||
for (const [benchmark, report] of Object.entries(benchmarkReports)) {
|
||||
const successRate = (report.successRate * 100).toFixed(2)
|
||||
markdown += ` ${benchmark}[${benchmark}: ${successRate}%]\n`
|
||||
}
|
||||
|
||||
markdown += "```\n\n"
|
||||
|
||||
// Success rate by model chart
|
||||
markdown += `### Success Rate by Model\n\n`
|
||||
markdown += "```mermaid\n"
|
||||
markdown += "graph TD\n"
|
||||
markdown += " title[Success Rate by Model]\n"
|
||||
markdown += " style title fill:none,stroke:none\n\n"
|
||||
|
||||
for (const [model, report] of Object.entries(modelReports)) {
|
||||
const successRate = (report.successRate * 100).toFixed(2)
|
||||
markdown += ` ${model.replace(/[-\.]/g, "_")}[${model}: ${successRate}%]\n`
|
||||
}
|
||||
|
||||
markdown += "```\n\n"
|
||||
|
||||
// Add timestamp
|
||||
markdown += `\n\n---\n\nReport generated on ${new Date().toISOString()}\n`
|
||||
|
||||
// Write markdown to file
|
||||
fs.writeFileSync(outputPath, markdown)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { ResultsDatabase } from "../db"
|
||||
import { Task } from "../adapters/types"
|
||||
|
||||
/**
|
||||
* Store task result in the database
|
||||
* @param runId The run ID
|
||||
* @param task The task that was executed
|
||||
* @param result The result from the test server
|
||||
* @param verification The verification result
|
||||
*/
|
||||
export async function storeTaskResult(runId: string, task: Task, result: any, verification: any): Promise<void> {
|
||||
const db = new ResultsDatabase()
|
||||
const taskId = uuidv4()
|
||||
|
||||
try {
|
||||
// Extract metrics from the result
|
||||
const { metrics } = result
|
||||
const totalToolCalls = metrics?.totalToolCalls || 0
|
||||
const totalToolFailures = metrics?.totalToolFailures || 0
|
||||
|
||||
// Create task with tool metrics
|
||||
db.createTask(taskId, runId, task.id)
|
||||
db.completeTask(taskId, verification.success, totalToolCalls, totalToolFailures)
|
||||
|
||||
// Store metrics
|
||||
if (metrics) {
|
||||
// Store token metrics
|
||||
if (metrics.tokensIn) db.addMetric(taskId, "tokensIn", metrics.tokensIn)
|
||||
if (metrics.tokensOut) db.addMetric(taskId, "tokensOut", metrics.tokensOut)
|
||||
if (metrics.cost) db.addMetric(taskId, "cost", metrics.cost)
|
||||
if (metrics.duration) db.addMetric(taskId, "duration", metrics.duration)
|
||||
|
||||
// Store tool call metrics
|
||||
if (metrics.toolCalls) {
|
||||
for (const [toolName, callCount] of Object.entries(metrics.toolCalls)) {
|
||||
const failureCount = metrics.toolFailures?.[toolName] || 0
|
||||
db.addToolCall(taskId, toolName, callCount as number, failureCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store verification metrics
|
||||
if (verification.metrics) {
|
||||
for (const [key, value] of Object.entries(verification.metrics)) {
|
||||
if (typeof value === "number") {
|
||||
db.addMetric(taskId, key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store file changes
|
||||
if (result.files) {
|
||||
// Store created files
|
||||
if (result.files.created) {
|
||||
for (const file of result.files.created) {
|
||||
db.addFile(taskId, file, "created")
|
||||
}
|
||||
}
|
||||
|
||||
// Store modified files
|
||||
if (result.files.modified) {
|
||||
for (const file of result.files.modified) {
|
||||
db.addFile(taskId, file, "modified")
|
||||
}
|
||||
}
|
||||
|
||||
// Store deleted files
|
||||
if (result.files.deleted) {
|
||||
for (const file of result.files.deleted) {
|
||||
db.addFile(taskId, file, "deleted")
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Close the database connection
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import fetch from "node-fetch"
|
||||
import chalk from "chalk"
|
||||
|
||||
/**
|
||||
* Send a task to the Cline test server
|
||||
* @param task The task description to send
|
||||
* @param apiKey Optional Cline API key to use for the task
|
||||
* @returns The result of the task execution
|
||||
*/
|
||||
export async function sendTaskToServer(task: string, apiKey?: string): Promise<any> {
|
||||
const SERVER_URL = "http://localhost:9876/task"
|
||||
|
||||
try {
|
||||
console.log(chalk.blue(`Sending task to server: ${task.substring(0, 100)}${task.length > 100 ? "..." : ""}`))
|
||||
|
||||
const response = await fetch(SERVER_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
task,
|
||||
apiKey,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`Server responded with status ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(`Task execution failed: ${result.error || "Unknown error"}`)
|
||||
}
|
||||
|
||||
if (result.timeout) {
|
||||
throw new Error("Task execution timed out")
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error: any) {
|
||||
if (error.code === "ECONNREFUSED") {
|
||||
throw new Error(
|
||||
"Could not connect to the test server. Make sure VSCode is running with the Cline extension and the test server is active.",
|
||||
)
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,570 +0,0 @@
|
||||
import execa from "execa"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs"
|
||||
import fetch from "node-fetch"
|
||||
import * as os from "os"
|
||||
import * as child_process from "child_process"
|
||||
import { installRequiredExtensions, configureExtensionSettings } from "./extensions"
|
||||
|
||||
// Store temporary directories for cleanup
|
||||
interface VSCodeResources {
|
||||
tempUserDataDir: string
|
||||
tempExtensionsDir: string
|
||||
vscodePid?: number
|
||||
}
|
||||
|
||||
// Global map to track resources for each workspace
|
||||
const workspaceResources = new Map<string, VSCodeResources>()
|
||||
|
||||
/**
|
||||
* Spawn a VSCode instance with the Cline extension
|
||||
* @param workspacePath The workspace path to open
|
||||
* @param vsixPath Optional path to a VSIX file to install
|
||||
* @returns The resources created for this VS Code instance
|
||||
*/
|
||||
export async function spawnVSCode(workspacePath: string, vsixPath?: string): Promise<VSCodeResources> {
|
||||
// Ensure the workspace path exists
|
||||
if (!fs.existsSync(workspacePath)) {
|
||||
throw new Error(`Workspace path does not exist: ${workspacePath}`)
|
||||
}
|
||||
|
||||
// If no VSIX path is provided, build one with IS_TEST=true
|
||||
if (!vsixPath) {
|
||||
try {
|
||||
// Build the VSIX with IS_TEST=true
|
||||
console.log("Building test VSIX...")
|
||||
const clineRoot = path.resolve(process.cwd(), "..", "..")
|
||||
await execa("npx", ["vsce", "package"], {
|
||||
cwd: clineRoot,
|
||||
env: {
|
||||
IS_TEST: "true",
|
||||
},
|
||||
stdio: "inherit",
|
||||
})
|
||||
|
||||
// Find the generated VSIX file
|
||||
const files = fs.readdirSync(clineRoot)
|
||||
const vsixFile = files.find((file) => file.endsWith(".vsix"))
|
||||
if (vsixFile) {
|
||||
vsixPath = path.join(clineRoot, vsixFile)
|
||||
console.log(`Using built VSIX: ${vsixPath}`)
|
||||
} else {
|
||||
console.warn("Could not find generated VSIX file")
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to build test VSIX:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a temporary user data directory for this VS Code instance
|
||||
const tempUserDataDir = path.join(os.tmpdir(), `vscode-cline-eval-${Date.now()}`)
|
||||
fs.mkdirSync(tempUserDataDir, { recursive: true })
|
||||
console.log(`Created temporary user data directory: ${tempUserDataDir}`)
|
||||
|
||||
// Create a temporary extensions directory to ensure no other extensions are loaded
|
||||
const tempExtensionsDir = path.join(os.tmpdir(), `vscode-cline-eval-ext-${Date.now()}`)
|
||||
fs.mkdirSync(tempExtensionsDir, { recursive: true })
|
||||
console.log(`Created temporary extensions directory: ${tempExtensionsDir}`)
|
||||
|
||||
// Create settings.json in the temporary user data directory to disable workspace trust
|
||||
// and configure Cline to auto-open on startup
|
||||
const settingsDir = path.join(tempUserDataDir, "User")
|
||||
fs.mkdirSync(settingsDir, { recursive: true })
|
||||
const settingsPath = path.join(settingsDir, "settings.json")
|
||||
const settings = {
|
||||
// Disable workspace trust
|
||||
"security.workspace.trust.enabled": false,
|
||||
"security.workspace.trust.startupPrompt": "never",
|
||||
"security.workspace.trust.banner": "never",
|
||||
"security.workspace.trust.emptyWindow": true,
|
||||
|
||||
// Configure startup behavior
|
||||
"workbench.startupEditor": "none",
|
||||
|
||||
// Auto-open Cline on startup
|
||||
"cline.autoOpenOnStartup": true,
|
||||
|
||||
// Show the activity bar and sidebar
|
||||
"workbench.activityBar.visible": true,
|
||||
"workbench.sideBar.visible": true,
|
||||
"workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.visible": true,
|
||||
"workbench.view.alwaysShowHeaderActions": true,
|
||||
"workbench.editor.openSideBySideDirection": "right",
|
||||
|
||||
// Disable GitLens from opening automatically
|
||||
"gitlens.views.repositories.autoReveal": false,
|
||||
"gitlens.views.fileHistory.autoReveal": false,
|
||||
"gitlens.views.lineHistory.autoReveal": false,
|
||||
"gitlens.views.compare.autoReveal": false,
|
||||
"gitlens.views.search.autoReveal": false,
|
||||
"gitlens.showWelcomeOnInstall": false,
|
||||
"gitlens.showWhatsNewAfterUpgrades": false,
|
||||
|
||||
// Disable other extensions that might compete for startup focus
|
||||
"extensions.autoUpdate": false,
|
||||
}
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
||||
console.log(`Created settings.json to disable workspace trust and auto-open Cline`)
|
||||
|
||||
// Create keybindings.json to automatically open Cline on startup
|
||||
const keybindingsPath = path.join(settingsDir, "keybindings.json")
|
||||
const keybindings = [
|
||||
{
|
||||
key: "alt+c",
|
||||
command: "workbench.view.extension.saoudrizwan.claude-dev-ActivityBar",
|
||||
when: "viewContainer.workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.enabled",
|
||||
},
|
||||
{
|
||||
key: "alt+shift+c",
|
||||
command: "cline.openInNewTab",
|
||||
when: "viewContainer.workbench.view.extension.saoudrizwan.claude-dev-ActivityBar.enabled",
|
||||
},
|
||||
]
|
||||
fs.writeFileSync(keybindingsPath, JSON.stringify(keybindings, null, 2))
|
||||
console.log(`Created keybindings.json to help with Cline activation`)
|
||||
|
||||
// Build the command arguments with custom user data directory
|
||||
const args = [
|
||||
// Use a custom user data directory to isolate this instance
|
||||
"--user-data-dir",
|
||||
tempUserDataDir,
|
||||
// Use a custom extensions directory to ensure only our extension is loaded
|
||||
"--extensions-dir",
|
||||
tempExtensionsDir,
|
||||
// Disable workspace trust
|
||||
"--disable-workspace-trust",
|
||||
"-n",
|
||||
workspacePath,
|
||||
// Force the extension to be activated on startup
|
||||
"--start-up-extension",
|
||||
"saoudrizwan.claude-dev",
|
||||
// Run a command on startup to open Cline
|
||||
"--command",
|
||||
"workbench.view.extension.saoudrizwan.claude-dev-ActivityBar",
|
||||
// Additional flags to help with extension activation
|
||||
"--disable-gpu=false",
|
||||
"--max-memory=4096",
|
||||
]
|
||||
|
||||
// Create a startup script to run commands after VS Code launches
|
||||
const startupScriptPath = path.join(settingsDir, "startup.js")
|
||||
const startupScript = `
|
||||
// This script will be executed when VS Code starts
|
||||
setTimeout(() => {
|
||||
// Try to open Cline in the sidebar
|
||||
require('vscode').commands.executeCommand('workbench.view.extension.saoudrizwan.claude-dev-ActivityBar');
|
||||
|
||||
// Also try to open Cline in a tab as a fallback
|
||||
setTimeout(() => {
|
||||
require('vscode').commands.executeCommand('cline.openInNewTab');
|
||||
}, 5000);
|
||||
}, 5000);
|
||||
`
|
||||
fs.writeFileSync(startupScriptPath, startupScript)
|
||||
console.log(`Created startup script to activate Cline`)
|
||||
|
||||
// If a VSIX is provided, install it
|
||||
if (vsixPath) {
|
||||
if (!fs.existsSync(vsixPath)) {
|
||||
throw new Error(`VSIX file does not exist: ${vsixPath}`)
|
||||
}
|
||||
args.unshift("--install-extension", vsixPath)
|
||||
}
|
||||
|
||||
// Install required extensions
|
||||
console.log("Installing required VSCode extensions...")
|
||||
await installRequiredExtensions(tempExtensionsDir)
|
||||
|
||||
// Configure extension settings
|
||||
console.log("Configuring extension settings...")
|
||||
configureExtensionSettings(tempUserDataDir)
|
||||
|
||||
// Execute the command
|
||||
try {
|
||||
// We don't need to install extensions globally anymore since we're using a custom user data directory
|
||||
// The VSIX will be installed in the isolated environment if provided in the args
|
||||
|
||||
// Launch VS Code
|
||||
console.log("Launching VS Code...")
|
||||
await execa("code", args, {
|
||||
stdio: "inherit",
|
||||
})
|
||||
|
||||
// Wait longer for VSCode to initialize and extension to load
|
||||
console.log("Waiting for VS Code to initialize...")
|
||||
await new Promise((resolve) => setTimeout(resolve, 30000))
|
||||
|
||||
// Create a JavaScript file that will be loaded as a VS Code extension
|
||||
const extensionDir = path.join(tempExtensionsDir, "cline-activator")
|
||||
fs.mkdirSync(extensionDir, { recursive: true })
|
||||
|
||||
// Create package.json for the extension
|
||||
const packageJsonPath = path.join(extensionDir, "package.json")
|
||||
const packageJson = {
|
||||
name: "cline-activator",
|
||||
displayName: "Cline Activator",
|
||||
description: "Activates Cline and starts the test server",
|
||||
version: "0.0.1",
|
||||
engines: {
|
||||
vscode: "^1.60.0",
|
||||
},
|
||||
main: "./extension.js",
|
||||
activationEvents: ["*"],
|
||||
contributes: {
|
||||
commands: [
|
||||
{
|
||||
command: "cline-activator.activate",
|
||||
title: "Activate Cline",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2))
|
||||
|
||||
// Create extension.js
|
||||
const extensionJsPath = path.join(extensionDir, "extension.js")
|
||||
const extensionJs = `
|
||||
const vscode = require('vscode');
|
||||
|
||||
/**
|
||||
* @param {vscode.ExtensionContext} context
|
||||
*/
|
||||
function activate(context) {
|
||||
console.log('Cline Activator is now active!');
|
||||
|
||||
// Register the command to activate Cline
|
||||
let disposable = vscode.commands.registerCommand('cline-activator.activate', async function () {
|
||||
try {
|
||||
// Make sure the Cline extension is activated
|
||||
const extension = vscode.extensions.getExtension('saoudrizwan.claude-dev');
|
||||
if (!extension) {
|
||||
console.error('Cline extension not found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!extension.isActive) {
|
||||
console.log('Activating Cline extension...');
|
||||
await extension.activate();
|
||||
}
|
||||
|
||||
// Show the Cline sidebar
|
||||
console.log('Opening Cline sidebar...');
|
||||
await vscode.commands.executeCommand('workbench.view.extension.saoudrizwan.claude-dev-ActivityBar');
|
||||
|
||||
// Wait a moment for the sidebar to initialize
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Also open Cline in a tab as a fallback
|
||||
console.log('Opening Cline in a tab...');
|
||||
await vscode.commands.executeCommand('cline.openInNewTab');
|
||||
|
||||
// Wait a moment for the tab to initialize
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Create the test server if it doesn't exist
|
||||
console.log('Creating test server...');
|
||||
|
||||
// Get the visible webview instance
|
||||
const clineRootPath = '${path.resolve(process.cwd(), "..", "..")}';
|
||||
const visibleWebview = require(path.join(clineRootPath, 'src', 'core', 'webview')).WebviewProvider.getVisibleInstance();
|
||||
if (visibleWebview) {
|
||||
require(path.join(clineRootPath, 'src', 'services', 'test', 'TestServer')).createTestServer(visibleWebview);
|
||||
console.log('Test server created successfully');
|
||||
} else {
|
||||
console.error('No visible webview instance found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error activating Cline:', error);
|
||||
}
|
||||
});
|
||||
|
||||
context.subscriptions.push(disposable);
|
||||
|
||||
// Automatically run the command after a delay
|
||||
setTimeout(() => {
|
||||
vscode.commands.executeCommand('cline-activator.activate');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function deactivate() {}
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate
|
||||
}
|
||||
`
|
||||
fs.writeFileSync(extensionJsPath, extensionJs)
|
||||
console.log(`Created Cline Activator extension`)
|
||||
|
||||
// Try multiple approaches to activate the extension
|
||||
let serverStarted = false
|
||||
|
||||
// Create an activation script to run in VS Code
|
||||
const activationScriptPath = path.join(settingsDir, "activate-cline.js")
|
||||
const activationScript = `
|
||||
// This script will be executed to activate Cline and start the test server
|
||||
const vscode = require('vscode');
|
||||
|
||||
// Execute the cline-activator.activate command
|
||||
vscode.commands.executeCommand('cline-activator.activate');
|
||||
`
|
||||
fs.writeFileSync(activationScriptPath, activationScript)
|
||||
console.log(`Created activation script to run in VS Code`)
|
||||
|
||||
// Execute the activation script
|
||||
try {
|
||||
console.log("Executing activation script to start Cline and test server...")
|
||||
await execa(
|
||||
"code",
|
||||
[
|
||||
"--user-data-dir",
|
||||
tempUserDataDir,
|
||||
"--extensions-dir",
|
||||
tempExtensionsDir,
|
||||
"--folder-uri",
|
||||
`file://${workspacePath}`,
|
||||
"--execute",
|
||||
activationScriptPath,
|
||||
],
|
||||
{
|
||||
stdio: "inherit",
|
||||
},
|
||||
)
|
||||
|
||||
// Wait for the test server to start
|
||||
console.log("Waiting for test server to start...")
|
||||
for (let i = 0; i < 30; i++) {
|
||||
try {
|
||||
// Try to connect to the test server
|
||||
const response = await fetch("http://localhost:9876/task", {
|
||||
method: "OPTIONS",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
|
||||
if (response.status === 204) {
|
||||
console.log("Test server is running!")
|
||||
serverStarted = true
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
// Server not started yet, wait and try again
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to execute activation script:", error)
|
||||
}
|
||||
|
||||
if (!serverStarted) {
|
||||
console.warn("Test server did not start after multiple attempts")
|
||||
console.log("You may need to manually open the Cline extension in VS Code")
|
||||
}
|
||||
|
||||
// Store the resources for this workspace
|
||||
const resources: VSCodeResources = {
|
||||
tempUserDataDir,
|
||||
tempExtensionsDir,
|
||||
}
|
||||
|
||||
// Store in the global map
|
||||
workspaceResources.set(workspacePath, resources)
|
||||
|
||||
// Return the resources
|
||||
return resources
|
||||
} catch (error: any) {
|
||||
throw new Error(`Failed to spawn VSCode: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up VS Code resources and shut down the test server
|
||||
* @param workspacePath The workspace path to clean up resources for
|
||||
*/
|
||||
export async function cleanupVSCode(workspacePath: string): Promise<void> {
|
||||
console.log(`Cleaning up VS Code resources for workspace: ${workspacePath}`)
|
||||
|
||||
// Get the resources for this workspace
|
||||
const resources = workspaceResources.get(workspacePath)
|
||||
if (!resources) {
|
||||
console.log(`No resources found for workspace: ${workspacePath}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to shut down the test server
|
||||
try {
|
||||
console.log("Shutting down test server...")
|
||||
await fetch("http://localhost:9876/shutdown", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).catch(() => {
|
||||
// Ignore errors, the server might already be down
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(`Error shutting down test server: ${error}`)
|
||||
}
|
||||
|
||||
// Try to gracefully close VS Code instead of killing it
|
||||
try {
|
||||
console.log("Attempting to gracefully close VS Code...")
|
||||
|
||||
// Create a settings file that will disable the crash reporter and the exit confirmation dialog
|
||||
const settingsDir = path.join(resources.tempUserDataDir, "User")
|
||||
const settingsPath = path.join(settingsDir, "settings.json")
|
||||
|
||||
// Read existing settings if they exist
|
||||
let settings = {}
|
||||
if (fs.existsSync(settingsPath)) {
|
||||
try {
|
||||
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"))
|
||||
} catch (error) {
|
||||
console.warn(`Error reading settings file: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Update settings to disable crash reporter and exit confirmation
|
||||
settings = {
|
||||
...settings,
|
||||
"window.confirmBeforeClose": "never",
|
||||
"telemetry.enableCrashReporter": false,
|
||||
"window.restoreWindows": "none",
|
||||
"window.newWindowDimensions": "default",
|
||||
}
|
||||
|
||||
// Write updated settings
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2))
|
||||
|
||||
// On macOS, use AppleScript to quit VS Code gracefully
|
||||
if (process.platform === "darwin") {
|
||||
try {
|
||||
// First try AppleScript to quit VS Code gracefully
|
||||
await execa("osascript", ["-e", 'tell application "Visual Studio Code" to quit'])
|
||||
|
||||
// Wait a moment for VS Code to close
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
} catch (appleScriptError) {
|
||||
console.warn(`Error using AppleScript to quit VS Code: ${appleScriptError}`)
|
||||
}
|
||||
} else if (process.platform === "win32") {
|
||||
// On Windows, try to use taskkill without /F first
|
||||
try {
|
||||
await execa("taskkill", ["/IM", "code.exe"])
|
||||
|
||||
// Wait a moment for VS Code to close
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
} catch (taskkillError) {
|
||||
console.warn(`Error using taskkill to quit VS Code: ${taskkillError}`)
|
||||
}
|
||||
} else {
|
||||
// On Linux, try to use SIGTERM first
|
||||
try {
|
||||
// Find VS Code processes
|
||||
const { stdout } = await execa("ps", ["aux"])
|
||||
const lines = stdout.split("\n")
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes(resources.tempUserDataDir)) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
const pid = parseInt(parts[1])
|
||||
|
||||
if (pid && !isNaN(pid)) {
|
||||
console.log(`Sending SIGTERM to VS Code process with PID: ${pid}`)
|
||||
try {
|
||||
// Use SIGTERM instead of SIGKILL for a graceful shutdown
|
||||
process.kill(pid, "SIGTERM")
|
||||
} catch (killError) {
|
||||
console.warn(`Failed to terminate process ${pid}: ${killError}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait a moment for VS Code to close
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
} catch (psError) {
|
||||
console.warn(`Error listing processes: ${psError}`)
|
||||
}
|
||||
}
|
||||
|
||||
// If graceful methods failed, fall back to forceful termination as a last resort
|
||||
// Check if VS Code is still running with the temp user data dir
|
||||
let vsCodeStillRunning = false
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
try {
|
||||
const { stdout } = await execa("ps", ["aux"])
|
||||
vsCodeStillRunning = stdout.split("\n").some((line) => line.includes(resources.tempUserDataDir))
|
||||
} catch (error) {
|
||||
console.warn(`Error checking if VS Code is still running: ${error}`)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { stdout } = await execa("tasklist", ["/FI", `IMAGENAME eq code.exe`])
|
||||
vsCodeStillRunning = stdout.includes("code.exe")
|
||||
} catch (error) {
|
||||
console.warn(`Error checking if VS Code is still running: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// If VS Code is still running, use forceful termination as a last resort
|
||||
if (vsCodeStillRunning) {
|
||||
console.log("Graceful shutdown failed, falling back to forceful termination...")
|
||||
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
await execa("taskkill", ["/IM", "code.exe", "/F"])
|
||||
} catch (error) {
|
||||
console.warn(`Error forcefully terminating VS Code: ${error}`)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { stdout } = await execa("ps", ["aux"])
|
||||
const lines = stdout.split("\n")
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes(resources.tempUserDataDir)) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
const pid = parseInt(parts[1])
|
||||
|
||||
if (pid && !isNaN(pid)) {
|
||||
console.log(`Forcefully killing VS Code process with PID: ${pid}`)
|
||||
try {
|
||||
process.kill(pid, "SIGKILL")
|
||||
} catch (killError) {
|
||||
console.warn(`Failed to kill process ${pid}: ${killError}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error forcefully terminating VS Code: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Error closing VS Code: ${error}`)
|
||||
}
|
||||
|
||||
// Clean up temporary directories
|
||||
try {
|
||||
console.log(`Removing temporary user data directory: ${resources.tempUserDataDir}`)
|
||||
fs.rmSync(resources.tempUserDataDir, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
console.warn(`Error removing temporary user data directory: ${error}`)
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Removing temporary extensions directory: ${resources.tempExtensionsDir}`)
|
||||
fs.rmSync(resources.tempExtensionsDir, { recursive: true, force: true })
|
||||
} catch (error) {
|
||||
console.warn(`Error removing temporary extensions directory: ${error}`)
|
||||
}
|
||||
|
||||
// Remove from the global map
|
||||
workspaceResources.delete(workspacePath)
|
||||
|
||||
console.log("Cleanup completed")
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020"],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
العربية | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">الإسبانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">الألمانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">اليابانية</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">الصينية المبسطة</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-tw/README.md" target="_blank">الصينية التقليدية</a> | <a href="https://github.com/cline/cline/blob/main/locales/pt-BR/README.md" target="_blank">البرتغالية</a>
|
||||
</sub></div>
|
||||
|
||||
# Cline
|
||||
# Cline – \#1 على OpenRouter
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Cline
|
||||
# Cline – \#1 auf OpenRouter
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Cline
|
||||
# Cline – #1 en OpenRouter
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Cline
|
||||
# Clineへの貢献
|
||||
|
||||
Clineへの貢献に興味をお持ちいただきありがとうございます。
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Cline
|
||||
# Cline – OpenRouterでのナンバーワン
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Cline
|
||||
# Cline에 기여하기
|
||||
|
||||
Cline에 기여하는 것에 관심을 가져주셔서 감사합니다! 버그 수정, 기능 추가, 문서 개선 등 모든 기여는 Cline을 더욱 스마트하게 만드는 데 기여합니다. 활기차고 환영하는 커뮤니티를 유지하기 위해 모든 구성원은 [행동 강령](CODE_OF_CONDUCT.md)을 준수해야 합니다.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Cline
|
||||
# Cline - 최고의 OpenRouter
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Cline
|
||||
# Cline – #1 no OpenRouter
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Cline
|
||||
# Cline – OpenRouter 排名第一
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
@@ -20,7 +20,7 @@
|
||||
<a href="https://github.com/cline/cline/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><strong>功能请求</strong></a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://docs.cline.bot/getting-started/for-new-coders" target="_blank"><strong>新手上路</strong></a>
|
||||
<a href="https://cline.bot/join-us" target="_blank"><strong>我们正在招聘!</strong></a>
|
||||
</td>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -33,12 +33,12 @@
|
||||
1. 输入你的任务并添加图像,将模型转换为功能应用程序或通过截图修复错误。
|
||||
2. Cline 首先分析你的文件结构和源代码 AST,运行正则表达式搜索,并阅读相关文件以了解现有项目。通过仔细管理添加到上下文中的信息,Cline 即使在大型复杂项目中也能提供有价值的帮助,而不会使上下文窗口过载。
|
||||
3. 一旦 Cline 获得所需信息,他可以:
|
||||
- 创建和编辑文件 + 监控 linter/编译器错误,从而主动修复诸如缺少导入和语法错误等问题。
|
||||
- 直接在你的终端中执行命令并监控其输出,从而在编辑文件后对开发服务器问题做出反应。
|
||||
- 对于 Web 开发任务,Cline 可以在无头浏览器中启动网站,点击、输入、滚动并捕获截图和控制台日志,从而修复运行时错误和视觉错误。
|
||||
- 创建和编辑文件 + 监控 linter/编译器错误,从而主动修复诸如缺少导入和语法错误等问题。
|
||||
- 直接在你的终端中执行命令并监控其输出,从而在编辑文件后对开发服务器问题做出反应。
|
||||
- 对于 Web 开发任务,Cline 可以在无头浏览器中启动网站,点击、输入、滚动并捕获截图和控制台日志,从而修复运行时错误和视觉错误。
|
||||
4. 当任务完成时,Cline 将通过终端命令如 `open -a "Google Chrome" index.html` 向你展示结果,你可以通过点击按钮运行该命令。
|
||||
|
||||
> [!TIP]
|
||||
> [!提示]
|
||||
> 使用 `CMD/CTRL + Shift + P` 快捷键打开命令面板并输入 "Cline: Open In New Tab" 将扩展作为标签在编辑器中打开。这让你可以与文件资源管理器并排使用 Cline,更清楚地看到他如何改变你的工作空间。
|
||||
|
||||
---
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a href="https://github.com/cline/cline/blob/main/README.md" target="_blank">English</a> | <a href="https://github.com/cline/cline/blob/main/locales/es/README.md" target="_blank">Español</a> | <a href="https://github.com/cline/cline/blob/main/locales/de/README.md" target="_blank">Deutsch</a> | <a href="https://github.com/cline/cline/blob/main/locales/ja/README.md" target="_blank">日本語</a> | <a href="https://github.com/cline/cline/blob/main/locales/zh-cn/README.md" target="_blank">简体中文</a> | 繁體中文 | <a href="https://github.com/cline/cline/blob/main/locales/ko/README.md" target="_blank">한국어</a>
|
||||
</sub></div>
|
||||
|
||||
# Cline
|
||||
# Cline – OpenRouter 第一名的 AI 工具
|
||||
|
||||
<p align="center">
|
||||
<img src="https://media.githubusercontent.com/media/cline/cline/main/assets/docs/demo.gif" width="100%" />
|
||||
|
||||
Generated
+9
-388
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "claude-dev",
|
||||
"version": "3.12.0",
|
||||
"version": "3.8.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-dev",
|
||||
"version": "3.12.0",
|
||||
"version": "3.8.4",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/bedrock-sdk": "^0.12.4",
|
||||
@@ -23,19 +23,16 @@
|
||||
"@opentelemetry/sdk-node": "^0.39.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"axios": "^1.8.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"chrome-launcher": "^1.1.2",
|
||||
"clone-deep": "^4.0.1",
|
||||
"default-shell": "^2.2.0",
|
||||
"diff": "^5.2.0",
|
||||
"execa": "^9.5.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"firebase": "^11.2.0",
|
||||
"fzf": "^0.5.2",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"ignore": "^7.0.3",
|
||||
@@ -69,7 +66,6 @@
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "20.x",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/proxyquire": "^1.3.31",
|
||||
"@types/should": "^11.2.0",
|
||||
"@types/sinon": "^17.0.4",
|
||||
"@types/turndown": "^5.0.5",
|
||||
@@ -84,10 +80,8 @@
|
||||
"husky": "^9.1.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"proxyquire": "^2.1.3",
|
||||
"should": "^13.2.3",
|
||||
"sinon": "^19.0.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4292,30 +4286,6 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
|
||||
@@ -6857,81 +6827,6 @@
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sentry-internal/browser-utils": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.12.0.tgz",
|
||||
"integrity": "sha512-GXuDEG2Ix8DmVtTkjsItWdusk2CvJ6EPWKYVqFKifxt+IAT3ZbhGZd99Rg3wdRmt9xhCNuS4QrDzDTPMPgfdCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "9.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.12.0.tgz",
|
||||
"integrity": "sha512-3+UxoT97QIXNSUQS4ATL1FFws0RkUb6PeaQN8CPndI6mFlqTW5tuVVLNg9Eo1seNg7R/dfk6WHCWrYN1NbFFKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "9.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.12.0.tgz",
|
||||
"integrity": "sha512-njEQosFeO/UX+gG+DMRANkPUuz6OIJLb+A1GVylhq9adUgFQydQ9Ay3v7/x1gMhdfHVP6Jeb27qkti0BWYbzBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "9.12.0",
|
||||
"@sentry/core": "9.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.12.0.tgz",
|
||||
"integrity": "sha512-p8LuKZgWT/CoQBbDOXkSGjWWnc8WsnAayWgna8M/ZFWNITCNEM2rCuqZOyWOElIlrni+M7qoEA3jS7MZe8Ejxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/replay": "9.12.0",
|
||||
"@sentry/core": "9.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.12.0.tgz",
|
||||
"integrity": "sha512-4xQYoZqi+VVhNvlhWiwRd57+SMr3Og4sLjuayAA+zIp1Wx/bDcIld697cugLwml/BR+mVJI2eokkgh1CBl6zag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "9.12.0",
|
||||
"@sentry-internal/feedback": "9.12.0",
|
||||
"@sentry-internal/replay": "9.12.0",
|
||||
"@sentry-internal/replay-canvas": "9.12.0",
|
||||
"@sentry/core": "9.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "9.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.12.0.tgz",
|
||||
"integrity": "sha512-jOqQK/90uzHmsBvkPTj/DAEFvA5poX4ZRyC7LE1zjg4F5jdOp3+M4W3qCy0CkSTu88Zu5VWBoppCU2Bs34XEqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/merge-streams": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
||||
@@ -8674,34 +8569,6 @@
|
||||
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/chai": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.0.1.tgz",
|
||||
@@ -8781,13 +8648,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/proxyquire": {
|
||||
"version": "1.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/proxyquire/-/proxyquire-1.3.31.tgz",
|
||||
"integrity": "sha512-uALowNG2TSM1HNPMMOR0AJwv4aPYPhqB0xlEhkeRTMuto5hjoSPZkvgu1nbPUkz3gEPAHv4sy4DmKsurZiEfRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/should": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/should/-/should-11.2.0.tgz",
|
||||
@@ -9252,19 +9112,6 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
|
||||
@@ -9368,13 +9215,6 @@
|
||||
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@@ -10043,24 +9883,6 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/chrome-launcher": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.1.2.tgz",
|
||||
"integrity": "sha512-YclTJey34KUm5jB1aEJCq807bSievi7Nb/TU4Gu504fUYi3jw3KCIaH6L7nFWQhdEgH3V+wCh+kKD1P5cXnfxw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
"lighthouse-logger": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"print-chrome-path": "bin/print-chrome-path.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.5.tgz",
|
||||
@@ -10351,13 +10173,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -11048,9 +10863,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -11066,6 +10881,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -11774,30 +11590,6 @@
|
||||
"node": "^10.12.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-keys": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz",
|
||||
"integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-object": "~1.0.1",
|
||||
"merge-descriptors": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-keys/node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
@@ -12091,12 +11883,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/fzf": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fzf/-/fzf-0.5.2.tgz",
|
||||
"integrity": "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/gauge": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-5.0.2.tgz",
|
||||
@@ -12991,21 +12777,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -13087,16 +12858,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-object": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
|
||||
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-path-inside": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
|
||||
@@ -13276,18 +13037,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
@@ -13559,31 +13308,6 @@
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lighthouse-logger": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz",
|
||||
"integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^2.6.9",
|
||||
"marky": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lighthouse-logger/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lighthouse-logger/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/load-json-file": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
|
||||
@@ -13722,13 +13446,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mammoth": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.8.0.tgz",
|
||||
@@ -13762,12 +13479,6 @@
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/marky": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz",
|
||||
"integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -14140,13 +13851,6 @@
|
||||
"integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/module-not-found-error": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz",
|
||||
"integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/monaco-vscode-textmate-theme-converter": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/monaco-vscode-textmate-theme-converter/-/monaco-vscode-textmate-theme-converter-0.1.7.tgz",
|
||||
@@ -15412,18 +15116,6 @@
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/proxyquire": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz",
|
||||
"integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-keys": "^1.0.2",
|
||||
"module-not-found-error": "^1.0.1",
|
||||
"resolve": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
@@ -15916,9 +15608,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"version": "7.6.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
|
||||
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -16916,60 +16608,6 @@
|
||||
"typescript": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node/node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
@@ -17294,13 +16932,6 @@
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
||||
@@ -17855,16 +17486,6 @@
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
+17
-24
@@ -2,7 +2,7 @@
|
||||
"name": "claude-dev",
|
||||
"displayName": "Cline",
|
||||
"description": "Autonomous coding agent right in your IDE, capable of creating/editing files, running commands, using the browser, and more with your permission every step of the way.",
|
||||
"version": "3.12.3",
|
||||
"version": "3.8.5",
|
||||
"icon": "assets/icons/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.84.0"
|
||||
@@ -118,16 +118,6 @@
|
||||
"category": "Cline"
|
||||
}
|
||||
],
|
||||
"keybindings": [
|
||||
{
|
||||
"command": "cline.addToChat",
|
||||
"key": "cmd+'",
|
||||
"mac": "cmd+'",
|
||||
"win": "ctrl+'",
|
||||
"linux": "ctrl+'",
|
||||
"when": "editorHasSelection"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"view/title": [
|
||||
{
|
||||
@@ -224,6 +214,21 @@
|
||||
},
|
||||
"description": "Settings for VSCode Language Model API"
|
||||
},
|
||||
"cline.mcp.mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"full",
|
||||
"server-use-only",
|
||||
"off"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Enable all MCP functionality (server use and build instructions)",
|
||||
"Enable MCP server use only (excludes instructions about building MCP servers)",
|
||||
"Disable all MCP functionality"
|
||||
],
|
||||
"default": "full",
|
||||
"description": "Controls MCP inclusion in prompts, reduces token usage if you only need access to certain functionality."
|
||||
},
|
||||
"cline.enableCheckpoints": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -287,12 +292,8 @@
|
||||
"compile": "npm run check-types && npm run lint && node esbuild.js",
|
||||
"watch": "npm-run-all -p watch:*",
|
||||
"watch:esbuild": "node esbuild.js --watch",
|
||||
"watch:esbuild:test": "IS_TEST=true node esbuild.js --watch",
|
||||
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
|
||||
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
|
||||
"package:test": "IS_TEST=true npm run build:webview:test && npm run check-types && npm run lint && IS_TEST=true node esbuild.js --production",
|
||||
"build:webview:test": "cd webview-ui && IS_TEST=true npm run build",
|
||||
"watch:test": "IS_TEST=true npm-run-all -p watch:tsc watch:esbuild:test",
|
||||
"compile-tests": "tsc -p ./tsconfig.test.json --outDir out",
|
||||
"watch-tests": "tsc -p . -w --outDir out",
|
||||
"pretest": "npm run compile-tests && npm run compile && npm run lint",
|
||||
@@ -300,10 +301,8 @@
|
||||
"lint": "eslint src --ext ts && eslint webview-ui/src --ext ts",
|
||||
"format": "prettier . --check",
|
||||
"format:fix": "prettier . --write",
|
||||
"test": "npm-run-all test:unit test:integration",
|
||||
"test": "vscode-test",
|
||||
"test:ci": "node scripts/test-ci.js",
|
||||
"test:integration": "vscode-test",
|
||||
"test:unit": "TS_NODE_PROJECT='./tsconfig.unit-test.json' mocha",
|
||||
"test:coverage": "vscode-test --coverage",
|
||||
"install:all": "npm install && cd webview-ui && npm install",
|
||||
"dev:webview": "cd webview-ui && npm run dev",
|
||||
@@ -324,7 +323,6 @@
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "20.x",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/proxyquire": "^1.3.31",
|
||||
"@types/should": "^11.2.0",
|
||||
"@types/sinon": "^17.0.4",
|
||||
"@types/turndown": "^5.0.5",
|
||||
@@ -339,10 +337,8 @@
|
||||
"husky": "^9.1.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"proxyquire": "^2.1.3",
|
||||
"should": "^13.2.3",
|
||||
"sinon": "^19.0.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -360,19 +356,16 @@
|
||||
"@opentelemetry/sdk-node": "^0.39.1",
|
||||
"@opentelemetry/sdk-trace-node": "^1.30.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.30.0",
|
||||
"@sentry/browser": "^9.12.0",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"axios": "^1.8.2",
|
||||
"cheerio": "^1.0.0",
|
||||
"chokidar": "^4.0.1",
|
||||
"chrome-launcher": "^1.1.2",
|
||||
"clone-deep": "^4.0.1",
|
||||
"default-shell": "^2.2.0",
|
||||
"diff": "^5.2.0",
|
||||
"execa": "^9.5.2",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"firebase": "^11.2.0",
|
||||
"fzf": "^0.5.2",
|
||||
"get-folder-size": "^5.0.0",
|
||||
"globby": "^14.0.2",
|
||||
"ignore": "^7.0.3",
|
||||
|
||||
+2
-2
@@ -9,10 +9,10 @@ try {
|
||||
execSync("which xvfb-run", { stdio: "ignore" })
|
||||
|
||||
console.log("xvfb-run is installed. Running tests with xvfb-run...")
|
||||
execSync("xvfb-run -a npm run test:integration", { stdio: "inherit" })
|
||||
execSync("xvfb-run -a npm run test", { stdio: "inherit" })
|
||||
} else {
|
||||
console.log("Non-Linux environment detected. Running tests normally.")
|
||||
execSync("npm run test:integration", { stdio: "inherit" })
|
||||
execSync("npm run test", { stdio: "inherit" })
|
||||
}
|
||||
} catch (error) {
|
||||
if (process.platform === "linux") {
|
||||
|
||||
@@ -15,7 +15,6 @@ import { RequestyHandler } from "./providers/requesty"
|
||||
import { TogetherHandler } from "./providers/together"
|
||||
import { QwenHandler } from "./providers/qwen"
|
||||
import { MistralHandler } from "./providers/mistral"
|
||||
import { DoubaoHandler } from "./providers/doubao"
|
||||
import { VsCodeLmHandler } from "./providers/vscode-lm"
|
||||
import { ClineHandler } from "./providers/cline"
|
||||
import { LiteLlmHandler } from "./providers/litellm"
|
||||
@@ -62,8 +61,6 @@ export function buildApiHandler(configuration: ApiConfiguration): ApiHandler {
|
||||
return new TogetherHandler(options)
|
||||
case "qwen":
|
||||
return new QwenHandler(options)
|
||||
case "doubao":
|
||||
return new DoubaoHandler(options)
|
||||
case "mistral":
|
||||
return new MistralHandler(options)
|
||||
case "vscode-lm":
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
import { describe, it, beforeEach, afterEach, before } from "mocha"
|
||||
import "should"
|
||||
import sinon from "sinon"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { OllamaHandler } from "../ollama"
|
||||
import { ApiHandlerOptions } from "../../../shared/api"
|
||||
import axios from "axios"
|
||||
|
||||
describe("OllamaHandler", () => {
|
||||
let ollamaAvailable = false
|
||||
|
||||
// Check if Ollama is running before running tests
|
||||
before(async function () {
|
||||
this.timeout(5000)
|
||||
try {
|
||||
await axios.get("http://localhost:11434/api/version", { timeout: 2000 })
|
||||
ollamaAvailable = true
|
||||
} catch (error) {
|
||||
console.log("Ollama server not available, skipping tests")
|
||||
ollamaAvailable = false
|
||||
}
|
||||
})
|
||||
let handler: OllamaHandler
|
||||
let options: ApiHandlerOptions
|
||||
let clock: sinon.SinonFakeTimers
|
||||
|
||||
beforeEach(() => {
|
||||
options = {
|
||||
ollamaModelId: "llama2",
|
||||
ollamaBaseUrl: "http://localhost:11434",
|
||||
}
|
||||
handler = new OllamaHandler(options)
|
||||
// Use fake timers for testing timeouts
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore()
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
describe("createMessage", () => {
|
||||
it("should handle successful responses", async function () {
|
||||
if (!ollamaAvailable) {
|
||||
this.skip()
|
||||
}
|
||||
this.timeout(5000)
|
||||
// Mock the Ollama client's chat method
|
||||
const chatStub = sinon.stub(handler["client"], "chat").resolves({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
message: { content: "Hello, world!" },
|
||||
eval_count: 10,
|
||||
prompt_eval_count: 20,
|
||||
}
|
||||
},
|
||||
} as any)
|
||||
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
const result = []
|
||||
const usageInfo = []
|
||||
|
||||
// Collect the results
|
||||
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
|
||||
if (chunk.type === "text") {
|
||||
result.push(chunk.text)
|
||||
} else if (chunk.type === "usage") {
|
||||
usageInfo.push({
|
||||
inputTokens: chunk.inputTokens,
|
||||
outputTokens: chunk.outputTokens,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the results
|
||||
result.should.deepEqual(["Hello, world!"])
|
||||
usageInfo.should.deepEqual([{ inputTokens: 20, outputTokens: 10 }])
|
||||
chatStub.calledOnce.should.be.true()
|
||||
})
|
||||
|
||||
it("should handle timeout errors", async function () {
|
||||
if (!ollamaAvailable) {
|
||||
this.skip()
|
||||
}
|
||||
this.timeout(10000)
|
||||
// Restore real timers for this test
|
||||
clock.restore()
|
||||
|
||||
// Create a handler with a very short timeout for testing
|
||||
const testHandler = new OllamaHandler(options)
|
||||
|
||||
// Replace the createMessage method with one that has a shorter timeout
|
||||
testHandler.createMessage = async function* (systemPrompt, messages) {
|
||||
try {
|
||||
// Create a promise that rejects after a short timeout
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Ollama request timed out after 30 seconds")), 100)
|
||||
})
|
||||
|
||||
// Create a promise that never resolves
|
||||
const neverPromise = new Promise(() => {})
|
||||
|
||||
// Race them
|
||||
await Promise.race([timeoutPromise, neverPromise])
|
||||
} catch (error: any) {
|
||||
// Enhance error reporting
|
||||
console.error(`Ollama API error: ${error.message}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
// Start the request and catch the error
|
||||
let errorMessage = ""
|
||||
try {
|
||||
for await (const _ of testHandler.createMessage(systemPrompt, messages)) {
|
||||
// This should not be reached
|
||||
}
|
||||
} catch (error: any) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
// Check the result
|
||||
errorMessage.should.equal("Ollama request timed out after 30 seconds")
|
||||
|
||||
// Restore the fake timers for other tests
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
|
||||
it("should retry on errors when using the withRetry decorator", async function () {
|
||||
if (!ollamaAvailable) {
|
||||
this.skip()
|
||||
}
|
||||
this.timeout(10000)
|
||||
// Restore real timers for this test
|
||||
clock.restore()
|
||||
|
||||
// Mock the Ollama client's chat method to fail on first call and succeed on second
|
||||
const chatStub = sinon.stub(handler["client"], "chat")
|
||||
|
||||
// First call throws an error
|
||||
chatStub.onFirstCall().rejects(new Error("API Error"))
|
||||
|
||||
// Second call succeeds
|
||||
chatStub.onSecondCall().resolves({
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
yield {
|
||||
message: { content: "Success after retry" },
|
||||
}
|
||||
},
|
||||
} as any)
|
||||
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
const result = []
|
||||
|
||||
// Add a small delay to ensure the retry mechanism has time to work
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
|
||||
// Collect the results
|
||||
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
|
||||
if (chunk.type === "text") {
|
||||
result.push(chunk.text)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the results
|
||||
result.should.deepEqual(["Success after retry"])
|
||||
chatStub.calledTwice.should.be.true()
|
||||
|
||||
// Restore the fake timers for other tests
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
|
||||
it("should handle stream processing errors", async function () {
|
||||
if (!ollamaAvailable) {
|
||||
this.skip()
|
||||
}
|
||||
this.timeout(10000)
|
||||
// Restore real timers for this test
|
||||
clock.restore()
|
||||
|
||||
// Create a handler with a custom implementation for testing
|
||||
const testHandler = new OllamaHandler(options)
|
||||
|
||||
// Replace the createMessage method with one that simulates a stream error
|
||||
testHandler.createMessage = async function* (systemPrompt, messages) {
|
||||
// First yield a successful chunk
|
||||
yield {
|
||||
type: "text",
|
||||
text: "Partial response",
|
||||
}
|
||||
|
||||
// Then throw an error in the stream
|
||||
throw new Error("Ollama stream processing error: Stream error")
|
||||
}
|
||||
|
||||
const systemPrompt = "You are a helpful assistant."
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]
|
||||
|
||||
const result = []
|
||||
|
||||
// Collect the results and catch the error
|
||||
let errorMessage = ""
|
||||
try {
|
||||
for await (const chunk of testHandler.createMessage(systemPrompt, messages)) {
|
||||
if (chunk.type === "text") {
|
||||
result.push(chunk.text)
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
errorMessage = error.message
|
||||
}
|
||||
|
||||
// Verify the results
|
||||
errorMessage.should.equal("Ollama stream processing error: Stream error")
|
||||
result.should.deepEqual(["Partial response"])
|
||||
|
||||
// Restore the fake timers for other tests
|
||||
clock = sinon.useFakeTimers()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -114,7 +114,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
break
|
||||
}
|
||||
default: {
|
||||
stream = await this.client.messages.create({
|
||||
stream = (await this.client.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: model.info.maxTokens || 8192,
|
||||
temperature: 0,
|
||||
@@ -123,7 +123,7 @@ export class AnthropicHandler implements ApiHandler {
|
||||
// tools,
|
||||
// tool_choice: { type: "auto" },
|
||||
stream: true,
|
||||
})
|
||||
})) as any
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,15 +36,14 @@ export class DeepSeekHandler implements ApiHandler {
|
||||
}
|
||||
const deepUsage = usage as DeepSeekUsage
|
||||
|
||||
const inputTokens = deepUsage?.prompt_tokens || 0 // sum of cache hits and misses
|
||||
const inputTokens = deepUsage?.prompt_tokens || 0
|
||||
const outputTokens = deepUsage?.completion_tokens || 0
|
||||
const cacheReadTokens = deepUsage?.prompt_cache_hit_tokens || 0
|
||||
const cacheWriteTokens = deepUsage?.prompt_cache_miss_tokens || 0
|
||||
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens) // this will always be 0
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: nonCachedInputTokens,
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { ApiHandler } from ".."
|
||||
import { ApiHandlerOptions, doubaoDefaultModelId, DoubaoModelId, doubaoModels, ModelInfo } from "../../shared/api"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class DoubaoHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
private client: OpenAI
|
||||
constructor(options: ApiHandlerOptions) {
|
||||
this.options = options
|
||||
this.client = new OpenAI({
|
||||
baseURL: "https://ark.cn-beijing.volces.com/api/v3/",
|
||||
apiKey: this.options.doubaoApiKey,
|
||||
})
|
||||
}
|
||||
|
||||
getModel(): { id: DoubaoModelId; info: ModelInfo } {
|
||||
const modelId = this.options.apiModelId
|
||||
if (modelId && modelId in doubaoModels) {
|
||||
const id = modelId as DoubaoModelId
|
||||
return { id, info: doubaoModels[id] }
|
||||
}
|
||||
return {
|
||||
id: doubaoDefaultModelId,
|
||||
info: doubaoModels[doubaoDefaultModelId],
|
||||
}
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
let openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_completion_tokens: model.info.maxTokens,
|
||||
messages: openAiMessages,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
text: delta.content,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheReadTokens: chunk.usage.prompt_cache_hit_tokens || 0,
|
||||
// @ts-ignore-next-line
|
||||
cacheWriteTokens: chunk.usage.prompt_cache_miss_tokens || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,53 +59,18 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
const modelId = this.options.liteLlmModelId || liteLlmDefaultModelId
|
||||
const isOminiModel = modelId.includes("o1-mini") || modelId.includes("o3-mini")
|
||||
|
||||
// Configuration for extended thinking
|
||||
const budgetTokens = this.options.thinkingBudgetTokens || 0
|
||||
const reasoningOn = budgetTokens !== 0 ? true : false
|
||||
const thinkingConfig = reasoningOn ? { type: "enabled", budget_tokens: budgetTokens } : undefined
|
||||
|
||||
let temperature: number | undefined = 0
|
||||
|
||||
if (isOminiModel && reasoningOn) {
|
||||
temperature = undefined // Thinking mode doesn't support temperature
|
||||
if (isOminiModel) {
|
||||
temperature = undefined // does not support temperature
|
||||
}
|
||||
|
||||
// Define cache control object if prompt caching is enabled
|
||||
const cacheControl = this.options.liteLlmUsePromptCache ? { cache_control: { type: "ephemeral" } } : undefined
|
||||
|
||||
// Add cache_control to system message if enabled
|
||||
const enhancedSystemMessage = {
|
||||
...systemMessage,
|
||||
...(cacheControl && cacheControl),
|
||||
}
|
||||
|
||||
// Find the last two user messages to apply caching
|
||||
const userMsgIndices = formattedMessages.reduce(
|
||||
(acc, msg, index) => (msg.role === "user" ? [...acc, index] : acc),
|
||||
[] as number[],
|
||||
)
|
||||
const lastUserMsgIndex = userMsgIndices[userMsgIndices.length - 1] ?? -1
|
||||
const secondLastUserMsgIndex = userMsgIndices[userMsgIndices.length - 2] ?? -1
|
||||
|
||||
// Apply cache_control to the last two user messages if enabled
|
||||
const enhancedMessages = formattedMessages.map((message, index) => {
|
||||
if ((index === lastUserMsgIndex || index === secondLastUserMsgIndex) && cacheControl) {
|
||||
return {
|
||||
...message,
|
||||
...cacheControl,
|
||||
}
|
||||
}
|
||||
return message
|
||||
})
|
||||
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: this.options.liteLlmModelId || liteLlmDefaultModelId,
|
||||
messages: [enhancedSystemMessage, ...enhancedMessages],
|
||||
messages: [systemMessage, ...formattedMessages],
|
||||
temperature,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...(thinkingConfig && { thinking: thinkingConfig }), // Add thinking configuration when applicable
|
||||
})
|
||||
|
||||
const inputCost = (await this.calculateCost(1e6, 0)) || 0
|
||||
@@ -113,8 +78,6 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
// Handle normal text content
|
||||
if (delta?.content) {
|
||||
yield {
|
||||
type: "text",
|
||||
@@ -122,44 +85,13 @@ export class LiteLlmHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle reasoning events (thinking)
|
||||
// Thinking is not in the standard types but may be in the response
|
||||
interface ThinkingDelta {
|
||||
thinking?: string
|
||||
}
|
||||
|
||||
if ((delta as ThinkingDelta)?.thinking) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
reasoning: (delta as ThinkingDelta).thinking || "",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle token usage information
|
||||
if (chunk.usage) {
|
||||
const totalCost =
|
||||
(inputCost * chunk.usage.prompt_tokens) / 1e6 + (outputCost * chunk.usage.completion_tokens) / 1e6
|
||||
|
||||
// Extract cache-related information if available
|
||||
// Need to use type assertion since these properties are not in the standard OpenAI types
|
||||
const usage = chunk.usage as {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
cache_creation_input_tokens?: number
|
||||
prompt_cache_miss_tokens?: number
|
||||
cache_read_input_tokens?: number
|
||||
prompt_cache_hit_tokens?: number
|
||||
}
|
||||
|
||||
const cacheWriteTokens = usage.cache_creation_input_tokens || usage.prompt_cache_miss_tokens || 0
|
||||
const cacheReadTokens = usage.cache_read_input_tokens || usage.prompt_cache_hit_tokens || 0
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: usage.prompt_tokens || 0,
|
||||
outputTokens: usage.completion_tokens || 0,
|
||||
cacheWriteTokens: cacheWriteTokens > 0 ? cacheWriteTokens : undefined,
|
||||
cacheReadTokens: cacheReadTokens > 0 ? cacheReadTokens : undefined,
|
||||
inputTokens: chunk.usage.prompt_tokens || 0,
|
||||
outputTokens: chunk.usage.completion_tokens || 0,
|
||||
totalCost,
|
||||
}
|
||||
}
|
||||
|
||||
+13
-53
@@ -4,7 +4,6 @@ import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
|
||||
import { convertToOllamaMessages } from "../transform/ollama-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { withRetry } from "../retry"
|
||||
|
||||
export class OllamaHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -15,63 +14,24 @@ export class OllamaHandler implements ApiHandler {
|
||||
this.client = new Ollama({ host: this.options.ollamaBaseUrl || "http://localhost:11434" })
|
||||
}
|
||||
|
||||
@withRetry({ retryAllErrors: true })
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const ollamaMessages: Message[] = [{ role: "system", content: systemPrompt }, ...convertToOllamaMessages(messages)]
|
||||
|
||||
try {
|
||||
// Create a promise that rejects after timeout
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Ollama request timed out after 30 seconds")), 30000)
|
||||
})
|
||||
|
||||
// Create the actual API request promise
|
||||
const apiPromise = this.client.chat({
|
||||
model: this.getModel().id,
|
||||
messages: ollamaMessages,
|
||||
stream: true,
|
||||
options: {
|
||||
num_ctx: Number(this.options.ollamaApiOptionsCtxNum) || 32768,
|
||||
},
|
||||
})
|
||||
|
||||
// Race the API request against the timeout
|
||||
const stream = (await Promise.race([apiPromise, timeoutPromise])) as Awaited<typeof apiPromise>
|
||||
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
if (typeof chunk.message.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.message.content,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle token usage if available
|
||||
if (chunk.eval_count !== undefined || chunk.prompt_eval_count !== undefined) {
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: chunk.prompt_eval_count || 0,
|
||||
outputTokens: chunk.eval_count || 0,
|
||||
}
|
||||
}
|
||||
const stream = await this.client.chat({
|
||||
model: this.getModel().id,
|
||||
messages: ollamaMessages,
|
||||
stream: true,
|
||||
options: {
|
||||
num_ctx: Number(this.options.ollamaApiOptionsCtxNum) || 32768,
|
||||
},
|
||||
})
|
||||
for await (const chunk of stream) {
|
||||
if (typeof chunk.message.content === "string") {
|
||||
yield {
|
||||
type: "text",
|
||||
text: chunk.message.content,
|
||||
}
|
||||
} catch (streamError: any) {
|
||||
console.error("Error processing Ollama stream:", streamError)
|
||||
throw new Error(`Ollama stream processing error: ${streamError.message || "Unknown error"}`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Check if it's a timeout error
|
||||
if (error.message && error.message.includes("timed out")) {
|
||||
throw new Error("Ollama request timed out after 30 seconds")
|
||||
}
|
||||
|
||||
// Enhance error reporting
|
||||
const statusCode = error.status || error.statusCode
|
||||
const errorMessage = error.message || "Unknown error"
|
||||
|
||||
console.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { calculateApiCostOpenAI } from "../../utils/cost"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
|
||||
|
||||
export class OpenAiNativeHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -26,15 +26,14 @@ export class OpenAiNativeHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
private async *yieldUsage(info: ModelInfo, usage: OpenAI.Completions.CompletionUsage | undefined): ApiStream {
|
||||
const inputTokens = usage?.prompt_tokens || 0 // sum of cache hits and misses
|
||||
const inputTokens = usage?.prompt_tokens || 0
|
||||
const outputTokens = usage?.completion_tokens || 0
|
||||
const cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0
|
||||
const cacheWriteTokens = 0
|
||||
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens)
|
||||
yield {
|
||||
type: "usage",
|
||||
inputTokens: nonCachedInputTokens,
|
||||
inputTokens: inputTokens,
|
||||
outputTokens: outputTokens,
|
||||
cacheWriteTokens: cacheWriteTokens,
|
||||
cacheReadTokens: cacheReadTokens,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ApiHandler } from "../index"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { convertToR1Format } from "../transform/r1-format"
|
||||
import type { ChatCompletionReasoningEffort } from "openai/resources/chat/completions"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
|
||||
|
||||
export class OpenAiHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { ApiHandlerOptions, ModelInfo, requestyDefaultModelId, requestyDefaultModelInfo } from "../../shared/api"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
|
||||
import { ApiHandler } from "../index"
|
||||
import { withRetry } from "../retry"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { calculateApiCostOpenAI } from "../../utils/cost"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
|
||||
export class RequestyHandler implements ApiHandler {
|
||||
@@ -25,34 +24,21 @@ export class RequestyHandler implements ApiHandler {
|
||||
|
||||
@withRetry()
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const model = this.getModel()
|
||||
const modelId = this.options.requestyModelId ?? ""
|
||||
|
||||
const openAiMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
...convertToOpenAiMessages(messages),
|
||||
]
|
||||
|
||||
const reasoningEffort = this.options.o3MiniReasoningEffort || "medium"
|
||||
const reasoning = { reasoning_effort: reasoningEffort }
|
||||
const reasoningArgs = model.id === "openai/o3-mini" ? reasoning : {}
|
||||
|
||||
const thinkingBudget = this.options.thinkingBudgetTokens || 0
|
||||
const thinking =
|
||||
thinkingBudget > 0
|
||||
? { thinking: { type: "enabled", budget_tokens: thinkingBudget } }
|
||||
: { thinking: { type: "disabled" } }
|
||||
const thinkingArgs = model.id.includes("claude-3-7-sonnet") ? thinking : {}
|
||||
|
||||
// @ts-ignore-next-line
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: model.id,
|
||||
max_tokens: model.info.maxTokens || undefined,
|
||||
model: modelId,
|
||||
messages: openAiMessages,
|
||||
temperature: 0,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
...reasoningArgs,
|
||||
...thinkingArgs,
|
||||
...(modelId === "openai/o3-mini" ? { reasoning_effort: this.options.o3MiniReasoningEffort || "medium" } : {}),
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
@@ -87,7 +73,7 @@ export class RequestyHandler implements ApiHandler {
|
||||
const outputTokens = usage.completion_tokens || 0
|
||||
const cacheWriteTokens = usage.prompt_tokens_details?.caching_tokens || undefined
|
||||
const cacheReadTokens = usage.prompt_tokens_details?.cached_tokens || undefined
|
||||
const totalCost = calculateApiCostOpenAI(model.info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
const totalCost = 0 // TODO: Replace with calculateApiCostOpenAI(model.info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
|
||||
|
||||
yield {
|
||||
type: "usage",
|
||||
@@ -102,11 +88,9 @@ export class RequestyHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
getModel(): { id: string; info: ModelInfo } {
|
||||
const modelId = this.options.requestyModelId
|
||||
const modelInfo = this.options.requestyModelInfo
|
||||
if (modelId && modelInfo) {
|
||||
return { id: modelId, info: modelInfo }
|
||||
return {
|
||||
id: this.options.requestyModelId ?? "",
|
||||
info: openAiModelInfoSaneDefaults,
|
||||
}
|
||||
return { id: requestyDefaultModelId, info: requestyDefaultModelInfo }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
// For the following openrouter error type sources, see the docs here:
|
||||
// https://openrouter.ai/docs/api-reference/errors
|
||||
|
||||
export interface LanguageModelChatSelector {
|
||||
vendor?: string
|
||||
family?: string
|
||||
version?: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
export type OpenRouterErrorResponse = {
|
||||
error: {
|
||||
message: string
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ApiStream } from "../transform/stream"
|
||||
import { convertToVsCodeLmMessages } from "../transform/vscode-lm-format"
|
||||
import { SELECTOR_SEPARATOR, stringifyVsCodeLmModelSelector } from "../../shared/vsCodeSelectorUtils"
|
||||
import { ApiHandlerOptions, ModelInfo, openAiModelInfoSaneDefaults } from "../../shared/api"
|
||||
import type { LanguageModelChatSelector as LanguageModelChatSelectorFromTypes } from "./types"
|
||||
|
||||
// Cline does not update VSCode type definitions or engine requirements to maintain compatibility.
|
||||
// This declaration (as seen in src/integrations/TerminalManager.ts) provides types for the Language Model API in newer versions of VSCode.
|
||||
@@ -20,7 +19,12 @@ declare module "vscode" {
|
||||
Auto = 1,
|
||||
Required = 2,
|
||||
}
|
||||
interface LanguageModelChatSelector extends LanguageModelChatSelectorFromTypes {}
|
||||
interface LanguageModelChatSelector {
|
||||
vendor?: string
|
||||
family?: string
|
||||
version?: string
|
||||
id?: string
|
||||
}
|
||||
interface LanguageModelChatTool {
|
||||
name: string
|
||||
description: string
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ApiHandler } from "../"
|
||||
import { ApiHandlerOptions, XAIModelId, ModelInfo, xaiDefaultModelId, xaiModels } from "../../shared/api"
|
||||
import { convertToOpenAiMessages } from "../transform/openai-format"
|
||||
import { ApiStream } from "../transform/stream"
|
||||
import { ChatCompletionReasoningEffort } from "openai/resources/chat/completions.mjs"
|
||||
|
||||
export class XAIHandler implements ApiHandler {
|
||||
private options: ApiHandlerOptions
|
||||
@@ -19,23 +18,13 @@ export class XAIHandler implements ApiHandler {
|
||||
}
|
||||
|
||||
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
|
||||
const modelId = this.getModel().id
|
||||
// ensure reasoning effort is either "low" or "high" for grok-3-mini
|
||||
let reasoningEffort: ChatCompletionReasoningEffort | undefined
|
||||
if (modelId.includes("3-mini")) {
|
||||
let reasoningEffort = this.options.reasoningEffort
|
||||
if (reasoningEffort && !["low", "high"].includes(reasoningEffort)) {
|
||||
reasoningEffort = undefined
|
||||
}
|
||||
}
|
||||
const stream = await this.client.chat.completions.create({
|
||||
model: modelId,
|
||||
model: this.getModel().id,
|
||||
max_completion_tokens: this.getModel().info.maxTokens,
|
||||
temperature: 0,
|
||||
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
reasoning_effort: reasoningEffort,
|
||||
})
|
||||
|
||||
for await (const chunk of stream) {
|
||||
@@ -47,14 +36,6 @@ export class XAIHandler implements ApiHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if ("reasoning_content" in delta && delta.reasoning_content) {
|
||||
yield {
|
||||
type: "reasoning",
|
||||
// @ts-ignore-next-line
|
||||
reasoning: delta.reasoning_content,
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.usage) {
|
||||
yield {
|
||||
type: "usage",
|
||||
|
||||
@@ -20,7 +20,7 @@ export function convertAnthropicContentToGemini(content: string | Anthropic.Cont
|
||||
},
|
||||
} as InlineDataPart
|
||||
default:
|
||||
throw new Error(`Unsupported content block type: ${block.type}`)
|
||||
throw new Error(`Unsupported content block type: ${(block as any).type}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
// This file contains `declare module "vscode"` so we must import it.
|
||||
import "../providers/vscode-lm"
|
||||
import { describe, it } from "mocha"
|
||||
import "should"
|
||||
import * as vscode from "vscode"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { asObjectSafe, convertToAnthropicRole, convertToVsCodeLmMessages, convertToAnthropicMessage } from "./vscode-lm-format"
|
||||
|
||||
describe("asObjectSafe", () => {
|
||||
it("should handle falsy values", () => {
|
||||
asObjectSafe(0).should.deepEqual({})
|
||||
asObjectSafe("").should.deepEqual({})
|
||||
asObjectSafe(null).should.deepEqual({})
|
||||
asObjectSafe(undefined).should.deepEqual({})
|
||||
})
|
||||
|
||||
it("should parse valid JSON strings", () => {
|
||||
asObjectSafe('{"key": "value"}').should.deepEqual({ key: "value" })
|
||||
})
|
||||
|
||||
it("should return an empty object for invalid JSON strings", () => {
|
||||
asObjectSafe("invalid json").should.deepEqual({})
|
||||
})
|
||||
|
||||
it("should convert objects to plain objects", () => {
|
||||
const input = { prop: "value" }
|
||||
asObjectSafe(input).should.deepEqual(input)
|
||||
asObjectSafe(input).should.not.equal(input) // Should be a new object
|
||||
})
|
||||
|
||||
it("should convert arrays to plain objects", () => {
|
||||
const input = ["hello world"]
|
||||
asObjectSafe(input).should.deepEqual({ 0: "hello world" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("convertToAnthropicRole", () => {
|
||||
it("should convert VSCode roles to Anthropic roles", () => {
|
||||
// @ts-expect-error(Testing with an invalid role)
|
||||
const unknownRole = "unknown" as vscode.LanguageModelChatMessageRole
|
||||
;(convertToAnthropicRole(vscode.LanguageModelChatMessageRole.Assistant) === "assistant").should.be.true()
|
||||
;(convertToAnthropicRole(vscode.LanguageModelChatMessageRole.User) === "user").should.be.true()
|
||||
;(convertToAnthropicRole(unknownRole) === null).should.be.true()
|
||||
})
|
||||
})
|
||||
|
||||
describe("convertToVsCodeLmMessages", () => {
|
||||
it("should convert simple string messages", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: "Hi there" },
|
||||
]
|
||||
|
||||
const result = convertToVsCodeLmMessages(anthropicMessages)
|
||||
|
||||
result.should.have.length(2)
|
||||
result[0].role.should.equal(vscode.LanguageModelChatMessageRole.User)
|
||||
result[0].content[0].should.be.instanceof(vscode.LanguageModelTextPart)
|
||||
const textPart0 = result[0].content[0] as vscode.LanguageModelTextPart
|
||||
textPart0.should.have.property("value", "Hello")
|
||||
|
||||
result[1].role.should.equal(vscode.LanguageModelChatMessageRole.Assistant)
|
||||
result[1].content[0].should.be.instanceof(vscode.LanguageModelTextPart)
|
||||
const textPart1 = result[1].content[0] as vscode.LanguageModelTextPart
|
||||
textPart1.should.have.property("value", "Hi there")
|
||||
})
|
||||
|
||||
it("should convert complex user messages with tool results", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "User text" },
|
||||
{
|
||||
type: "tool_result",
|
||||
tool_use_id: "tool-123",
|
||||
content: [{ type: "text", text: "Tool result" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToVsCodeLmMessages(anthropicMessages)
|
||||
|
||||
result.should.have.length(1)
|
||||
result[0].role.should.equal(vscode.LanguageModelChatMessageRole.User)
|
||||
result[0].content.should.have.length(2)
|
||||
|
||||
// Check that the first content part is a ToolResultPart
|
||||
result[0].content[0].should.be.instanceof(vscode.LanguageModelToolResultPart)
|
||||
const toolResultPart = result[0].content[0] as vscode.LanguageModelToolResultPart
|
||||
toolResultPart.should.have.property("callId", "tool-123")
|
||||
|
||||
// Skip detailed testing of internal structure as it may vary
|
||||
// Just verify it's the right type with the right ID
|
||||
|
||||
// Check the second content part is a TextPart
|
||||
result[0].content[1].should.be.instanceof(vscode.LanguageModelTextPart)
|
||||
const textPart = result[0].content[1] as vscode.LanguageModelTextPart
|
||||
textPart.should.have.property("value", "User text")
|
||||
})
|
||||
|
||||
it("should convert complex assistant messages with tool calls", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "Assistant text" },
|
||||
{
|
||||
type: "tool_use",
|
||||
id: "tool-123",
|
||||
name: "testTool",
|
||||
input: { param: "value" },
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToVsCodeLmMessages(anthropicMessages)
|
||||
|
||||
result.should.have.length(1)
|
||||
result[0].role.should.equal(vscode.LanguageModelChatMessageRole.Assistant)
|
||||
result[0].content.should.have.length(2)
|
||||
|
||||
result[0].content[0].should.be.instanceof(vscode.LanguageModelToolCallPart)
|
||||
const toolCallPart = result[0].content[0] as vscode.LanguageModelToolCallPart
|
||||
toolCallPart.should.have.property("callId", "tool-123")
|
||||
toolCallPart.should.have.property("name", "testTool")
|
||||
toolCallPart.should.have.property("input")
|
||||
toolCallPart.input.should.deepEqual({ param: "value" })
|
||||
|
||||
result[0].content[1].should.be.instanceof(vscode.LanguageModelTextPart)
|
||||
const textPart = result[0].content[1] as vscode.LanguageModelTextPart
|
||||
textPart.should.have.property("value", "Assistant text")
|
||||
})
|
||||
|
||||
it("should handle image blocks with appropriate placeholders", () => {
|
||||
const anthropicMessages: Anthropic.Messages.MessageParam[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "image",
|
||||
source: {
|
||||
type: "base64",
|
||||
media_type: "image/jpeg",
|
||||
data: "base64data",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = convertToVsCodeLmMessages(anthropicMessages)
|
||||
|
||||
result.should.have.length(1)
|
||||
result[0].content[0].should.be.instanceof(vscode.LanguageModelTextPart)
|
||||
const textPart = result[0].content[0] as vscode.LanguageModelTextPart
|
||||
textPart.should.have.property("value")
|
||||
textPart.value.should.match(/Image \(base64\): image\/jpeg not supported by VSCode LM API/)
|
||||
})
|
||||
})
|
||||
|
||||
describe("convertToAnthropicMessage", () => {
|
||||
it("should convert VSCode assistant messages to Anthropic format", () => {
|
||||
const vsCodeMsg = vscode.LanguageModelChatMessage.Assistant([
|
||||
new vscode.LanguageModelTextPart("Test message"),
|
||||
new vscode.LanguageModelToolCallPart("tool-id", "testTool", { param: "value" }),
|
||||
])
|
||||
|
||||
const result = convertToAnthropicMessage(vsCodeMsg)
|
||||
|
||||
result.should.have.property("role", "assistant")
|
||||
result.should.have.property("content").which.is.an.Array()
|
||||
result.content.should.have.length(2)
|
||||
|
||||
// Check properties carefully to avoid null reference errors
|
||||
if (result.content && result.content.length >= 1) {
|
||||
const textContent = result.content[0]
|
||||
if (textContent) {
|
||||
textContent.should.have.property("type", "text")
|
||||
if (textContent.type === "text") {
|
||||
textContent.should.have.property("text", "Test message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.content && result.content.length >= 2) {
|
||||
const toolContent = result.content[1]
|
||||
if (toolContent) {
|
||||
toolContent.should.have.property("type", "tool_use")
|
||||
if (toolContent.type === "tool_use") {
|
||||
toolContent.should.have.property("id", "tool-id")
|
||||
toolContent.should.have.property("name", "testTool")
|
||||
toolContent.should.have.property("input").which.deepEqual({ param: "value" })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw an error for non-assistant messages", () => {
|
||||
const vsCodeMsg = vscode.LanguageModelChatMessage.User("User message")
|
||||
|
||||
try {
|
||||
convertToAnthropicMessage(vsCodeMsg)
|
||||
throw new Error("Should have thrown an error")
|
||||
} catch (error: any) {
|
||||
error.message.should.match(/Only assistant messages are supported/)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import * as vscode from "vscode"
|
||||
/**
|
||||
* Safely converts a value into a plain object.
|
||||
*/
|
||||
export function asObjectSafe(value: any): object {
|
||||
function asObjectSafe(value: any): object {
|
||||
// Handle null/undefined
|
||||
if (!value) {
|
||||
return {}
|
||||
@@ -145,9 +145,7 @@ export function convertToVsCodeLmMessages(
|
||||
return vsCodeLmMessages
|
||||
}
|
||||
|
||||
export function convertToAnthropicRole(
|
||||
vsCodeLmMessageRole: vscode.LanguageModelChatMessageRole,
|
||||
): Anthropic.Messages.MessageParam["role"] | null {
|
||||
export function convertToAnthropicRole(vsCodeLmMessageRole: vscode.LanguageModelChatMessageRole): string | null {
|
||||
switch (vsCodeLmMessageRole) {
|
||||
case vscode.LanguageModelChatMessageRole.Assistant:
|
||||
return "assistant"
|
||||
@@ -158,8 +156,10 @@ export function convertToAnthropicRole(
|
||||
}
|
||||
}
|
||||
|
||||
export function convertToAnthropicMessage(vsCodeLmMessage: vscode.LanguageModelChatMessage): Anthropic.Messages.Message {
|
||||
const anthropicRole = convertToAnthropicRole(vsCodeLmMessage.role)
|
||||
export async function convertToAnthropicMessage(
|
||||
vsCodeLmMessage: vscode.LanguageModelChatMessage,
|
||||
): Promise<Anthropic.Messages.Message> {
|
||||
const anthropicRole: string | null = convertToAnthropicRole(vsCodeLmMessage.role)
|
||||
if (anthropicRole !== "assistant") {
|
||||
throw new Error("Cline <Language Model API>: Only assistant messages are supported.")
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ export async function constructNewFileContent(diffContent: string, originalConte
|
||||
;[searchMatchIndex, searchEndIndex] = blockMatch
|
||||
} else {
|
||||
throw new Error(
|
||||
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file or was searched out of order in the provided blocks.`,
|
||||
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,7 @@ export const toolUseNames = [
|
||||
"access_mcp_resource",
|
||||
"ask_followup_question",
|
||||
"plan_mode_respond",
|
||||
"load_mcp_documentation",
|
||||
"attempt_completion",
|
||||
"new_task",
|
||||
] as const
|
||||
|
||||
// Converts array of tool call names into a union type ("execute_command" | "read_file" | ...)
|
||||
@@ -50,7 +48,6 @@ export const toolParamNames = [
|
||||
"options",
|
||||
"response",
|
||||
"result",
|
||||
"context",
|
||||
] as const
|
||||
|
||||
export type ToolParamName = (typeof toolParamNames)[number]
|
||||
|
||||
+23
-5
@@ -1,9 +1,9 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { ClineApiReqInfo, ClineMessage } from "../../../shared/ExtensionMessage"
|
||||
import { ApiHandler } from "../../../api"
|
||||
import { getContextWindowInfo } from "./context-window-utils"
|
||||
import { ClineApiReqInfo, ClineMessage } from "../../shared/ExtensionMessage"
|
||||
import { ApiHandler } from "../../api"
|
||||
import { OpenAiHandler } from "../../api/providers/openai"
|
||||
|
||||
class ContextManager {
|
||||
export class ContextManager {
|
||||
getNewContextMessagesAndMetadata(
|
||||
apiConversationHistory: Anthropic.Messages.MessageParam[],
|
||||
clineMessages: ClineMessage[],
|
||||
@@ -19,7 +19,25 @@ class ContextManager {
|
||||
if (previousRequest && previousRequest.text) {
|
||||
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
|
||||
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
|
||||
const { maxAllowedSize } = getContextWindowInfo(api)
|
||||
let contextWindow = api.getModel().info.contextWindow || 128_000
|
||||
// FIXME: hack to get anyone using openai compatible with deepseek to have the proper context window instead of the default 128k. We need a way for the user to specify the context window for models they input through openai compatible
|
||||
if (api instanceof OpenAiHandler && api.getModel().id.toLowerCase().includes("deepseek")) {
|
||||
contextWindow = 64_000
|
||||
}
|
||||
let maxAllowedSize: number
|
||||
switch (contextWindow) {
|
||||
case 64_000: // deepseek models
|
||||
maxAllowedSize = contextWindow - 27_000
|
||||
break
|
||||
case 128_000: // most models
|
||||
maxAllowedSize = contextWindow - 30_000
|
||||
break
|
||||
case 200_000: // claude models
|
||||
maxAllowedSize = contextWindow - 40_000
|
||||
break
|
||||
default:
|
||||
maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
|
||||
}
|
||||
|
||||
// This is the most reliable way to know when we're close to hitting the context window.
|
||||
if (totalTokens >= maxAllowedSize) {
|
||||
@@ -0,0 +1,10 @@
|
||||
export function checkIsOpenRouterContextWindowError(error: any): boolean {
|
||||
return error.code === 400 && error.message?.includes("context length")
|
||||
}
|
||||
|
||||
export function checkIsAnthropicContextWindowError(response: any): boolean {
|
||||
return (
|
||||
response?.error?.error?.type === "invalid_request_error" &&
|
||||
response?.error?.error?.message?.includes("prompt is too long")
|
||||
)
|
||||
}
|
||||
@@ -1,834 +0,0 @@
|
||||
import { getContextWindowInfo } from "./context-window-utils"
|
||||
import { formatResponse } from "../../prompts/responses"
|
||||
import { GlobalFileNames } from "../../storage/disk"
|
||||
import { fileExistsAtPath } from "../../../utils/fs"
|
||||
import * as path from "path"
|
||||
import fs from "fs/promises"
|
||||
import cloneDeep from "clone-deep"
|
||||
import { ClineApiReqInfo, ClineMessage } from "../../../shared/ExtensionMessage"
|
||||
import { ApiHandler } from "../../../api"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
enum EditType {
|
||||
UNDEFINED = 0,
|
||||
NO_FILE_READ = 1,
|
||||
READ_FILE_TOOL = 2,
|
||||
ALTER_FILE_TOOL = 3,
|
||||
FILE_MENTION = 4,
|
||||
}
|
||||
|
||||
// array of string values allows us to cover all changes for message types currently supported
|
||||
type MessageContent = string[]
|
||||
type MessageMetadata = string[][]
|
||||
|
||||
// Type for a single context update
|
||||
type ContextUpdate = [number, string, MessageContent, MessageMetadata] // [timestamp, updateType, update, metadata]
|
||||
|
||||
// Type for the serialized format of our nested maps
|
||||
type SerializedContextHistory = Array<
|
||||
[
|
||||
number, // messageIndex
|
||||
[
|
||||
number, // EditType (message type)
|
||||
Array<
|
||||
[
|
||||
number, // blockIndex
|
||||
ContextUpdate[], // updates array (now with 4 elements including metadata)
|
||||
]
|
||||
>,
|
||||
],
|
||||
]
|
||||
>
|
||||
|
||||
export class ContextManager {
|
||||
// mapping from the apiMessages outer index to the inner message index to a list of actual changes, ordered by timestamp
|
||||
// timestamp is required in order to support full checkpointing, where the changes we apply need to be able to be undone when
|
||||
// moving to an earlier conversation history checkpoint - this ordering intuitively allows for binary search on truncation
|
||||
// there is also a number stored for each (EditType) which defines which message type it is, for custom handling
|
||||
|
||||
// format: { outerIndex => [EditType, { innerIndex => [[timestamp, updateType, update], ...] }] }
|
||||
// example: { 1 => { [0, 0 => [[<timestamp>, "text", "[NOTE] Some previous conversation history with the user has been removed ..."], ...] }] }
|
||||
// the above example would be how we update the first assistant message to indicate we truncated text
|
||||
private contextHistoryUpdates: Map<number, [number, Map<number, ContextUpdate[]>]>
|
||||
|
||||
constructor() {
|
||||
this.contextHistoryUpdates = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* public function for loading contextHistoryUpdates from disk, if it exists
|
||||
*/
|
||||
async initializeContextHistory(taskDirectory: string) {
|
||||
this.contextHistoryUpdates = await this.getSavedContextHistory(taskDirectory)
|
||||
}
|
||||
|
||||
/**
|
||||
* get the stored context history updates from disk
|
||||
*/
|
||||
private async getSavedContextHistory(taskDirectory: string): Promise<Map<number, [number, Map<number, ContextUpdate[]>]>> {
|
||||
try {
|
||||
const filePath = path.join(taskDirectory, GlobalFileNames.contextHistory)
|
||||
if (await fileExistsAtPath(filePath)) {
|
||||
const data = await fs.readFile(filePath, "utf8")
|
||||
const serializedUpdates = JSON.parse(data) as SerializedContextHistory
|
||||
|
||||
// Update to properly reconstruct the tuple structure
|
||||
return new Map(
|
||||
serializedUpdates.map(([messageIndex, [numberValue, innerMapArray]]) => [
|
||||
messageIndex,
|
||||
[numberValue, new Map(innerMapArray)],
|
||||
]),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load context history:", error)
|
||||
}
|
||||
return new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* save the context history updates to disk
|
||||
*/
|
||||
private async saveContextHistory(taskDirectory: string) {
|
||||
try {
|
||||
const serializedUpdates: SerializedContextHistory = Array.from(this.contextHistoryUpdates.entries()).map(
|
||||
([messageIndex, [numberValue, innerMap]]) => [messageIndex, [numberValue, Array.from(innerMap.entries())]],
|
||||
)
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(taskDirectory, GlobalFileNames.contextHistory),
|
||||
JSON.stringify(serializedUpdates),
|
||||
"utf8",
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Failed to save context history:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* primary entry point for getting up to date context & truncating when required
|
||||
*/
|
||||
async getNewContextMessagesAndMetadata(
|
||||
apiConversationHistory: Anthropic.Messages.MessageParam[],
|
||||
clineMessages: ClineMessage[],
|
||||
api: ApiHandler,
|
||||
conversationHistoryDeletedRange: [number, number] | undefined,
|
||||
previousApiReqIndex: number,
|
||||
taskDirectory: string,
|
||||
) {
|
||||
let updatedConversationHistoryDeletedRange = false
|
||||
|
||||
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
|
||||
if (previousApiReqIndex >= 0) {
|
||||
const previousRequest = clineMessages[previousApiReqIndex]
|
||||
if (previousRequest && previousRequest.text) {
|
||||
const timestamp = previousRequest.ts
|
||||
const { tokensIn, tokensOut, cacheWrites, cacheReads }: ClineApiReqInfo = JSON.parse(previousRequest.text)
|
||||
const totalTokens = (tokensIn || 0) + (tokensOut || 0) + (cacheWrites || 0) + (cacheReads || 0)
|
||||
const { maxAllowedSize } = getContextWindowInfo(api)
|
||||
|
||||
// This is the most reliable way to know when we're close to hitting the context window.
|
||||
if (totalTokens >= maxAllowedSize) {
|
||||
// Since the user may switch between models with different context windows, truncating half may not be enough (ie if switching from claude 200k to deepseek 64k, half truncation will only remove 100k tokens, but we need to remove much more)
|
||||
// So if totalTokens/2 is greater than maxAllowedSize, we truncate 3/4 instead of 1/2
|
||||
const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half"
|
||||
|
||||
// we later check how many chars we trim to determine if we should still truncate history
|
||||
let [anyContextUpdates, uniqueFileReadIndices] = this.applyContextOptimizations(
|
||||
apiConversationHistory,
|
||||
conversationHistoryDeletedRange ? conversationHistoryDeletedRange[1] + 1 : 2,
|
||||
timestamp,
|
||||
)
|
||||
|
||||
let needToTruncate = true
|
||||
if (anyContextUpdates) {
|
||||
// determine whether we've saved enough chars to not truncate
|
||||
const charactersSavedPercentage = this.calculateContextOptimizationMetrics(
|
||||
apiConversationHistory,
|
||||
conversationHistoryDeletedRange,
|
||||
uniqueFileReadIndices,
|
||||
)
|
||||
if (charactersSavedPercentage >= 0.3) {
|
||||
needToTruncate = false
|
||||
}
|
||||
}
|
||||
|
||||
if (needToTruncate) {
|
||||
// go ahead with truncation
|
||||
anyContextUpdates = this.applyStandardContextTruncationNoticeChange(timestamp) || anyContextUpdates
|
||||
|
||||
// NOTE: it's okay that we overwriteConversationHistory in resume task since we're only ever removing the last user message and not anything in the middle which would affect this range
|
||||
conversationHistoryDeletedRange = this.getNextTruncationRange(
|
||||
apiConversationHistory,
|
||||
conversationHistoryDeletedRange,
|
||||
keep,
|
||||
)
|
||||
|
||||
updatedConversationHistoryDeletedRange = true
|
||||
}
|
||||
|
||||
// if we alter the context history, save the updated version to disk
|
||||
if (anyContextUpdates) {
|
||||
await this.saveContextHistory(taskDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const truncatedConversationHistory = this.getAndAlterTruncatedMessages(
|
||||
apiConversationHistory,
|
||||
conversationHistoryDeletedRange,
|
||||
)
|
||||
|
||||
return {
|
||||
conversationHistoryDeletedRange: conversationHistoryDeletedRange,
|
||||
updatedConversationHistoryDeletedRange: updatedConversationHistoryDeletedRange,
|
||||
truncatedConversationHistory: truncatedConversationHistory,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* get truncation range
|
||||
*/
|
||||
public getNextTruncationRange(
|
||||
apiMessages: Anthropic.Messages.MessageParam[],
|
||||
currentDeletedRange: [number, number] | undefined,
|
||||
keep: "half" | "quarter",
|
||||
): [number, number] {
|
||||
// We always keep the first user-assistant pairing, and truncate an even number of messages from there
|
||||
const rangeStartIndex = 2 // index 0 and 1 are kept
|
||||
const startOfRest = currentDeletedRange ? currentDeletedRange[1] + 1 : 2 // inclusive starting index
|
||||
|
||||
let messagesToRemove: number
|
||||
if (keep === "half") {
|
||||
// Remove half of remaining user-assistant pairs
|
||||
// We first calculate half of the messages then divide by 2 to get the number of pairs.
|
||||
// After flooring, we multiply by 2 to get the number of messages.
|
||||
// Note that this will also always be an even number.
|
||||
messagesToRemove = Math.floor((apiMessages.length - startOfRest) / 4) * 2 // Keep even number
|
||||
} else {
|
||||
// Remove 3/4 of remaining user-assistant pairs
|
||||
// We calculate 3/4ths of the messages then divide by 2 to get the number of pairs.
|
||||
// After flooring, we multiply by 2 to get the number of messages.
|
||||
// Note that this will also always be an even number.
|
||||
messagesToRemove = Math.floor(((apiMessages.length - startOfRest) * 3) / 4 / 2) * 2
|
||||
}
|
||||
|
||||
let rangeEndIndex = startOfRest + messagesToRemove - 1 // inclusive ending index
|
||||
|
||||
// Make sure that the last message being removed is a assistant message, so the next message after the initial user-assistant pair is an assistant message. This preserves the user-assistant-user-assistant structure.
|
||||
// NOTE: anthropic format messages are always user-assistant-user-assistant, while openai format messages can have multiple user messages in a row (we use anthropic format throughout cline)
|
||||
if (apiMessages[rangeEndIndex].role !== "assistant") {
|
||||
rangeEndIndex -= 1
|
||||
}
|
||||
|
||||
// this is an inclusive range that will be removed from the conversation history
|
||||
return [rangeStartIndex, rangeEndIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
* external interface to support old calls
|
||||
*/
|
||||
public getTruncatedMessages(
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
deletedRange: [number, number] | undefined,
|
||||
): Anthropic.Messages.MessageParam[] {
|
||||
return this.getAndAlterTruncatedMessages(messages, deletedRange)
|
||||
}
|
||||
|
||||
/**
|
||||
* apply all required truncation methods to the messages in context
|
||||
*/
|
||||
private getAndAlterTruncatedMessages(
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
deletedRange: [number, number] | undefined,
|
||||
): Anthropic.Messages.MessageParam[] {
|
||||
if (messages.length <= 1) {
|
||||
return messages
|
||||
}
|
||||
|
||||
const updatedMessages = this.applyContextHistoryUpdates(messages, deletedRange ? deletedRange[1] + 1 : 2)
|
||||
|
||||
// OLD NOTE: if you try to console log these, don't forget that logging a reference to an array may not provide the same result as logging a slice() snapshot of that array at that exact moment. The following DOES in fact include the latest assistant message.
|
||||
return updatedMessages
|
||||
}
|
||||
|
||||
/**
|
||||
* applies deletedRange truncation and other alterations based on changes in this.contextHistoryUpdates
|
||||
*/
|
||||
private applyContextHistoryUpdates(
|
||||
messages: Anthropic.Messages.MessageParam[],
|
||||
startFromIndex: number,
|
||||
): Anthropic.Messages.MessageParam[] {
|
||||
// runtime is linear in length of user messages, if expecting a limited number of alterations, could be more optimal to loop over alterations
|
||||
|
||||
const firstChunk = messages.slice(0, 2) // get first user-assistant pair
|
||||
const secondChunk = messages.slice(startFromIndex) // get remaining messages within context
|
||||
const messagesToUpdate = [...firstChunk, ...secondChunk]
|
||||
|
||||
// we need the mapping from the local indices in messagesToUpdate to the global array of updates in this.contextHistoryUpdates
|
||||
const originalIndices = [
|
||||
...Array(2).keys(),
|
||||
...Array(secondChunk.length)
|
||||
.fill(0)
|
||||
.map((_, i) => i + startFromIndex),
|
||||
]
|
||||
|
||||
for (let arrayIndex = 0; arrayIndex < messagesToUpdate.length; arrayIndex++) {
|
||||
const messageIndex = originalIndices[arrayIndex]
|
||||
|
||||
const innerTuple = this.contextHistoryUpdates.get(messageIndex)
|
||||
if (!innerTuple) {
|
||||
continue
|
||||
}
|
||||
|
||||
// because we are altering this, we need a deep copy
|
||||
messagesToUpdate[arrayIndex] = cloneDeep(messagesToUpdate[arrayIndex])
|
||||
|
||||
// Extract the map from the tuple
|
||||
const innerMap = innerTuple[1]
|
||||
for (const [blockIndex, changes] of innerMap) {
|
||||
// apply the latest change among n changes - [timestamp, updateType, update]
|
||||
const latestChange = changes[changes.length - 1]
|
||||
|
||||
if (latestChange[1] === "text") {
|
||||
// only altering text for now
|
||||
const message = messagesToUpdate[arrayIndex]
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
const block = message.content[blockIndex]
|
||||
if (block && block.type === "text") {
|
||||
block.text = latestChange[2][0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messagesToUpdate
|
||||
}
|
||||
|
||||
/**
|
||||
* removes all context history updates that occurred after the specified timestamp and saves to disk
|
||||
*/
|
||||
async truncateContextHistory(timestamp: number, taskDirectory: string): Promise<void> {
|
||||
this.truncateContextHistoryAtTimestamp(this.contextHistoryUpdates, timestamp)
|
||||
|
||||
// save the modified context history to disk
|
||||
await this.saveContextHistory(taskDirectory)
|
||||
}
|
||||
|
||||
/**
|
||||
* alters the context history to remove all alterations after a given timestamp
|
||||
* removes the index if there are no alterations there anymore, both outer and inner indices
|
||||
*/
|
||||
private truncateContextHistoryAtTimestamp(
|
||||
contextHistory: Map<number, [number, Map<number, ContextUpdate[]>]>,
|
||||
timestamp: number,
|
||||
): void {
|
||||
for (const [messageIndex, [_, innerMap]] of contextHistory) {
|
||||
// track which blockIndices to delete
|
||||
const blockIndicesToDelete: number[] = []
|
||||
|
||||
// loop over the innerIndices of the messages in this block
|
||||
for (const [blockIndex, updates] of innerMap) {
|
||||
// updates ordered by timestamp, so find cutoff point by iterating from right to left
|
||||
let cutoffIndex = updates.length - 1
|
||||
while (cutoffIndex >= 0 && updates[cutoffIndex][0] > timestamp) {
|
||||
cutoffIndex--
|
||||
}
|
||||
|
||||
// If we found updates to remove
|
||||
if (cutoffIndex < updates.length - 1) {
|
||||
// Modify the array in place to keep only updates up to cutoffIndex
|
||||
updates.length = cutoffIndex + 1
|
||||
|
||||
// If no updates left after truncation, mark this block for deletion
|
||||
if (updates.length === 0) {
|
||||
blockIndicesToDelete.push(blockIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty blocks from inner map
|
||||
for (const blockIndex of blockIndicesToDelete) {
|
||||
innerMap.delete(blockIndex)
|
||||
}
|
||||
|
||||
// If inner map is now empty, remove the message index from outer map
|
||||
if (innerMap.size === 0) {
|
||||
contextHistory.delete(messageIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* applies the context optimization steps and returns whether any changes were made
|
||||
*/
|
||||
private applyContextOptimizations(
|
||||
apiMessages: Anthropic.Messages.MessageParam[],
|
||||
startFromIndex: number,
|
||||
timestamp: number,
|
||||
): [boolean, Set<number>] {
|
||||
const [fileReadUpdatesBool, uniqueFileReadIndices] = this.findAndPotentiallySaveFileReadContextHistoryUpdates(
|
||||
apiMessages,
|
||||
startFromIndex,
|
||||
timestamp,
|
||||
)
|
||||
|
||||
// true if any context optimization steps alter state
|
||||
const contextHistoryUpdated = fileReadUpdatesBool
|
||||
|
||||
return [contextHistoryUpdated, uniqueFileReadIndices]
|
||||
}
|
||||
|
||||
/**
|
||||
* if there is any truncation and there is no other alteration already set, alter the assistant message to indicate this occurred
|
||||
*/
|
||||
private applyStandardContextTruncationNoticeChange(timestamp: number): boolean {
|
||||
if (!this.contextHistoryUpdates.has(1)) {
|
||||
// first assistant message always at index 1
|
||||
const innerMap = new Map<number, ContextUpdate[]>()
|
||||
innerMap.set(0, [[timestamp, "text", [formatResponse.contextTruncationNotice()], []]])
|
||||
this.contextHistoryUpdates.set(1, [0, innerMap]) // EditType is undefined for first assistant message
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* wraps the logic for determining file reads to overwrite, and altering state
|
||||
* returns whether any updates were made (bool) and indices where updates were made
|
||||
*/
|
||||
private findAndPotentiallySaveFileReadContextHistoryUpdates(
|
||||
apiMessages: Anthropic.Messages.MessageParam[],
|
||||
startFromIndex: number,
|
||||
timestamp: number,
|
||||
): [boolean, Set<number>] {
|
||||
const [fileReadIndices, messageFilePaths] = this.getPossibleDuplicateFileReads(apiMessages, startFromIndex)
|
||||
return this.applyFileReadContextHistoryUpdates(fileReadIndices, messageFilePaths, apiMessages, timestamp)
|
||||
}
|
||||
|
||||
/**
|
||||
* generate a mapping from unique file reads from multiple tool calls to their outer index position(s)
|
||||
* also return additional metadata to support multiple file reads in file mention text blocks
|
||||
*/
|
||||
private getPossibleDuplicateFileReads(
|
||||
apiMessages: Anthropic.Messages.MessageParam[],
|
||||
startFromIndex: number,
|
||||
): [Map<string, [number, number, string, string][]>, Map<number, string[]>] {
|
||||
// fileReadIndices: { fileName => [outerIndex, EditType, searchText, replaceText] }
|
||||
// messageFilePaths: { outerIndex => [fileRead1, fileRead2, ..] }
|
||||
// searchText in fileReadIndices is only required for file mention file-reads since there can be more than one file in the text
|
||||
// searchText will be the empty string "" in the case that it's not required, for non-file mentions
|
||||
// messageFilePaths is only used for file mentions as there can be multiple files read in the same text chunk
|
||||
|
||||
// for all text blocks per file, has info for updating the block
|
||||
const fileReadIndices = new Map<string, [number, number, string, string][]>()
|
||||
|
||||
// for file mention text blocks, track all the unique files read
|
||||
const messageFilePaths = new Map<number, string[]>()
|
||||
|
||||
for (let i = startFromIndex; i < apiMessages.length; i++) {
|
||||
let thisExistingFileReads: string[] = []
|
||||
|
||||
if (this.contextHistoryUpdates.has(i)) {
|
||||
const innerTuple = this.contextHistoryUpdates.get(i)
|
||||
|
||||
if (innerTuple) {
|
||||
// safety check
|
||||
const editType = innerTuple[0]
|
||||
|
||||
if (editType === EditType.FILE_MENTION) {
|
||||
const innerMap = innerTuple[1]
|
||||
|
||||
const blockIndex = 1 // file mention blocks assumed to be at index 1
|
||||
const blockUpdates = innerMap.get(blockIndex)
|
||||
|
||||
// if we have updated this text previously, we want to check whether the lists of files in the metadata are the same
|
||||
if (blockUpdates && blockUpdates.length > 0) {
|
||||
// the first list indicates the files we have replaced in this text, second list indicates all unique files in this text
|
||||
// if they are equal then we have replaced all the files in this text already, and can ignore further processing
|
||||
if (
|
||||
blockUpdates[blockUpdates.length - 1][3][0].length ===
|
||||
blockUpdates[blockUpdates.length - 1][3][1].length
|
||||
) {
|
||||
continue
|
||||
}
|
||||
// otherwise there are still file reads here we can overwrite, so still need to process this text chunk
|
||||
// to do so we need to keep track of which files we've already replaced so we don't replace them again
|
||||
else {
|
||||
thisExistingFileReads = blockUpdates[blockUpdates.length - 1][3][0]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// for all other cases we can assume that we dont need to check this again
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const message = apiMessages[i]
|
||||
if (message.role === "user" && Array.isArray(message.content) && message.content.length > 0) {
|
||||
const firstBlock = message.content[0]
|
||||
if (firstBlock.type === "text") {
|
||||
const matchTup = this.parsePotentialToolCall(firstBlock.text)
|
||||
let foundNormalFileRead = false
|
||||
if (matchTup) {
|
||||
if (matchTup[0] === "read_file") {
|
||||
this.handleReadFileToolCall(i, matchTup[1], fileReadIndices)
|
||||
foundNormalFileRead = true
|
||||
} else if (matchTup[0] === "replace_in_file" || matchTup[0] === "write_to_file") {
|
||||
if (message.content.length > 1) {
|
||||
const secondBlock = message.content[1]
|
||||
if (secondBlock.type === "text") {
|
||||
this.handlePotentialFileChangeToolCalls(i, matchTup[1], secondBlock.text, fileReadIndices)
|
||||
foundNormalFileRead = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// file mentions can happen in most other user message blocks
|
||||
if (!foundNormalFileRead) {
|
||||
if (message.content.length > 1) {
|
||||
const secondBlock = message.content[1]
|
||||
if (secondBlock.type === "text") {
|
||||
const [hasFileRead, filePaths] = this.handlePotentialFileMentionCalls(
|
||||
i,
|
||||
secondBlock.text,
|
||||
fileReadIndices,
|
||||
thisExistingFileReads, // file reads we've already replaced in this text in the latest version of this updated text
|
||||
)
|
||||
if (hasFileRead) {
|
||||
messageFilePaths.set(i, filePaths) // all file paths in this string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [fileReadIndices, messageFilePaths]
|
||||
}
|
||||
|
||||
/**
|
||||
* handles potential file content mentions in text blocks
|
||||
* there will not be more than one of the same file read in a text block
|
||||
*/
|
||||
private handlePotentialFileMentionCalls(
|
||||
i: number,
|
||||
secondBlockText: string,
|
||||
fileReadIndices: Map<string, [number, number, string, string][]>,
|
||||
thisExistingFileReads: string[],
|
||||
): [boolean, string[]] {
|
||||
const pattern = new RegExp(`<file_content path="([^"]*)">([\\s\\S]*?)</file_content>`, "g")
|
||||
|
||||
let foundMatch = false
|
||||
const filePaths: string[] = []
|
||||
|
||||
let match
|
||||
while ((match = pattern.exec(secondBlockText)) !== null) {
|
||||
foundMatch = true
|
||||
|
||||
const filePath = match[1]
|
||||
filePaths.push(filePath) // we will record all unique paths from file mentions in this text
|
||||
|
||||
// we can assume that thisExistingFileReads does not have many entries
|
||||
if (!thisExistingFileReads.includes(filePath)) {
|
||||
// meaning we havent already replaced this file read
|
||||
|
||||
const entireMatch = match[0] // The entire matched string
|
||||
|
||||
// Create the replacement text - keep the tags but replace the content
|
||||
const replacementText = `<file_content path="${filePath}">${formatResponse.duplicateFileReadNotice()}</file_content>`
|
||||
|
||||
const indices = fileReadIndices.get(filePath) || []
|
||||
indices.push([i, EditType.FILE_MENTION, entireMatch, replacementText])
|
||||
fileReadIndices.set(filePath, indices)
|
||||
}
|
||||
}
|
||||
|
||||
return [foundMatch, filePaths]
|
||||
}
|
||||
|
||||
/**
|
||||
* parses specific tool call formats, returns null if no acceptable format is found
|
||||
*/
|
||||
private parsePotentialToolCall(text: string): [string, string] | null {
|
||||
const match = text.match(/^\[([^\s]+) for '([^']+)'\] Result:$/)
|
||||
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
|
||||
return [match[1], match[2]]
|
||||
}
|
||||
|
||||
/**
|
||||
* file_read tool call always pastes the file, so this is always a hit
|
||||
*/
|
||||
private handleReadFileToolCall(
|
||||
i: number,
|
||||
filePath: string,
|
||||
fileReadIndices: Map<string, [number, number, string, string][]>,
|
||||
) {
|
||||
const indices = fileReadIndices.get(filePath) || []
|
||||
indices.push([i, EditType.READ_FILE_TOOL, "", formatResponse.duplicateFileReadNotice()])
|
||||
fileReadIndices.set(filePath, indices)
|
||||
}
|
||||
|
||||
/**
|
||||
* write_to_file and replace_in_file tool output are handled similarly
|
||||
*/
|
||||
private handlePotentialFileChangeToolCalls(
|
||||
i: number,
|
||||
filePath: string,
|
||||
secondBlockText: string,
|
||||
fileReadIndices: Map<string, [number, number, string, string][]>,
|
||||
) {
|
||||
const pattern = new RegExp(`(<final_file_content path="[^"]*">)[\\s\\S]*?(</final_file_content>)`)
|
||||
|
||||
// check if this exists in the text, it wont exist if the user rejects the file change for example
|
||||
if (pattern.test(secondBlockText)) {
|
||||
const replacementText = secondBlockText.replace(pattern, `$1 ${formatResponse.duplicateFileReadNotice()} $2`)
|
||||
const indices = fileReadIndices.get(filePath) || []
|
||||
indices.push([i, EditType.ALTER_FILE_TOOL, "", replacementText])
|
||||
fileReadIndices.set(filePath, indices)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* alter all occurrences of file read operations and track which messages were updated
|
||||
* returns the outer index of messages we alter, to count number of changes
|
||||
*/
|
||||
private applyFileReadContextHistoryUpdates(
|
||||
fileReadIndices: Map<string, [number, number, string, string][]>,
|
||||
messageFilePaths: Map<number, string[]>,
|
||||
apiMessages: Anthropic.Messages.MessageParam[],
|
||||
timestamp: number,
|
||||
): [boolean, Set<number>] {
|
||||
let didUpdate = false
|
||||
const updatedMessageIndices = new Set<number>() // track which messages we update on this round
|
||||
const fileMentionUpdates = new Map<number, [string, string[]]>()
|
||||
|
||||
for (const [filePath, indices] of fileReadIndices.entries()) {
|
||||
// Only process if there are multiple reads of the same file, else we will want to keep the latest read of the file
|
||||
if (indices.length > 1) {
|
||||
// Process all but the last index, as we will keep that instance of the file read
|
||||
for (let i = 0; i < indices.length - 1; i++) {
|
||||
const messageIndex = indices[i][0]
|
||||
const messageType = indices[i][1] // EditType value
|
||||
const searchText = indices[i][2] // search text (for file mentions, else empty string)
|
||||
const messageString = indices[i][3] // what we will replace the string with
|
||||
|
||||
didUpdate = true
|
||||
updatedMessageIndices.add(messageIndex)
|
||||
|
||||
// for single-fileread text we can set the updates here
|
||||
// for potential multi-fileread text we need to determine all changes & iteratively update the text prior to saving the final change
|
||||
if (messageType === EditType.FILE_MENTION) {
|
||||
if (!fileMentionUpdates.has(messageIndex)) {
|
||||
// Get base text either from existing updates or from apiMessages
|
||||
let baseText = ""
|
||||
let prevFilesReplaced: string[] = []
|
||||
|
||||
const innerTuple = this.contextHistoryUpdates.get(messageIndex)
|
||||
if (innerTuple) {
|
||||
const blockUpdates = innerTuple[1].get(1) // assumed index=1 for file mention filereads
|
||||
if (blockUpdates && blockUpdates.length > 0) {
|
||||
baseText = blockUpdates[blockUpdates.length - 1][2][0] // index 0 of MessageContent
|
||||
prevFilesReplaced = blockUpdates[blockUpdates.length - 1][3][0] // previously overwritten file reads in this text
|
||||
}
|
||||
}
|
||||
|
||||
// can assume that this content will exist, otherwise it would not have been in fileReadIndices
|
||||
const messageContent = apiMessages[messageIndex]?.content
|
||||
if (!baseText && Array.isArray(messageContent) && messageContent.length > 1) {
|
||||
const contentBlock = messageContent[1] // assume index=1 for all text to replace for file mention filereads
|
||||
if (contentBlock.type === "text") {
|
||||
baseText = contentBlock.text
|
||||
}
|
||||
}
|
||||
|
||||
// prevFilesReplaced keeps track of the previous file reads we've replace in this string, empty array if none
|
||||
fileMentionUpdates.set(messageIndex, [baseText, prevFilesReplaced])
|
||||
}
|
||||
|
||||
// Replace searchText with messageString for all file reads we need to replace in this text
|
||||
if (searchText) {
|
||||
const currentTuple = fileMentionUpdates.get(messageIndex) || ["", []]
|
||||
if (currentTuple[0]) {
|
||||
// safety check
|
||||
// replace this text chunk
|
||||
const updatedText = currentTuple[0].replace(searchText, messageString)
|
||||
|
||||
// add the newly added filePath read
|
||||
const updatedFileReads = currentTuple[1]
|
||||
updatedFileReads.push(filePath)
|
||||
|
||||
fileMentionUpdates.set(messageIndex, [updatedText, updatedFileReads])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let innerTuple = this.contextHistoryUpdates.get(messageIndex)
|
||||
let innerMap: Map<number, ContextUpdate[]>
|
||||
|
||||
if (!innerTuple) {
|
||||
innerMap = new Map<number, ContextUpdate[]>()
|
||||
this.contextHistoryUpdates.set(messageIndex, [messageType, innerMap])
|
||||
} else {
|
||||
innerMap = innerTuple[1]
|
||||
}
|
||||
|
||||
// block index for file reads from read_file, write_to_file, replace_in_file tools is 1
|
||||
const blockIndex = 1
|
||||
|
||||
const updates = innerMap.get(blockIndex) || []
|
||||
|
||||
// metadata array is empty for non-file mention occurrences
|
||||
updates.push([timestamp, "text", [messageString], []])
|
||||
|
||||
innerMap.set(blockIndex, updates)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// apply file mention updates to contextHistoryUpdates
|
||||
// in fileMentionUpdates, filePathsUpdated includes all the file paths which are updated in the latest version of this altered text
|
||||
for (const [messageIndex, [updatedText, filePathsUpdated]] of fileMentionUpdates.entries()) {
|
||||
let innerTuple = this.contextHistoryUpdates.get(messageIndex)
|
||||
let innerMap: Map<number, ContextUpdate[]>
|
||||
|
||||
if (!innerTuple) {
|
||||
innerMap = new Map<number, ContextUpdate[]>()
|
||||
this.contextHistoryUpdates.set(messageIndex, [EditType.FILE_MENTION, innerMap])
|
||||
} else {
|
||||
innerMap = innerTuple[1]
|
||||
}
|
||||
|
||||
const blockIndex = 1 // we only consider the block index of 1 for file mentions
|
||||
const updates = innerMap.get(blockIndex) || []
|
||||
|
||||
// filePathsUpdated includes changes done previously to this timestamp, and right now
|
||||
if (messageFilePaths.has(messageIndex)) {
|
||||
const allFileReads = messageFilePaths.get(messageIndex)
|
||||
if (allFileReads) {
|
||||
// safety check
|
||||
// we gather all the file reads possible in this text from messageFilePaths
|
||||
// filePathsUpdated from fileMentionUpdates stores all the files reads we have replaced now & previously
|
||||
updates.push([timestamp, "text", [updatedText], [filePathsUpdated, allFileReads]])
|
||||
innerMap.set(blockIndex, updates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [didUpdate, updatedMessageIndices]
|
||||
}
|
||||
|
||||
/**
|
||||
* count total characters in messages and total savings within this range
|
||||
*/
|
||||
private countCharactersAndSavingsInRange(
|
||||
apiMessages: Anthropic.Messages.MessageParam[],
|
||||
startIndex: number,
|
||||
endIndex: number,
|
||||
uniqueFileReadIndices: Set<number>,
|
||||
): { totalCharacters: number; charactersSaved: number } {
|
||||
let totalCharCount = 0
|
||||
let totalCharactersSaved = 0
|
||||
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
// looping over the outer indicies of messages
|
||||
const message = apiMessages[i]
|
||||
|
||||
if (!message.content) {
|
||||
continue
|
||||
}
|
||||
|
||||
// hasExistingAlterations checks whether the outer idnex has any changes
|
||||
// hasExistingAlterations will also include the alterations we just made
|
||||
const hasExistingAlterations = this.contextHistoryUpdates.has(i)
|
||||
const hasNewAlterations = uniqueFileReadIndices.has(i)
|
||||
|
||||
if (Array.isArray(message.content)) {
|
||||
for (let blockIndex = 0; blockIndex < message.content.length; blockIndex++) {
|
||||
// looping over inner indices of messages
|
||||
const block = message.content[blockIndex]
|
||||
|
||||
if (block.type === "text" && block.text) {
|
||||
// true if we just altered it, or it was altered before
|
||||
if (hasExistingAlterations) {
|
||||
const innerTuple = this.contextHistoryUpdates.get(i)
|
||||
const updates = innerTuple?.[1].get(blockIndex) // updated text for this inner index
|
||||
|
||||
if (updates && updates.length > 0) {
|
||||
// exists if we have an update for the message at this index
|
||||
const latestUpdate = updates[updates.length - 1]
|
||||
|
||||
// if block was just altered, then calculate savings
|
||||
if (hasNewAlterations) {
|
||||
let originalTextLength
|
||||
if (updates.length > 1) {
|
||||
originalTextLength = updates[updates.length - 2][2][0].length // handles case if we have multiple updates for same text block
|
||||
} else {
|
||||
originalTextLength = block.text.length
|
||||
}
|
||||
|
||||
const newTextLength = latestUpdate[2][0].length // replacement text
|
||||
totalCharactersSaved += originalTextLength - newTextLength
|
||||
|
||||
totalCharCount += originalTextLength
|
||||
} else {
|
||||
// meaning there was an update to this text previously, but we didnt just alter it
|
||||
totalCharCount += latestUpdate[2][0].length
|
||||
}
|
||||
} else {
|
||||
// reach here if there was one inner index with an update, but now we are at a different index, so updates is not defined
|
||||
totalCharCount += block.text.length
|
||||
}
|
||||
} else {
|
||||
// reach here if there's no alterations for this outer index, meaning each inner index wont have any changes either
|
||||
totalCharCount += block.text.length
|
||||
}
|
||||
} else if (block.type === "image" && block.source) {
|
||||
if (block.source.type === "base64" && block.source.data) {
|
||||
totalCharCount += block.source.data.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { totalCharacters: totalCharCount, charactersSaved: totalCharactersSaved }
|
||||
}
|
||||
|
||||
/**
|
||||
* count total percentage character savings across in-range conversation
|
||||
*/
|
||||
private calculateContextOptimizationMetrics(
|
||||
apiMessages: Anthropic.Messages.MessageParam[],
|
||||
conversationHistoryDeletedRange: [number, number] | undefined,
|
||||
uniqueFileReadIndices: Set<number>,
|
||||
): number {
|
||||
// count for first user-assistant message pair
|
||||
const firstChunkResult = this.countCharactersAndSavingsInRange(apiMessages, 0, 2, uniqueFileReadIndices)
|
||||
|
||||
// count for the remaining in-range messages
|
||||
const secondChunkResult = this.countCharactersAndSavingsInRange(
|
||||
apiMessages,
|
||||
conversationHistoryDeletedRange ? conversationHistoryDeletedRange[1] + 1 : 2,
|
||||
apiMessages.length,
|
||||
uniqueFileReadIndices,
|
||||
)
|
||||
|
||||
const totalCharacters = firstChunkResult.totalCharacters + secondChunkResult.totalCharacters
|
||||
const totalCharactersSaved = firstChunkResult.charactersSaved + secondChunkResult.charactersSaved
|
||||
|
||||
const percentCharactersSaved = totalCharacters === 0 ? 0 : totalCharactersSaved / totalCharacters
|
||||
|
||||
return percentCharactersSaved
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import { ContextManager } from "../ContextManager"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { expect } from "chai"
|
||||
|
||||
describe("ContextManager", () => {
|
||||
function createMessages(count: number): Anthropic.Messages.MessageParam[] {
|
||||
const messages: Anthropic.Messages.MessageParam[] = []
|
||||
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: "Initial task message",
|
||||
})
|
||||
|
||||
let role: "user" | "assistant" = "assistant"
|
||||
for (let i = 1; i < count; i++) {
|
||||
messages.push({
|
||||
role,
|
||||
content: `Message ${i}`,
|
||||
})
|
||||
role = role === "user" ? "assistant" : "user"
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
describe("getNextTruncationRange", () => {
|
||||
let contextManager: ContextManager
|
||||
|
||||
beforeEach(() => {
|
||||
contextManager = new ContextManager()
|
||||
})
|
||||
|
||||
it("first truncation with half keep", () => {
|
||||
const messages = createMessages(11)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
expect(result).to.deep.equal([1, 4])
|
||||
})
|
||||
|
||||
it("first truncation with quarter keep", () => {
|
||||
const messages = createMessages(11)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "quarter")
|
||||
|
||||
expect(result).to.deep.equal([1, 6])
|
||||
})
|
||||
|
||||
it("sequential truncation with half keep", () => {
|
||||
const messages = createMessages(21)
|
||||
const firstRange = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
expect(firstRange).to.deep.equal([1, 10])
|
||||
|
||||
// Pass the previous range for sequential truncation
|
||||
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "half")
|
||||
expect(secondRange).to.deep.equal([1, 14])
|
||||
})
|
||||
|
||||
it("sequential truncation with quarter keep", () => {
|
||||
const messages = createMessages(41)
|
||||
const firstRange = contextManager.getNextTruncationRange(messages, undefined, "quarter")
|
||||
|
||||
const secondRange = contextManager.getNextTruncationRange(messages, firstRange, "quarter")
|
||||
|
||||
expect(secondRange[0]).to.equal(1)
|
||||
expect(secondRange[1]).to.be.greaterThan(firstRange[1])
|
||||
})
|
||||
|
||||
it("ensures the last message in range is a user message", () => {
|
||||
const messages = createMessages(14)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
// Check if the message at the end of range is a user message
|
||||
const lastRemovedMessage = messages[result[1]]
|
||||
expect(lastRemovedMessage.role).to.equal("user")
|
||||
|
||||
// Check if the next message after the range is an assistant message
|
||||
const nextMessage = messages[result[1] + 1]
|
||||
expect(nextMessage.role).to.equal("assistant")
|
||||
})
|
||||
|
||||
it("handles small message arrays", () => {
|
||||
const messages = createMessages(3)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
expect(result).to.deep.equal([1, 0])
|
||||
})
|
||||
|
||||
it("preserves the message structure when truncating", () => {
|
||||
const messages = createMessages(20)
|
||||
const result = contextManager.getNextTruncationRange(messages, undefined, "half")
|
||||
|
||||
// Get messages after removing the range
|
||||
const effectiveMessages = [...messages.slice(0, result[0]), ...messages.slice(result[1] + 1)]
|
||||
|
||||
// Check first message and alternating pattern
|
||||
expect(effectiveMessages[0].role).to.equal("user")
|
||||
for (let i = 1; i < effectiveMessages.length; i++) {
|
||||
const expectedRole = i % 2 === 1 ? "assistant" : "user"
|
||||
expect(effectiveMessages[i].role).to.equal(expectedRole)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTruncatedMessages", () => {
|
||||
let contextManager: ContextManager
|
||||
|
||||
beforeEach(() => {
|
||||
contextManager = new ContextManager()
|
||||
})
|
||||
|
||||
it("returns original messages when no range is provided", () => {
|
||||
const messages = createMessages(3)
|
||||
|
||||
const result = contextManager.getTruncatedMessages(messages, undefined)
|
||||
expect(result).to.deep.equal(messages)
|
||||
})
|
||||
|
||||
it("correctly removes messages in the specified range", () => {
|
||||
const messages = createMessages(5)
|
||||
|
||||
const range: [number, number] = [1, 3]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
expect(result).to.have.lengthOf(2)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
expect(result[1]).to.deep.equal(messages[4])
|
||||
})
|
||||
|
||||
it("works with a range that starts at the first message after task", () => {
|
||||
const messages = createMessages(4)
|
||||
|
||||
const range: [number, number] = [1, 2]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
expect(result).to.have.lengthOf(2)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
expect(result[1]).to.deep.equal(messages[3])
|
||||
})
|
||||
|
||||
it("correctly handles removing a range while preserving alternation pattern", () => {
|
||||
const messages = createMessages(5)
|
||||
|
||||
const range: [number, number] = [1, 2]
|
||||
const result = contextManager.getTruncatedMessages(messages, range)
|
||||
|
||||
expect(result).to.have.lengthOf(3)
|
||||
expect(result[0]).to.deep.equal(messages[0])
|
||||
expect(result[1]).to.deep.equal(messages[3])
|
||||
expect(result[2]).to.deep.equal(messages[4])
|
||||
|
||||
expect(result[0].role).to.equal("user")
|
||||
expect(result[1].role).to.equal("assistant")
|
||||
expect(result[2].role).to.equal("user")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,18 +0,0 @@
|
||||
export function checkIsOpenRouterContextWindowError(error: any): boolean {
|
||||
try {
|
||||
return error.code === 400 && error.message?.includes("context length")
|
||||
} catch (e: unknown) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function checkIsAnthropicContextWindowError(response: any): boolean {
|
||||
try {
|
||||
return (
|
||||
response?.error?.error?.type === "invalid_request_error" &&
|
||||
response?.error?.error?.message?.includes("prompt is too long")
|
||||
)
|
||||
} catch (e: unknown) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { ApiHandler } from "../../../api"
|
||||
import { OpenAiHandler } from "../../../api/providers/openai"
|
||||
|
||||
/**
|
||||
* Gets context window information for the given API handler
|
||||
*
|
||||
* @param api The API handler to get context window information for
|
||||
* @returns An object containing the raw context window size and the effective max allowed size
|
||||
*/
|
||||
export function getContextWindowInfo(api: ApiHandler) {
|
||||
let contextWindow = api.getModel().info.contextWindow || 128_000
|
||||
// FIXME: hack to get anyone using openai compatible with deepseek to have the proper context window instead of the default 128k. We need a way for the user to specify the context window for models they input through openai compatible
|
||||
|
||||
// Handle special cases like DeepSeek
|
||||
if (api instanceof OpenAiHandler && api.getModel().id.toLowerCase().includes("deepseek")) {
|
||||
contextWindow = 64_000
|
||||
}
|
||||
|
||||
let maxAllowedSize: number
|
||||
switch (contextWindow) {
|
||||
case 64_000: // deepseek models
|
||||
maxAllowedSize = contextWindow - 27_000
|
||||
break
|
||||
case 128_000: // most models
|
||||
maxAllowedSize = contextWindow - 30_000
|
||||
break
|
||||
case 200_000: // claude models
|
||||
maxAllowedSize = contextWindow - 40_000
|
||||
break
|
||||
default:
|
||||
maxAllowedSize = Math.max(contextWindow - 40_000, contextWindow * 0.8) // for deepseek, 80% of 64k meant only ~10k buffer which was too small and resulted in users getting context window errors.
|
||||
}
|
||||
|
||||
return { contextWindow, maxAllowedSize }
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
|
||||
// Type definitions for FileContextTracker
|
||||
export interface FileMetadataEntry {
|
||||
path: string
|
||||
record_state: "active" | "stale"
|
||||
record_source: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned"
|
||||
cline_read_date: number | null
|
||||
cline_edit_date: number | null
|
||||
user_edit_date?: number | null
|
||||
}
|
||||
|
||||
export interface ModelMetadataEntry {
|
||||
ts: number
|
||||
model_id: string
|
||||
model_provider_id: string
|
||||
mode: string
|
||||
}
|
||||
|
||||
export interface TaskMetadata {
|
||||
files_in_context: FileMetadataEntry[]
|
||||
model_usage: ModelMetadataEntry[]
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import { expect } from "chai"
|
||||
import * as sinon from "sinon"
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { FileContextTracker } from "./FileContextTracker"
|
||||
import * as diskModule from "../../storage/disk"
|
||||
import type { TaskMetadata, FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
|
||||
describe("FileContextTracker", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let mockContext: vscode.ExtensionContext
|
||||
let mockWorkspace: sinon.SinonStub
|
||||
let mockFileSystemWatcher: any
|
||||
let tracker: FileContextTracker
|
||||
let taskId: string
|
||||
let mockTaskMetadata: TaskMetadata
|
||||
let getTaskMetadataStub: sinon.SinonStub
|
||||
let saveTaskMetadataStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock vscode workspace
|
||||
mockWorkspace = sandbox.stub(vscode.workspace, "workspaceFolders").value([
|
||||
{
|
||||
uri: {
|
||||
fsPath: "/mock/workspace",
|
||||
},
|
||||
} as vscode.WorkspaceFolder,
|
||||
])
|
||||
|
||||
// Mock file system watcher
|
||||
mockFileSystemWatcher = {
|
||||
dispose: sandbox.stub(),
|
||||
onDidChange: sandbox.stub().returns({ dispose: () => {} }),
|
||||
}
|
||||
|
||||
// Use a function replacement instead of a direct stub
|
||||
const originalCreateFileSystemWatcher = vscode.workspace.createFileSystemWatcher
|
||||
vscode.workspace.createFileSystemWatcher = function () {
|
||||
return mockFileSystemWatcher
|
||||
}
|
||||
|
||||
// Mock controller and context
|
||||
mockContext = {
|
||||
globalStorageUri: { fsPath: "/mock/storage" },
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
// Mock disk module functions
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
|
||||
// Create tracker instance
|
||||
taskId = "test-task-id"
|
||||
tracker = new FileContextTracker(mockContext, taskId)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it("should add a record when a file is read by a tool", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
await tracker.trackFileContext(filePath, "read_tool")
|
||||
|
||||
// Verify getTaskMetadata was called
|
||||
expect(getTaskMetadataStub.calledOnce).to.be.true
|
||||
expect(getTaskMetadataStub.firstCall.args[1]).to.equal(taskId)
|
||||
|
||||
// Verify saveTaskMetadata was called with the correct data
|
||||
expect(saveTaskMetadataStub.calledOnce).to.be.true
|
||||
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
expect(savedMetadata.files_in_context.length).to.equal(1)
|
||||
|
||||
const fileEntry = savedMetadata.files_in_context[0]
|
||||
expect(fileEntry.path).to.equal(filePath)
|
||||
expect(fileEntry.record_state).to.equal("active")
|
||||
expect(fileEntry.record_source).to.equal("read_tool")
|
||||
expect(fileEntry.cline_read_date).to.be.a("number")
|
||||
expect(fileEntry.cline_edit_date).to.be.null
|
||||
})
|
||||
|
||||
it("should add a record when a file is edited by Cline", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
await tracker.trackFileContext(filePath, "cline_edited")
|
||||
|
||||
// Verify saveTaskMetadata was called with the correct data
|
||||
expect(saveTaskMetadataStub.calledOnce).to.be.true
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
|
||||
// Check that we have at least one entry in files_in_context
|
||||
expect(savedMetadata.files_in_context).to.be.an("array").that.is.not.empty
|
||||
|
||||
// Find the active entry for this file
|
||||
const activeEntry = savedMetadata.files_in_context.find(
|
||||
(entry: FileMetadataEntry) => entry.path === filePath && entry.record_state === "active",
|
||||
)
|
||||
|
||||
// Assert that we found an active entry
|
||||
expect(activeEntry).to.exist
|
||||
|
||||
// Now check the properties of the active entry
|
||||
expect(activeEntry.path).to.equal(filePath)
|
||||
expect(activeEntry.record_state).to.equal("active")
|
||||
expect(activeEntry.record_source).to.equal("cline_edited")
|
||||
expect(activeEntry.cline_read_date).to.be.a("number")
|
||||
expect(activeEntry.cline_edit_date).to.be.a("number")
|
||||
})
|
||||
|
||||
it("should add a record when a file is mentioned", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
await tracker.trackFileContext(filePath, "file_mentioned")
|
||||
|
||||
// Verify saveTaskMetadata was called with the correct data
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
const fileEntry = savedMetadata.files_in_context[0]
|
||||
|
||||
expect(fileEntry.path).to.equal(filePath)
|
||||
expect(fileEntry.record_state).to.equal("active")
|
||||
expect(fileEntry.record_source).to.equal("file_mentioned")
|
||||
expect(fileEntry.cline_read_date).to.be.a("number")
|
||||
expect(fileEntry.cline_edit_date).to.be.null
|
||||
})
|
||||
|
||||
it("should add a record when a file is edited by the user", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
await tracker.trackFileContext(filePath, "user_edited")
|
||||
|
||||
// Verify saveTaskMetadata was called with the correct data
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
const fileEntry = savedMetadata.files_in_context[0]
|
||||
|
||||
expect(fileEntry.path).to.equal(filePath)
|
||||
expect(fileEntry.record_state).to.equal("active")
|
||||
expect(fileEntry.record_source).to.equal("user_edited")
|
||||
expect(fileEntry.user_edit_date).to.be.a("number")
|
||||
|
||||
// Verify the file was added to recentlyModifiedFiles
|
||||
const modifiedFiles = tracker.getAndClearRecentlyModifiedFiles()
|
||||
expect(modifiedFiles).to.include(filePath)
|
||||
})
|
||||
|
||||
it("should mark existing entries as stale when adding a new entry for the same file", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
// Add an initial entry
|
||||
mockTaskMetadata.files_in_context = [
|
||||
{
|
||||
path: filePath,
|
||||
record_state: "active",
|
||||
record_source: "read_tool",
|
||||
cline_read_date: Date.now() - 1000, // 1 second ago
|
||||
cline_edit_date: null,
|
||||
user_edit_date: null,
|
||||
},
|
||||
]
|
||||
|
||||
// Track a new operation on the same file
|
||||
await tracker.trackFileContext(filePath, "cline_edited")
|
||||
|
||||
// Verify the metadata now has two entries - one stale and one active
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
expect(savedMetadata.files_in_context.length).to.equal(2)
|
||||
|
||||
// First entry should be marked as stale
|
||||
expect(savedMetadata.files_in_context[0].record_state).to.equal("stale")
|
||||
|
||||
// New entry should be active
|
||||
const newEntry = savedMetadata.files_in_context[1]
|
||||
expect(newEntry.record_state).to.equal("active")
|
||||
expect(newEntry.record_source).to.equal("cline_edited")
|
||||
})
|
||||
|
||||
it("should setup a file watcher for tracked files", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
// Create a spy to track if createFileSystemWatcher was called
|
||||
const createWatcherSpy = sinon.spy(vscode.workspace, "createFileSystemWatcher")
|
||||
|
||||
await tracker.trackFileContext(filePath, "read_tool")
|
||||
|
||||
// Verify createFileSystemWatcher was called
|
||||
expect(createWatcherSpy.called).to.be.true
|
||||
createWatcherSpy.restore()
|
||||
|
||||
// Verify onDidChange was called to set up the change listener
|
||||
expect(mockFileSystemWatcher.onDidChange.called).to.be.true
|
||||
})
|
||||
|
||||
it("should track user edits when file watcher detects changes", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
// First track the file to set up the watcher
|
||||
await tracker.trackFileContext(filePath, "read_tool")
|
||||
|
||||
// Reset the stubs to check the next calls
|
||||
getTaskMetadataStub.resetHistory()
|
||||
saveTaskMetadataStub.resetHistory()
|
||||
|
||||
// Create a spy on trackFileContext to verify it's called with the right parameters
|
||||
const trackFileContextSpy = sandbox.spy(tracker, "trackFileContext")
|
||||
|
||||
// Get the callback that was registered with onDidChange
|
||||
const callback = mockFileSystemWatcher.onDidChange.firstCall.args[0]
|
||||
|
||||
// Directly call the callback to simulate a file change event
|
||||
callback(vscode.Uri.file(path.resolve("/mock/workspace", filePath)))
|
||||
|
||||
// Verify trackFileContext was called with the right parameters
|
||||
expect(trackFileContextSpy.calledWith(filePath, "user_edited")).to.be.true
|
||||
|
||||
// Verify the file was added to recentlyModifiedFiles
|
||||
const modifiedFiles = tracker.getAndClearRecentlyModifiedFiles()
|
||||
expect(modifiedFiles).to.include(filePath)
|
||||
})
|
||||
|
||||
it("should not track Cline edits as user edits", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
// First track the file to set up the watcher
|
||||
await tracker.trackFileContext(filePath, "read_tool")
|
||||
|
||||
// Mark the file as edited by Cline
|
||||
tracker.markFileAsEditedByCline(filePath)
|
||||
|
||||
// Reset the stubs to check the next calls
|
||||
getTaskMetadataStub.resetHistory()
|
||||
saveTaskMetadataStub.resetHistory()
|
||||
|
||||
// Create a spy on trackFileContext to verify it's not called
|
||||
const trackFileContextSpy = sandbox.spy(tracker, "trackFileContext")
|
||||
|
||||
// Get the callback that was registered with onDidChange
|
||||
const callback = mockFileSystemWatcher.onDidChange.firstCall.args[0]
|
||||
|
||||
// Directly call the callback to simulate a file change event
|
||||
callback(vscode.Uri.file(path.resolve("/mock/workspace", filePath)))
|
||||
|
||||
// Verify trackFileContext was not called with user_edited
|
||||
expect(trackFileContextSpy.calledWith(filePath, "user_edited")).to.be.false
|
||||
|
||||
// Verify the file was not added to recentlyModifiedFiles
|
||||
const modifiedFiles = tracker.getAndClearRecentlyModifiedFiles()
|
||||
expect(modifiedFiles).to.not.include(filePath)
|
||||
})
|
||||
|
||||
it("should dispose file watchers when dispose is called", async () => {
|
||||
const filePath = "src/test-file.ts"
|
||||
|
||||
// Track a file to set up the watcher
|
||||
await tracker.trackFileContext(filePath, "read_tool")
|
||||
|
||||
// Call dispose
|
||||
tracker.dispose()
|
||||
|
||||
// Verify the watcher was disposed
|
||||
expect(mockFileSystemWatcher.dispose.called).to.be.true
|
||||
})
|
||||
})
|
||||
@@ -1,176 +0,0 @@
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { getTaskMetadata, saveTaskMetadata } from "../../storage/disk"
|
||||
import type { FileMetadataEntry } from "./ContextTrackerTypes"
|
||||
|
||||
// This class is responsible for tracking file operations that may result in stale context.
|
||||
// If a user modifies a file outside of Cline, the context may become stale and need to be updated.
|
||||
// We do not want Cline to reload the context every time a file is modified, so we use this class merely
|
||||
// to inform Cline that the change has occurred, and tell Cline to reload the file before making
|
||||
// any changes to it. This fixes an issue with diff editing, where Cline was unable to complete a diff edit.
|
||||
// a diff edit because the file was modified since Cline last read it.
|
||||
|
||||
// FileContextTracker
|
||||
//
|
||||
// This class is responsible for tracking file operations.
|
||||
// If the full contents of a file are pass to Cline via a tool, mention, or edit, the file is marked as active.
|
||||
// If a file is modified outside of Cline, we detect and track this change to prevent stale context.
|
||||
export class FileContextTracker {
|
||||
private context: vscode.ExtensionContext
|
||||
readonly taskId: string
|
||||
|
||||
// File tracking and watching
|
||||
private fileWatchers = new Map<string, vscode.FileSystemWatcher>()
|
||||
private recentlyModifiedFiles = new Set<string>()
|
||||
private recentlyEditedByCline = new Set<string>()
|
||||
|
||||
constructor(context: vscode.ExtensionContext, taskId: string) {
|
||||
this.context = context
|
||||
this.taskId = taskId
|
||||
}
|
||||
|
||||
// Gets the current working directory or returns undefined if it cannot be determined
|
||||
private getCwd(): string | undefined {
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (!cwd) {
|
||||
console.info("No workspace folder available - cannot determine current working directory")
|
||||
}
|
||||
return cwd
|
||||
}
|
||||
|
||||
// File watchers are set up for each file that is tracked in the task metadata.
|
||||
async setupFileWatcher(filePath: string) {
|
||||
// Only setup watcher if it doesn't already exist for this file
|
||||
if (this.fileWatchers.has(filePath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const cwd = this.getCwd()
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file system watcher for this specific file
|
||||
const fileUri = vscode.Uri.file(path.resolve(cwd, filePath))
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(
|
||||
new vscode.RelativePattern(path.dirname(fileUri.fsPath), path.basename(fileUri.fsPath)),
|
||||
)
|
||||
|
||||
// Track file changes
|
||||
watcher.onDidChange(() => {
|
||||
if (this.recentlyEditedByCline.has(filePath)) {
|
||||
this.recentlyEditedByCline.delete(filePath) // This was an edit by Cline, no need to inform Cline
|
||||
} else {
|
||||
this.recentlyModifiedFiles.add(filePath) // This was a user edit, we will inform Cline
|
||||
this.trackFileContext(filePath, "user_edited") // Update the task metadata with file tracking
|
||||
}
|
||||
})
|
||||
|
||||
// Store the watcher so we can dispose it later
|
||||
this.fileWatchers.set(filePath, watcher)
|
||||
}
|
||||
|
||||
// Tracks a file operation in metadata and sets up a watcher for the file
|
||||
// This is the main entry point for FileContextTracker and is called when a file is passed to Cline via a tool, mention, or edit.
|
||||
async trackFileContext(filePath: string, operation: "read_tool" | "user_edited" | "cline_edited" | "file_mentioned") {
|
||||
try {
|
||||
const cwd = this.getCwd()
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
|
||||
// Add file to metadata
|
||||
await this.addFileToFileContextTracker(this.context, this.taskId, filePath, operation)
|
||||
|
||||
// Set up file watcher for this file
|
||||
await this.setupFileWatcher(filePath)
|
||||
} catch (error) {
|
||||
console.error("Failed to track file operation:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a file to the metadata tracker
|
||||
// This handles the business logic of determining if the file is new, stale, or active.
|
||||
// It also updates the metadata with the latest read/edit dates.
|
||||
async addFileToFileContextTracker(
|
||||
context: vscode.ExtensionContext,
|
||||
taskId: string,
|
||||
filePath: string,
|
||||
source: FileMetadataEntry["record_source"],
|
||||
) {
|
||||
try {
|
||||
const metadata = await getTaskMetadata(context, taskId)
|
||||
const now = Date.now()
|
||||
|
||||
// Mark existing entries for this file as stale
|
||||
metadata.files_in_context.forEach((entry) => {
|
||||
if (entry.path === filePath && entry.record_state === "active") {
|
||||
entry.record_state = "stale"
|
||||
}
|
||||
})
|
||||
|
||||
// Helper to get the latest date for a specific field and file
|
||||
const getLatestDateForField = (path: string, field: keyof FileMetadataEntry): number | null => {
|
||||
const relevantEntries = metadata.files_in_context
|
||||
.filter((entry) => entry.path === path && entry[field])
|
||||
.sort((a, b) => (b[field] as number) - (a[field] as number))
|
||||
|
||||
return relevantEntries.length > 0 ? (relevantEntries[0][field] as number) : null
|
||||
}
|
||||
|
||||
let newEntry: FileMetadataEntry = {
|
||||
path: filePath,
|
||||
record_state: "active",
|
||||
record_source: source,
|
||||
cline_read_date: getLatestDateForField(filePath, "cline_read_date"),
|
||||
cline_edit_date: getLatestDateForField(filePath, "cline_edit_date"),
|
||||
user_edit_date: getLatestDateForField(filePath, "user_edit_date"),
|
||||
}
|
||||
|
||||
switch (source) {
|
||||
// user_edited: The user has edited the file
|
||||
case "user_edited":
|
||||
newEntry.user_edit_date = now
|
||||
this.recentlyModifiedFiles.add(filePath)
|
||||
break
|
||||
|
||||
// cline_edited: Cline has edited the file
|
||||
case "cline_edited":
|
||||
newEntry.cline_read_date = now
|
||||
newEntry.cline_edit_date = now
|
||||
break
|
||||
|
||||
// read_tool/file_mentioned: Cline has read the file via a tool or file mention
|
||||
case "read_tool":
|
||||
case "file_mentioned":
|
||||
newEntry.cline_read_date = now
|
||||
break
|
||||
}
|
||||
|
||||
metadata.files_in_context.push(newEntry)
|
||||
await saveTaskMetadata(context, taskId, metadata)
|
||||
} catch (error) {
|
||||
console.error("Failed to add file to metadata:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns (and then clears) the set of recently modified files
|
||||
getAndClearRecentlyModifiedFiles(): string[] {
|
||||
const files = Array.from(this.recentlyModifiedFiles)
|
||||
this.recentlyModifiedFiles.clear()
|
||||
return files
|
||||
}
|
||||
|
||||
// Marks a file as edited by Cline to prevent false positives in file watchers
|
||||
markFileAsEditedByCline(filePath: string): void {
|
||||
this.recentlyEditedByCline.add(filePath)
|
||||
}
|
||||
|
||||
// Disposes all file watchers
|
||||
dispose(): void {
|
||||
for (const watcher of this.fileWatchers.values()) {
|
||||
watcher.dispose()
|
||||
}
|
||||
this.fileWatchers.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
import { describe, it, beforeEach, afterEach } from "mocha"
|
||||
import { expect } from "chai"
|
||||
import * as sinon from "sinon"
|
||||
import * as vscode from "vscode"
|
||||
import { ModelContextTracker } from "./ModelContextTracker"
|
||||
import * as diskModule from "../../storage/disk"
|
||||
import type { TaskMetadata } from "./ContextTrackerTypes"
|
||||
|
||||
describe("ModelContextTracker", () => {
|
||||
let sandbox: sinon.SinonSandbox
|
||||
let mockContext: vscode.ExtensionContext
|
||||
let tracker: ModelContextTracker
|
||||
let taskId: string
|
||||
let mockTaskMetadata: TaskMetadata
|
||||
let getTaskMetadataStub: sinon.SinonStub
|
||||
let saveTaskMetadataStub: sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Mock controller and context
|
||||
mockContext = {
|
||||
globalStorageUri: { fsPath: "/mock/storage" },
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
// Mock disk module functions
|
||||
mockTaskMetadata = { files_in_context: [], model_usage: [] }
|
||||
getTaskMetadataStub = sandbox.stub(diskModule, "getTaskMetadata").resolves(mockTaskMetadata)
|
||||
saveTaskMetadataStub = sandbox.stub(diskModule, "saveTaskMetadata").resolves()
|
||||
|
||||
// Create tracker instance
|
||||
taskId = "test-task-id"
|
||||
tracker = new ModelContextTracker(mockContext, taskId)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it("should record model usage with correct data", async () => {
|
||||
// Test data
|
||||
const apiProviderId = "anthropic"
|
||||
const modelId = "claude-3-opus"
|
||||
const mode = "act"
|
||||
|
||||
// Use a fake timer to have a predictable timestamp
|
||||
const fakeNow = 1617293940000 // Some fixed timestamp
|
||||
const clock = sandbox.useFakeTimers(fakeNow)
|
||||
|
||||
try {
|
||||
// Call the method being tested
|
||||
await tracker.recordModelUsage(apiProviderId, modelId, mode)
|
||||
|
||||
// Verify getTaskMetadata was called with correct parameters
|
||||
expect(getTaskMetadataStub.calledOnce).to.be.true
|
||||
expect(getTaskMetadataStub.firstCall.args[1]).to.equal(taskId)
|
||||
|
||||
// Verify saveTaskMetadata was called with the correct data
|
||||
expect(saveTaskMetadataStub.calledOnce).to.be.true
|
||||
|
||||
// Extract the saved metadata from the call arguments
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
|
||||
// Verify model_usage array has one entry
|
||||
expect(savedMetadata.model_usage.length).to.equal(1)
|
||||
|
||||
// Verify the entry has the correct properties
|
||||
const modelUsageEntry = savedMetadata.model_usage[0]
|
||||
expect(modelUsageEntry.ts).to.equal(fakeNow)
|
||||
expect(modelUsageEntry.model_id).to.equal(modelId)
|
||||
expect(modelUsageEntry.model_provider_id).to.equal(apiProviderId)
|
||||
expect(modelUsageEntry.mode).to.equal(mode)
|
||||
} finally {
|
||||
// Restore the clock
|
||||
clock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
it("should throw an error when controller is dereferenced", async () => {
|
||||
// Create a new tracker with a controller that will be garbage collected
|
||||
const weakTracker = new ModelContextTracker(mockContext, taskId)
|
||||
|
||||
// Force the WeakRef to return null by overriding the deref method
|
||||
const weakRef = { deref: sandbox.stub().returns(null) }
|
||||
sandbox.stub(WeakRef.prototype, "deref").callsFake(() => weakRef.deref())
|
||||
|
||||
try {
|
||||
// Try to call the method - this should throw
|
||||
await weakTracker.recordModelUsage("any-provider", "any-model", "any-mode")
|
||||
|
||||
// If we get here, the test should fail
|
||||
expect.fail("Expected an error to be thrown")
|
||||
} catch (error) {
|
||||
// Verify the error message
|
||||
expect(error.message).to.equal("Unable to access extension context")
|
||||
}
|
||||
})
|
||||
|
||||
it("should append model usage to existing entries", async () => {
|
||||
// Add an existing model usage entry
|
||||
const existingTimestamp = 1617200000000
|
||||
mockTaskMetadata.model_usage = [
|
||||
{
|
||||
ts: existingTimestamp,
|
||||
model_id: "existing-model",
|
||||
model_provider_id: "existing-provider",
|
||||
mode: "plan",
|
||||
},
|
||||
]
|
||||
|
||||
// Test data for new entry
|
||||
const apiProviderId = "anthropic"
|
||||
const modelId = "claude-3-sonnet"
|
||||
const mode = "act"
|
||||
|
||||
// Use a fake timer
|
||||
const newTimestamp = 1617300000000
|
||||
const clock = sandbox.useFakeTimers(newTimestamp)
|
||||
|
||||
try {
|
||||
// Call the method being tested
|
||||
await tracker.recordModelUsage(apiProviderId, modelId, mode)
|
||||
|
||||
// Verify saveTaskMetadata was called
|
||||
expect(saveTaskMetadataStub.calledOnce).to.be.true
|
||||
|
||||
// Extract the saved metadata
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
|
||||
// Verify model_usage array now has two entries
|
||||
expect(savedMetadata.model_usage.length).to.equal(2)
|
||||
|
||||
// Verify the existing entry is preserved
|
||||
expect(savedMetadata.model_usage[0]).to.deep.equal({
|
||||
ts: existingTimestamp,
|
||||
model_id: "existing-model",
|
||||
model_provider_id: "existing-provider",
|
||||
mode: "plan",
|
||||
})
|
||||
|
||||
// Verify the new entry has correct data
|
||||
expect(savedMetadata.model_usage[1]).to.deep.equal({
|
||||
ts: newTimestamp,
|
||||
model_id: modelId,
|
||||
model_provider_id: apiProviderId,
|
||||
mode: mode,
|
||||
})
|
||||
} finally {
|
||||
clock.restore()
|
||||
}
|
||||
})
|
||||
|
||||
it("should handle multiple model usages in sequence", async () => {
|
||||
// Test data for sequential calls
|
||||
const usages = [
|
||||
{ provider: "anthropic", model: "claude-3-opus", mode: "plan" },
|
||||
{ provider: "openai", model: "gpt-4", mode: "act" },
|
||||
{ provider: "anthropic", model: "claude-3-haiku", mode: "plan" },
|
||||
]
|
||||
|
||||
// Use a fake timer that advances with each call
|
||||
const startTime = 1617300000000
|
||||
const clock = sandbox.useFakeTimers(startTime)
|
||||
|
||||
try {
|
||||
// Record multiple model usages
|
||||
for (let i = 0; i < usages.length; i++) {
|
||||
const { provider, model, mode } = usages[i]
|
||||
|
||||
// Advance time by 1 second for each call
|
||||
clock.tick(1000)
|
||||
const expectedTime = startTime + (i + 1) * 1000
|
||||
|
||||
// Reset history between calls to check individual call behavior
|
||||
getTaskMetadataStub.resetHistory()
|
||||
saveTaskMetadataStub.resetHistory()
|
||||
|
||||
// Reset mock metadata for each iteration to avoid accumulation
|
||||
mockTaskMetadata.model_usage = []
|
||||
|
||||
// Call the method
|
||||
await tracker.recordModelUsage(provider, model, mode)
|
||||
|
||||
// Verify interaction with disk module
|
||||
expect(getTaskMetadataStub.calledOnce).to.be.true
|
||||
expect(saveTaskMetadataStub.calledOnce).to.be.true
|
||||
|
||||
// Get the saved metadata
|
||||
const savedMetadata = saveTaskMetadataStub.firstCall.args[2]
|
||||
|
||||
// Since we reset the array for each call, we should always have 1 entry
|
||||
expect(savedMetadata.model_usage.length).to.equal(1)
|
||||
|
||||
// Check the entry
|
||||
const entry = savedMetadata.model_usage[0]
|
||||
expect(entry.ts).to.equal(expectedTime)
|
||||
expect(entry.model_id).to.equal(model)
|
||||
expect(entry.model_provider_id).to.equal(provider)
|
||||
expect(entry.mode).to.equal(mode)
|
||||
}
|
||||
} finally {
|
||||
clock.restore()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,40 +0,0 @@
|
||||
import * as vscode from "vscode"
|
||||
import { getTaskMetadata, saveTaskMetadata } from "../../storage/disk"
|
||||
|
||||
export class ModelContextTracker {
|
||||
readonly taskId: string
|
||||
private context: vscode.ExtensionContext
|
||||
|
||||
constructor(context: vscode.ExtensionContext, taskId: string) {
|
||||
this.context = context
|
||||
this.taskId = taskId
|
||||
}
|
||||
|
||||
async recordModelUsage(apiProviderId: string, modelId: string, mode: string) {
|
||||
const metadata = await getTaskMetadata(this.context, this.taskId)
|
||||
|
||||
if (!metadata.model_usage) {
|
||||
metadata.model_usage = []
|
||||
}
|
||||
|
||||
// check to see if the last entry is the same as the new one
|
||||
const lastEntry = metadata.model_usage[metadata.model_usage.length - 1]
|
||||
if (
|
||||
lastEntry &&
|
||||
lastEntry.model_id === modelId &&
|
||||
lastEntry.model_provider_id === apiProviderId &&
|
||||
lastEntry.mode === mode
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
metadata.model_usage.push({
|
||||
ts: Date.now(),
|
||||
model_id: modelId,
|
||||
model_provider_id: apiProviderId,
|
||||
mode: mode,
|
||||
})
|
||||
|
||||
await saveTaskMetadata(this.context, this.taskId, metadata)
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import path from "path"
|
||||
import { GlobalFileNames } from "../../../storage/disk"
|
||||
import { fileExistsAtPath, isDirectory, readDirectory } from "../../../../utils/fs"
|
||||
import { formatResponse } from "../../../prompts/responses"
|
||||
import fs from "fs/promises"
|
||||
|
||||
export const getGlobalClineRules = async (globalClineRulesFilePath: string) => {
|
||||
if (await fileExistsAtPath(globalClineRulesFilePath)) {
|
||||
if (await isDirectory(globalClineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(globalClineRulesFilePath)
|
||||
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, globalClineRulesFilePath)
|
||||
const clineRulesFileInstructions = formatResponse.clineRulesGlobalDirectoryInstructions(rulesFilesTotalContent)
|
||||
return clineRulesFileInstructions
|
||||
} catch {
|
||||
console.error(`Failed to read .clinerules directory at ${globalClineRulesFilePath}`)
|
||||
}
|
||||
} else {
|
||||
console.error(`${globalClineRulesFilePath} is not a directory`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const getLocalClineRules = async (cwd: string) => {
|
||||
const clineRulesFilePath = path.resolve(cwd, GlobalFileNames.clineRules)
|
||||
|
||||
let clineRulesFileInstructions: string | undefined
|
||||
|
||||
if (await fileExistsAtPath(clineRulesFilePath)) {
|
||||
if (await isDirectory(clineRulesFilePath)) {
|
||||
try {
|
||||
const rulesFilePaths = await readDirectory(path.join(cwd, GlobalFileNames.clineRules))
|
||||
const rulesFilesTotalContent = await getClineRulesFilesTotalContent(rulesFilePaths, cwd)
|
||||
clineRulesFileInstructions = formatResponse.clineRulesLocalDirectoryInstructions(cwd, rulesFilesTotalContent)
|
||||
} catch {
|
||||
console.error(`Failed to read .clinerules directory at ${clineRulesFilePath}`)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const ruleFileContent = (await fs.readFile(clineRulesFilePath, "utf8")).trim()
|
||||
if (ruleFileContent) {
|
||||
clineRulesFileInstructions = formatResponse.clineRulesLocalFileInstructions(cwd, ruleFileContent)
|
||||
}
|
||||
} catch {
|
||||
console.error(`Failed to read .clinerules file at ${clineRulesFilePath}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clineRulesFileInstructions
|
||||
}
|
||||
|
||||
const getClineRulesFilesTotalContent = async (rulesFilePaths: string[], basePath: string) => {
|
||||
const ruleFilesTotalContent = await Promise.all(
|
||||
rulesFilePaths.map(async (filePath) => {
|
||||
const ruleFilePath = path.resolve(basePath, filePath)
|
||||
const ruleFilePathRelative = path.relative(basePath, ruleFilePath)
|
||||
return `${ruleFilePathRelative}\n` + (await fs.readFile(ruleFilePath, "utf8")).trim()
|
||||
}),
|
||||
).then((contents) => contents.join("\n\n"))
|
||||
return ruleFilesTotalContent
|
||||
}
|
||||
+133
-411
@@ -1,9 +1,10 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import axios from "axios"
|
||||
import type { AxiosRequestConfig } from "axios"
|
||||
import crypto from "crypto"
|
||||
import { execa } from "execa"
|
||||
import fs from "fs/promises"
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
import os from "os"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
@@ -16,12 +17,10 @@ import { selectImages } from "../../integrations/misc/process-images"
|
||||
import { getTheme } from "../../integrations/theme/getTheme"
|
||||
import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker"
|
||||
import { ClineAccountService } from "../../services/account/ClineAccountService"
|
||||
import { discoverChromeInstances } from "../../services/browser/BrowserDiscovery"
|
||||
import { BrowserSession } from "../../services/browser/BrowserSession"
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
import { searchWorkspaceFiles } from "../../services/search/file-search"
|
||||
import { telemetryService } from "../../services/telemetry/TelemetryService"
|
||||
import { ApiProvider, ModelInfo } from "../../shared/api"
|
||||
import { findLast } from "../../shared/array"
|
||||
import { ChatContent } from "../../shared/ChatContent"
|
||||
import { ChatSettings } from "../../shared/ChatSettings"
|
||||
import { ExtensionMessage, ExtensionState, Invoke, Platform } from "../../shared/ExtensionMessage"
|
||||
@@ -31,10 +30,9 @@ import { TelemetrySetting } from "../../shared/TelemetrySetting"
|
||||
import { ClineCheckpointRestore, WebviewMessage } from "../../shared/WebviewMessage"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { searchCommits } from "../../utils/git"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { getTotalTasksSize } from "../../utils/storage"
|
||||
import { Task } from "../task"
|
||||
import { openMention } from "../mentions"
|
||||
import { ensureMcpServersDirectoryExists, ensureSettingsDirectoryExists, GlobalFileNames } from "../storage/disk"
|
||||
import {
|
||||
getAllExtensionState,
|
||||
getGlobalState,
|
||||
@@ -44,7 +42,8 @@ import {
|
||||
updateApiConfiguration,
|
||||
updateGlobalState,
|
||||
} from "../storage/state"
|
||||
import { Task } from "../task"
|
||||
import { WebviewProvider } from "../webview"
|
||||
import { GlobalFileNames } from "../storage/disk"
|
||||
|
||||
/*
|
||||
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
|
||||
@@ -53,37 +52,25 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
||||
*/
|
||||
|
||||
export class Controller {
|
||||
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
|
||||
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private task?: Task
|
||||
workspaceTracker: WorkspaceTracker
|
||||
mcpHub: McpHub
|
||||
accountService: ClineAccountService
|
||||
private latestAnnouncementId = "april-11-2025" // update to some unique identifier when we add a new announcement
|
||||
workspaceTracker?: WorkspaceTracker
|
||||
mcpHub?: McpHub
|
||||
accountService?: ClineAccountService
|
||||
private latestAnnouncementId = "march-22-2025" // update to some unique identifier when we add a new announcement
|
||||
private webviewProviderRef: WeakRef<WebviewProvider>
|
||||
|
||||
constructor(
|
||||
readonly context: vscode.ExtensionContext,
|
||||
private readonly outputChannel: vscode.OutputChannel,
|
||||
postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined,
|
||||
webviewProvider: WebviewProvider,
|
||||
) {
|
||||
this.outputChannel.appendLine("ClineProvider instantiated")
|
||||
this.postMessage = postMessage
|
||||
this.webviewProviderRef = new WeakRef(webviewProvider)
|
||||
|
||||
this.workspaceTracker = new WorkspaceTracker((msg) => this.postMessageToWebview(msg))
|
||||
this.mcpHub = new McpHub(
|
||||
() => ensureMcpServersDirectoryExists(),
|
||||
() => ensureSettingsDirectoryExists(this.context),
|
||||
(msg) => this.postMessageToWebview(msg),
|
||||
this.context.extension?.packageJSON?.version ?? "1.0.0",
|
||||
)
|
||||
this.accountService = new ClineAccountService(
|
||||
(msg) => this.postMessageToWebview(msg),
|
||||
async () => {
|
||||
const { apiConfiguration } = await this.getStateToPostToWebview()
|
||||
return apiConfiguration?.clineApiKey
|
||||
},
|
||||
)
|
||||
this.workspaceTracker = new WorkspaceTracker(this)
|
||||
this.mcpHub = new McpHub(this)
|
||||
this.accountService = new ClineAccountService(this)
|
||||
|
||||
// Clean up legacy checkpoints
|
||||
cleanupLegacyCheckpoints(this.context.globalStorageUri.fsPath, this.outputChannel).catch((error) => {
|
||||
@@ -106,8 +93,11 @@ export class Controller {
|
||||
x.dispose()
|
||||
}
|
||||
}
|
||||
this.workspaceTracker.dispose()
|
||||
this.mcpHub.dispose()
|
||||
this.workspaceTracker?.dispose()
|
||||
this.workspaceTracker = undefined
|
||||
this.mcpHub?.dispose()
|
||||
this.mcpHub = undefined
|
||||
this.accountService = undefined
|
||||
this.outputChannel.appendLine("Disposed all disposables")
|
||||
|
||||
console.error("Controller disposed")
|
||||
@@ -130,19 +120,12 @@ export class Controller {
|
||||
await updateGlobalState(this.context, "userInfo", info)
|
||||
}
|
||||
|
||||
async initTask(task?: string, images?: string[], historyItem?: HistoryItem) {
|
||||
async initClineWithTask(task?: string, images?: string[]) {
|
||||
await this.clearTask() // ensures that an existing task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
|
||||
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
|
||||
await getAllExtensionState(this.context)
|
||||
this.task = new Task(
|
||||
this.context,
|
||||
this.mcpHub,
|
||||
this.workspaceTracker,
|
||||
(historyItem) => this.updateTaskHistory(historyItem),
|
||||
() => this.postStateToWebview(),
|
||||
(message) => this.postMessageToWebview(message),
|
||||
(taskId) => this.reinitExistingTaskFromId(taskId),
|
||||
() => this.cancelTask(),
|
||||
this,
|
||||
apiConfiguration,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
@@ -150,20 +133,29 @@ export class Controller {
|
||||
customInstructions,
|
||||
task,
|
||||
images,
|
||||
)
|
||||
}
|
||||
|
||||
async initClineWithHistoryItem(historyItem: HistoryItem) {
|
||||
await this.clearTask()
|
||||
const { apiConfiguration, customInstructions, autoApprovalSettings, browserSettings, chatSettings } =
|
||||
await getAllExtensionState(this.context)
|
||||
this.task = new Task(
|
||||
this,
|
||||
apiConfiguration,
|
||||
autoApprovalSettings,
|
||||
browserSettings,
|
||||
chatSettings,
|
||||
customInstructions,
|
||||
undefined,
|
||||
undefined,
|
||||
historyItem,
|
||||
)
|
||||
}
|
||||
|
||||
async reinitExistingTaskFromId(taskId: string) {
|
||||
const history = await this.getTaskWithId(taskId)
|
||||
if (history) {
|
||||
await this.initTask(undefined, undefined, history.historyItem)
|
||||
}
|
||||
}
|
||||
|
||||
// Send any JSON serializable data to the react app
|
||||
async postMessageToWebview(message: ExtensionMessage) {
|
||||
await this.postMessage(message)
|
||||
await this.webviewProviderRef.deref()?.view?.webview.postMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,22 +169,9 @@ export class Controller {
|
||||
case "addRemoteServer": {
|
||||
try {
|
||||
await this.mcpHub?.addRemoteServer(message.serverName!, message.serverUrl!)
|
||||
await this.postMessageToWebview({
|
||||
type: "addRemoteServerResult",
|
||||
addRemoteServerResult: {
|
||||
success: true,
|
||||
serverName: message.serverName!,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
await this.postMessageToWebview({
|
||||
type: "addRemoteServerResult",
|
||||
addRemoteServerResult: {
|
||||
success: false,
|
||||
serverName: message.serverName!,
|
||||
error: error.message,
|
||||
},
|
||||
})
|
||||
// We handle the errorin McpHub.ts where the function is defined
|
||||
console.error(`Failed to add remote server ${message.serverName}:`, error)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -263,7 +242,7 @@ export class Controller {
|
||||
// Could also do this in extension .ts
|
||||
//this.postMessageToWebview({ type: "text", text: `Extension: ${Date.now()}` })
|
||||
// initializing new instance of Cline will make sure that any agentically running promises in old instance don't affect our new task. this essentially creates a fresh slate for the new task
|
||||
await this.initTask(message.text, message.images)
|
||||
await this.initClineWithTask(message.text, message.images)
|
||||
break
|
||||
case "apiConfiguration":
|
||||
if (message.apiConfiguration) {
|
||||
@@ -285,12 +264,6 @@ export class Controller {
|
||||
break
|
||||
case "browserSettings":
|
||||
if (message.browserSettings) {
|
||||
// remoteBrowserEnabled now means "enable remote browser connection"
|
||||
// commenting out since this is being done in BrowserSettingsSection updateRemoteBrowserEnabled
|
||||
// if (!message.browserSettings.remoteBrowserEnabled) {
|
||||
// // If disabling remote browser connection, clear the remoteBrowserHost
|
||||
// message.browserSettings.remoteBrowserHost = undefined
|
||||
// }
|
||||
await updateGlobalState(this.context, "browserSettings", message.browserSettings)
|
||||
if (this.task) {
|
||||
this.task.browserSettings = message.browserSettings
|
||||
@@ -299,123 +272,6 @@ export class Controller {
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
break
|
||||
case "getBrowserConnectionInfo":
|
||||
try {
|
||||
// Get the current browser session from Cline if it exists
|
||||
if (this.task?.browserSession) {
|
||||
const connectionInfo = this.task.browserSession.getConnectionInfo()
|
||||
await this.postMessageToWebview({
|
||||
type: "browserConnectionInfo",
|
||||
isConnected: connectionInfo.isConnected,
|
||||
isRemote: connectionInfo.isRemote,
|
||||
host: connectionInfo.host,
|
||||
})
|
||||
} else {
|
||||
// If no active browser session, just return the settings
|
||||
const { browserSettings } = await getAllExtensionState(this.context)
|
||||
await this.postMessageToWebview({
|
||||
type: "browserConnectionInfo",
|
||||
isConnected: false,
|
||||
isRemote: !!browserSettings.remoteBrowserEnabled,
|
||||
host: browserSettings.remoteBrowserHost,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error getting browser connection info:", error)
|
||||
await this.postMessageToWebview({
|
||||
type: "browserConnectionInfo",
|
||||
isConnected: false,
|
||||
isRemote: false,
|
||||
})
|
||||
}
|
||||
break
|
||||
case "testBrowserConnection":
|
||||
try {
|
||||
const { browserSettings } = await getAllExtensionState(this.context)
|
||||
const browserSession = new BrowserSession(this.context, browserSettings)
|
||||
// If no text is provided, try auto-discovery
|
||||
if (!message.text) {
|
||||
try {
|
||||
const discoveredHost = await discoverChromeInstances()
|
||||
if (discoveredHost) {
|
||||
// Test the connection to the discovered host
|
||||
const result = await browserSession.testConnection(discoveredHost)
|
||||
// Send the result back to the webview
|
||||
await this.postMessageToWebview({
|
||||
type: "browserConnectionResult",
|
||||
success: result.success,
|
||||
text: `Auto-discovered and tested connection to Chrome at ${discoveredHost}: ${result.message}`,
|
||||
endpoint: result.endpoint,
|
||||
})
|
||||
} else {
|
||||
await this.postMessageToWebview({
|
||||
type: "browserConnectionResult",
|
||||
success: false,
|
||||
text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
await this.postMessageToWebview({
|
||||
type: "browserConnectionResult",
|
||||
success: false,
|
||||
text: `Error during auto-discovery: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Test the provided URL
|
||||
const result = await browserSession.testConnection(message.text)
|
||||
|
||||
// Send the result back to the webview
|
||||
await this.postMessageToWebview({
|
||||
type: "browserConnectionResult",
|
||||
success: result.success,
|
||||
text: result.message,
|
||||
endpoint: result.endpoint,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
await this.postMessageToWebview({
|
||||
type: "browserConnectionResult",
|
||||
success: false,
|
||||
text: `Error testing connection: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
}
|
||||
break
|
||||
case "discoverBrowser":
|
||||
try {
|
||||
const discoveredHost = await discoverChromeInstances()
|
||||
|
||||
if (discoveredHost) {
|
||||
// Don't update the remoteBrowserHost state when auto-discovering
|
||||
// This way we don't override the user's preference
|
||||
|
||||
// Test the connection to get the endpoint
|
||||
const { browserSettings } = await getAllExtensionState(this.context)
|
||||
const browserSession = new BrowserSession(this.context, browserSettings)
|
||||
const result = await browserSession.testConnection(discoveredHost)
|
||||
|
||||
// Send the result back to the webview
|
||||
await this.postMessageToWebview({
|
||||
type: "browserConnectionResult",
|
||||
success: true,
|
||||
text: `Successfully discovered and connected to Chrome at ${discoveredHost}`,
|
||||
endpoint: result.endpoint,
|
||||
})
|
||||
} else {
|
||||
await this.postMessageToWebview({
|
||||
type: "browserConnectionResult",
|
||||
success: false,
|
||||
text: "No Chrome instances found on the network. Make sure Chrome is running with remote debugging enabled (--remote-debugging-port=9222).",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
await this.postMessageToWebview({
|
||||
type: "browserConnectionResult",
|
||||
success: false,
|
||||
text: `Error discovering browser: ${error instanceof Error ? error.message : String(error)}`,
|
||||
})
|
||||
}
|
||||
break
|
||||
case "togglePlanActMode":
|
||||
if (message.chatSettings) {
|
||||
await this.togglePlanActModeWithChatSettings(message.chatSettings, message.chatContent)
|
||||
@@ -428,11 +284,11 @@ export class Controller {
|
||||
text: message.text,
|
||||
})
|
||||
break
|
||||
case "relaunchChromeDebugMode":
|
||||
const { browserSettings } = await getAllExtensionState(this.context)
|
||||
const browserSession = new BrowserSession(this.context, browserSettings)
|
||||
await browserSession.relaunchChromeDebugMode(this)
|
||||
break
|
||||
// case "relaunchChromeDebugMode":
|
||||
// if (this.task) {
|
||||
// this.task.browserSession.relaunchChromeDebugMode()
|
||||
// }
|
||||
// break
|
||||
case "askResponse":
|
||||
this.task?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
|
||||
break
|
||||
@@ -491,9 +347,6 @@ export class Controller {
|
||||
case "refreshOpenRouterModels":
|
||||
await this.refreshOpenRouterModels()
|
||||
break
|
||||
case "refreshRequestyModels":
|
||||
await this.refreshRequestyModels()
|
||||
break
|
||||
case "refreshOpenAiModels":
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const openAiModels = await this.getOpenAiModels(apiConfiguration.openAiBaseUrl, apiConfiguration.openAiApiKey)
|
||||
@@ -582,7 +435,7 @@ export class Controller {
|
||||
break
|
||||
}
|
||||
case "showMcpView": {
|
||||
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked", tab: message.tab || undefined })
|
||||
await this.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
|
||||
break
|
||||
}
|
||||
case "openMcpSettings": {
|
||||
@@ -604,7 +457,14 @@ export class Controller {
|
||||
await this.togglePlanActModeWithChatSettings({ mode: "act" })
|
||||
}
|
||||
|
||||
// 2. download MCP
|
||||
// 2. Enable MCP settings if disabled
|
||||
// Enable MCP mode if disabled
|
||||
const mcpConfig = vscode.workspace.getConfiguration("cline.mcp")
|
||||
if (mcpConfig.get<string>("mode") !== "full") {
|
||||
await mcpConfig.update("mode", "full", true)
|
||||
}
|
||||
|
||||
// 3. download MCP
|
||||
await this.downloadMcp(message.mcpId)
|
||||
}
|
||||
break
|
||||
@@ -748,13 +608,6 @@ export class Controller {
|
||||
})
|
||||
break
|
||||
}
|
||||
case "scrollToSettings": {
|
||||
await this.postMessageToWebview({
|
||||
type: "scrollToSettings",
|
||||
text: message.text,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "telemetrySetting": {
|
||||
if (message.telemetrySetting) {
|
||||
await this.updateTelemetrySetting(message.telemetrySetting)
|
||||
@@ -795,121 +648,6 @@ export class Controller {
|
||||
this.postMessageToWebview({ type: "relinquishControl" })
|
||||
break
|
||||
}
|
||||
case "getDetectedChromePath": {
|
||||
try {
|
||||
const { browserSettings } = await getAllExtensionState(this.context)
|
||||
const browserSession = new BrowserSession(this.context, browserSettings)
|
||||
const { path, isBundled } = await browserSession.getDetectedChromePath()
|
||||
await this.postMessageToWebview({
|
||||
type: "detectedChromePath",
|
||||
text: path,
|
||||
isBundled,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Error getting detected Chrome path:", error)
|
||||
}
|
||||
break
|
||||
}
|
||||
case "getRelativePaths": {
|
||||
if (message.uris && message.uris.length > 0) {
|
||||
const resolvedPaths = await Promise.all(
|
||||
message.uris.map(async (uriString) => {
|
||||
try {
|
||||
const fileUri = vscode.Uri.parse(uriString, true)
|
||||
const relativePath = vscode.workspace.asRelativePath(fileUri, false)
|
||||
|
||||
if (path.isAbsolute(relativePath)) {
|
||||
console.warn(`Dropped file ${relativePath} is outside the workspace. Sending original path.`)
|
||||
return fileUri.fsPath.replace(/\\/g, "/")
|
||||
} else {
|
||||
let finalPath = "/" + relativePath.replace(/\\/g, "/")
|
||||
try {
|
||||
const stat = await vscode.workspace.fs.stat(fileUri)
|
||||
if (stat.type === vscode.FileType.Directory) {
|
||||
finalPath += "/"
|
||||
}
|
||||
} catch (statError) {
|
||||
console.error(`Error stating file ${fileUri.fsPath}:`, statError)
|
||||
}
|
||||
return finalPath
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error calculating relative path for ${uriString}:`, error)
|
||||
return null
|
||||
}
|
||||
}),
|
||||
)
|
||||
await this.postMessageToWebview({
|
||||
type: "relativePathsResponse",
|
||||
paths: resolvedPaths,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "searchFiles": {
|
||||
const workspacePath = getWorkspacePath()
|
||||
|
||||
if (!workspacePath) {
|
||||
// Handle case where workspace path is not available
|
||||
await this.postMessageToWebview({
|
||||
type: "fileSearchResults",
|
||||
results: [],
|
||||
mentionsRequestId: message.mentionsRequestId,
|
||||
error: "No workspace path available",
|
||||
})
|
||||
break
|
||||
}
|
||||
try {
|
||||
// Call file search service with query from message
|
||||
const results = await searchWorkspaceFiles(
|
||||
message.query || "",
|
||||
workspacePath,
|
||||
20, // Use default limit, as filtering is now done in the backend
|
||||
)
|
||||
|
||||
// debug logging to be removed
|
||||
//console.log(`controller/index.ts: Search results: ${results.length}`)
|
||||
|
||||
// Send results back to webview
|
||||
await this.postMessageToWebview({
|
||||
type: "fileSearchResults",
|
||||
results,
|
||||
mentionsRequestId: message.mentionsRequestId,
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
// Send error response to webview
|
||||
await this.postMessageToWebview({
|
||||
type: "fileSearchResults",
|
||||
results: [],
|
||||
error: errorMessage,
|
||||
mentionsRequestId: message.mentionsRequestId,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case "toggleFavoriteModel": {
|
||||
if (message.modelId) {
|
||||
const { apiConfiguration } = await getAllExtensionState(this.context)
|
||||
const favoritedModelIds = apiConfiguration.favoritedModelIds || []
|
||||
|
||||
// Toggle favorite status
|
||||
const updatedFavorites = favoritedModelIds.includes(message.modelId)
|
||||
? favoritedModelIds.filter((id) => id !== message.modelId)
|
||||
: [...favoritedModelIds, message.modelId]
|
||||
|
||||
await updateGlobalState(this.context, "favoritedModelIds", updatedFavorites)
|
||||
|
||||
// Capture telemetry for model favorite toggle
|
||||
const isFavorited = !favoritedModelIds.includes(message.modelId)
|
||||
telemetryService.captureModelFavoritesUsage(message.modelId, isFavorited)
|
||||
|
||||
// Post state to webview without changing any other configuration
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
break
|
||||
}
|
||||
// Add more switch case statements here as more webview message commands
|
||||
// are created within the webview context (i.e. inside media/main.js)
|
||||
}
|
||||
@@ -935,7 +673,6 @@ export class Controller {
|
||||
previousModeModelInfo: newModelInfo,
|
||||
previousModeVsCodeLmModelSelector: newVsCodeLmModelSelector,
|
||||
previousModeThinkingBudgetTokens: newThinkingBudgetTokens,
|
||||
previousModeReasoningEffort: newReasoningEffort,
|
||||
planActSeparateModelsSetting,
|
||||
} = await getAllExtensionState(this.context)
|
||||
|
||||
@@ -945,7 +682,6 @@ export class Controller {
|
||||
// Save the last model used in this mode
|
||||
await updateGlobalState(this.context, "previousModeApiProvider", apiConfiguration.apiProvider)
|
||||
await updateGlobalState(this.context, "previousModeThinkingBudgetTokens", apiConfiguration.thinkingBudgetTokens)
|
||||
await updateGlobalState(this.context, "previousModeReasoningEffort", apiConfiguration.reasoningEffort)
|
||||
switch (apiConfiguration.apiProvider) {
|
||||
case "anthropic":
|
||||
case "bedrock":
|
||||
@@ -955,7 +691,6 @@ export class Controller {
|
||||
case "openai-native":
|
||||
case "qwen":
|
||||
case "deepseek":
|
||||
case "xai":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.apiModelId)
|
||||
break
|
||||
case "openrouter":
|
||||
@@ -986,21 +721,13 @@ export class Controller {
|
||||
break
|
||||
case "requesty":
|
||||
await updateGlobalState(this.context, "previousModeModelId", apiConfiguration.requestyModelId)
|
||||
await updateGlobalState(this.context, "previousModeModelInfo", apiConfiguration.requestyModelInfo)
|
||||
break
|
||||
}
|
||||
|
||||
// Restore the model used in previous mode
|
||||
if (
|
||||
newApiProvider ||
|
||||
newModelId ||
|
||||
newThinkingBudgetTokens !== undefined ||
|
||||
newReasoningEffort ||
|
||||
newVsCodeLmModelSelector
|
||||
) {
|
||||
if (newApiProvider || newModelId || newThinkingBudgetTokens !== undefined || newVsCodeLmModelSelector) {
|
||||
await updateGlobalState(this.context, "apiProvider", newApiProvider)
|
||||
await updateGlobalState(this.context, "thinkingBudgetTokens", newThinkingBudgetTokens)
|
||||
await updateGlobalState(this.context, "reasoningEffort", newReasoningEffort)
|
||||
switch (newApiProvider) {
|
||||
case "anthropic":
|
||||
case "bedrock":
|
||||
@@ -1010,7 +737,6 @@ export class Controller {
|
||||
case "openai-native":
|
||||
case "qwen":
|
||||
case "deepseek":
|
||||
case "xai":
|
||||
await updateGlobalState(this.context, "apiModelId", newModelId)
|
||||
break
|
||||
case "openrouter":
|
||||
@@ -1036,7 +762,6 @@ export class Controller {
|
||||
break
|
||||
case "requesty":
|
||||
await updateGlobalState(this.context, "requestyModelId", newModelId)
|
||||
await updateGlobalState(this.context, "requestyModelInfo", newModelInfo)
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1091,7 +816,7 @@ export class Controller {
|
||||
// 'abandoned' will prevent this cline instance from affecting future cline instance gui. this may happen if its hanging on a streaming request
|
||||
this.task.abandoned = true
|
||||
}
|
||||
await this.initTask(undefined, undefined, historyItem) // clears task again, so we need to abortTask manually above
|
||||
await this.initClineWithHistoryItem(historyItem) // clears task again, so we need to abortTask manually above
|
||||
// await this.postStateToWebview() // new Cline instance will post state when it's ready. having this here sent an empty messages array to webview leading to virtuoso having to reload the entire list
|
||||
}
|
||||
}
|
||||
@@ -1104,6 +829,61 @@ export class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// MCP
|
||||
|
||||
async getDocumentsPath(): Promise<string> {
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
const { stdout: docsPath } = await execa("powershell", [
|
||||
"-NoProfile", // Ignore user's PowerShell profile(s)
|
||||
"-Command",
|
||||
"[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments)",
|
||||
])
|
||||
const trimmedPath = docsPath.trim()
|
||||
if (trimmedPath) {
|
||||
return trimmedPath
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to retrieve Windows Documents path. Falling back to homedir/Documents.")
|
||||
}
|
||||
} else if (process.platform === "linux") {
|
||||
try {
|
||||
// First check if xdg-user-dir exists
|
||||
await execa("which", ["xdg-user-dir"])
|
||||
|
||||
// If it exists, try to get XDG documents path
|
||||
const { stdout } = await execa("xdg-user-dir", ["DOCUMENTS"])
|
||||
const trimmedPath = stdout.trim()
|
||||
if (trimmedPath) {
|
||||
return trimmedPath
|
||||
}
|
||||
} catch {
|
||||
// Log error but continue to fallback
|
||||
console.error("Failed to retrieve XDG Documents path. Falling back to homedir/Documents.")
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback for all platforms
|
||||
return path.join(os.homedir(), "Documents")
|
||||
}
|
||||
|
||||
async ensureMcpServersDirectoryExists(): Promise<string> {
|
||||
const userDocumentsPath = await this.getDocumentsPath()
|
||||
const mcpServersDir = path.join(userDocumentsPath, "Cline", "MCP")
|
||||
try {
|
||||
await fs.mkdir(mcpServersDir, { recursive: true })
|
||||
} catch (error) {
|
||||
return "~/Documents/Cline/MCP" // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine since this path is only ever used in the system prompt
|
||||
}
|
||||
return mcpServersDir
|
||||
}
|
||||
|
||||
async ensureSettingsDirectoryExists(): Promise<string> {
|
||||
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
|
||||
await fs.mkdir(settingsDir, { recursive: true })
|
||||
return settingsDir
|
||||
}
|
||||
|
||||
// VSCode LM API
|
||||
|
||||
private async getVsCodeLmModels() {
|
||||
@@ -1345,17 +1125,15 @@ export class Controller {
|
||||
|
||||
// Create task with context from README and added guidelines for MCP server installation
|
||||
const task = `Set up the MCP server from ${mcpDetails.githubUrl} while adhering to these MCP server installation rules:
|
||||
- Start by loading the MCP documentation.
|
||||
- Use "${mcpDetails.mcpId}" as the server name in cline_mcp_settings.json.
|
||||
- Create the directory for the new MCP server before starting installation.
|
||||
- Make sure you read the user's existing cline_mcp_settings.json file before editing it with this new mcp, to not overwrite any existing servers.
|
||||
- Use commands aligned with the user's shell and operating system best practices.
|
||||
- The following README may contain instructions that conflict with the user's OS, in which case proceed thoughtfully.
|
||||
- Once installed, demonstrate the server's capabilities by using one of its tools.
|
||||
Here is the project's README to help you get started:\n\n${mcpDetails.readmeContent}\n${mcpDetails.llmsInstallationContent}`
|
||||
|
||||
// Initialize task and show chat view
|
||||
await this.initTask(task)
|
||||
await this.initClineWithTask(task)
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
@@ -1399,7 +1177,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
return []
|
||||
}
|
||||
|
||||
const config: AxiosRequestConfig = {}
|
||||
const config: Record<string, any> = {}
|
||||
if (apiKey) {
|
||||
config["headers"] = { Authorization: `Bearer ${apiKey}` }
|
||||
}
|
||||
@@ -1580,52 +1358,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
return models
|
||||
}
|
||||
|
||||
async refreshRequestyModels() {
|
||||
const parsePrice = (price: any) => {
|
||||
if (price) {
|
||||
return parseFloat(price) * 1_000_000
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
let models: Record<string, ModelInfo> = {}
|
||||
try {
|
||||
const apiKey = await getSecret(this.context, "requestyApiKey")
|
||||
const headers = {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
}
|
||||
const response = await axios.get("https://router.requesty.ai/v1/models", { headers })
|
||||
if (response.data?.data) {
|
||||
for (const model of response.data.data) {
|
||||
const modelInfo: ModelInfo = {
|
||||
maxTokens: model.max_output_tokens || undefined,
|
||||
contextWindow: model.context_window,
|
||||
supportsImages: model.supports_vision || undefined,
|
||||
supportsComputerUse: model.supports_computer_use || undefined,
|
||||
supportsPromptCache: model.supports_caching || undefined,
|
||||
inputPrice: parsePrice(model.input_price),
|
||||
outputPrice: parsePrice(model.output_price),
|
||||
cacheWritesPrice: parsePrice(model.caching_price),
|
||||
cacheReadsPrice: parsePrice(model.cached_price),
|
||||
description: model.description,
|
||||
}
|
||||
models[model.id] = modelInfo
|
||||
}
|
||||
console.log("Requesty models fetched", models)
|
||||
} else {
|
||||
console.error("Invalid response from Requesty API")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching Requesty models:", error)
|
||||
}
|
||||
|
||||
await this.postMessageToWebview({
|
||||
type: "requestyModels",
|
||||
requestyModels: models,
|
||||
})
|
||||
return models
|
||||
}
|
||||
|
||||
// Context menus and code actions
|
||||
|
||||
getFileMentionFromPath(filePath: string) {
|
||||
@@ -1689,7 +1421,9 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
|
||||
const fileMention = this.getFileMentionFromPath(filePath)
|
||||
const problemsString = this.convertDiagnosticsToProblemsString(diagnostics)
|
||||
await this.initTask(`Fix the following code in ${fileMention}\n\`\`\`\n${code}\n\`\`\`\n\nProblems:\n${problemsString}`)
|
||||
await this.initClineWithTask(
|
||||
`Fix the following code in ${fileMention}\n\`\`\`\n${code}\n\`\`\`\n\nProblems:\n${problemsString}`,
|
||||
)
|
||||
|
||||
console.log("fixWithCline", code, filePath, languageId, diagnostics, problemsString)
|
||||
}
|
||||
@@ -1729,8 +1463,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
taskDirPath: string
|
||||
apiConversationHistoryFilePath: string
|
||||
uiMessagesFilePath: string
|
||||
contextHistoryFilePath: string
|
||||
taskMetadataFilePath: string
|
||||
apiConversationHistory: Anthropic.MessageParam[]
|
||||
}> {
|
||||
const history = ((await getGlobalState(this.context, "taskHistory")) as HistoryItem[] | undefined) || []
|
||||
@@ -1739,8 +1471,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
|
||||
const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
|
||||
const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
|
||||
const contextHistoryFilePath = path.join(taskDirPath, GlobalFileNames.contextHistory)
|
||||
const taskMetadataFilePath = path.join(taskDirPath, GlobalFileNames.taskMetadata)
|
||||
const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
|
||||
if (fileExists) {
|
||||
const apiConversationHistory = JSON.parse(await fs.readFile(apiConversationHistoryFilePath, "utf8"))
|
||||
@@ -1749,8 +1479,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
taskDirPath,
|
||||
apiConversationHistoryFilePath,
|
||||
uiMessagesFilePath,
|
||||
contextHistoryFilePath,
|
||||
taskMetadataFilePath,
|
||||
apiConversationHistory,
|
||||
}
|
||||
}
|
||||
@@ -1765,7 +1493,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
if (id !== this.task?.taskId) {
|
||||
// non-current task
|
||||
const { historyItem } = await this.getTaskWithId(id)
|
||||
await this.initTask(undefined, undefined, historyItem) // clears existing task
|
||||
await this.initClineWithHistoryItem(historyItem) // clears existing task
|
||||
}
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
@@ -1822,28 +1550,22 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
console.debug("cleared task")
|
||||
}
|
||||
|
||||
const {
|
||||
taskDirPath,
|
||||
apiConversationHistoryFilePath,
|
||||
uiMessagesFilePath,
|
||||
contextHistoryFilePath,
|
||||
taskMetadataFilePath,
|
||||
} = await this.getTaskWithId(id)
|
||||
const legacyMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
|
||||
const { taskDirPath, apiConversationHistoryFilePath, uiMessagesFilePath } = await this.getTaskWithId(id)
|
||||
|
||||
const updatedTaskHistory = await this.deleteTaskFromState(id)
|
||||
|
||||
// Delete the task files
|
||||
for (const filePath of [
|
||||
apiConversationHistoryFilePath,
|
||||
uiMessagesFilePath,
|
||||
contextHistoryFilePath,
|
||||
taskMetadataFilePath,
|
||||
legacyMessagesFilePath,
|
||||
]) {
|
||||
const fileExists = await fileExistsAtPath(filePath)
|
||||
if (fileExists) {
|
||||
await fs.unlink(filePath)
|
||||
}
|
||||
const apiConversationHistoryFileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
|
||||
if (apiConversationHistoryFileExists) {
|
||||
await fs.unlink(apiConversationHistoryFilePath)
|
||||
}
|
||||
const uiMessagesFileExists = await fileExistsAtPath(uiMessagesFilePath)
|
||||
if (uiMessagesFileExists) {
|
||||
await fs.unlink(uiMessagesFilePath)
|
||||
}
|
||||
const legacyMessagesFilePath = path.join(taskDirPath, "claude_messages.json")
|
||||
if (await fileExistsAtPath(legacyMessagesFilePath)) {
|
||||
await fs.unlink(legacyMessagesFilePath)
|
||||
}
|
||||
|
||||
await fs.rmdir(taskDirPath) // succeeds if the dir is empty
|
||||
|
||||
@@ -10,7 +10,6 @@ import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
|
||||
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
|
||||
import { getCommitInfo } from "../../utils/git"
|
||||
import { getWorkingState } from "../../utils/git"
|
||||
import { FileContextTracker } from "../context/context-tracking/FileContextTracker"
|
||||
|
||||
export function openMention(mention?: string): void {
|
||||
if (!mention) {
|
||||
@@ -39,12 +38,7 @@ export function openMention(mention?: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseMentions(
|
||||
text: string,
|
||||
cwd: string,
|
||||
urlContentFetcher: UrlContentFetcher,
|
||||
fileContextTracker?: FileContextTracker,
|
||||
): Promise<string> {
|
||||
export async function parseMentions(text: string, cwd: string, urlContentFetcher: UrlContentFetcher): Promise<string> {
|
||||
const mentions: Set<string> = new Set()
|
||||
let parsedText = text.replace(mentionRegexGlobal, (match, mention) => {
|
||||
mentions.add(mention)
|
||||
@@ -104,10 +98,6 @@ export async function parseMentions(
|
||||
parsedText += `\n\n<folder_content path="${mentionPath}">\n${content}\n</folder_content>`
|
||||
} else {
|
||||
parsedText += `\n\n<file_content path="${mentionPath}">\n${content}\n</file_content>`
|
||||
// Track that this file was mentioned and its content was included
|
||||
if (fileContextTracker) {
|
||||
await fileContextTracker.trackFileContext(mentionPath, "file_mentioned")
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (mention.endsWith("/")) {
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
import { McpHub } from "../../services/mcp/McpHub"
|
||||
|
||||
export async function loadMcpDocumentation(mcpHub: McpHub) {
|
||||
return `## Creating an MCP Server
|
||||
|
||||
When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through obtaining an application client ID and secret, you may have to create a separate one-time setup script (like get-refresh-token.js) that captures and logs the final piece of the puzzle: the user's refresh token (i.e. you might run the script using execute_command which would open a browser for authentication, and then log the refresh token so that you can see it in the command output for you to use in the MCP settings configuration).
|
||||
|
||||
Unless the user specifies otherwise, new MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
|
||||
|
||||
### Example MCP Server
|
||||
|
||||
For example, if the user wanted to give you the ability to retrieve weather information, you could create an MCP server that uses the OpenWeather API to get weather information, add it to the MCP settings configuration file, and then notice that you now have access to new tools and resources in the system prompt that you might use to show the user your new capabilities.
|
||||
|
||||
The following example demonstrates how to build an MCP server that provides weather data functionality. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS)
|
||||
|
||||
1. Use the \`create-typescript-server\` tool to bootstrap a new project in the default MCP servers directory:
|
||||
|
||||
\`\`\`bash
|
||||
cd ${await mcpHub.getMcpServersPath()}
|
||||
npx @modelcontextprotocol/create-server weather-server
|
||||
cd weather-server
|
||||
# Install dependencies
|
||||
npm install axios
|
||||
\`\`\`
|
||||
|
||||
This will create a new project with the following structure:
|
||||
|
||||
\`\`\`
|
||||
weather-server/
|
||||
├── package.json
|
||||
{
|
||||
...
|
||||
"type": "module", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)
|
||||
"scripts": {
|
||||
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
└── weather-server/
|
||||
└── index.ts # Main server implementation
|
||||
\`\`\`
|
||||
|
||||
2. Replace \`src/index.ts\` with the following:
|
||||
|
||||
\`\`\`typescript
|
||||
#!/usr/bin/env node
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
ListResourcesRequestSchema,
|
||||
ListResourceTemplatesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_KEY = process.env.OPENWEATHER_API_KEY; // provided by MCP config
|
||||
if (!API_KEY) {
|
||||
throw new Error('OPENWEATHER_API_KEY environment variable is required');
|
||||
}
|
||||
|
||||
interface OpenWeatherResponse {
|
||||
main: {
|
||||
temp: number;
|
||||
humidity: number;
|
||||
};
|
||||
weather: [{ description: string }];
|
||||
wind: { speed: number };
|
||||
dt_txt?: string;
|
||||
}
|
||||
|
||||
const isValidForecastArgs = (
|
||||
args: any
|
||||
): args is { city: string; days?: number } =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.city === 'string' &&
|
||||
(args.days === undefined || typeof args.days === 'number');
|
||||
|
||||
class WeatherServer {
|
||||
private server: Server;
|
||||
private axiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'example-weather-server',
|
||||
version: '0.1.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: 'http://api.openweathermap.org/data/2.5',
|
||||
params: {
|
||||
appid: API_KEY,
|
||||
units: 'metric',
|
||||
},
|
||||
});
|
||||
|
||||
this.setupResourceHandlers();
|
||||
this.setupToolHandlers();
|
||||
|
||||
// Error handling
|
||||
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
||||
process.on('SIGINT', async () => {
|
||||
await this.server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// MCP Resources represent any kind of UTF-8 encoded data that an MCP server wants to make available to clients, such as database records, API responses, log files, and more. Servers define direct resources with a static URI or dynamic resources with a URI template that follows the format \`[protocol]://[host]/[path]\`.
|
||||
private setupResourceHandlers() {
|
||||
// For static resources, servers can expose a list of resources:
|
||||
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||
resources: [
|
||||
// This is a poor example since you could use the resource template to get the same information but this demonstrates how to define a static resource
|
||||
{
|
||||
uri: \`weather://San Francisco/current\`, // Unique identifier for San Francisco weather resource
|
||||
name: \`Current weather in San Francisco\`, // Human-readable name
|
||||
mimeType: 'application/json', // Optional MIME type
|
||||
// Optional description
|
||||
description:
|
||||
'Real-time weather data for San Francisco including temperature, conditions, humidity, and wind speed',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// For dynamic resources, servers can expose resource templates:
|
||||
this.server.setRequestHandler(
|
||||
ListResourceTemplatesRequestSchema,
|
||||
async () => ({
|
||||
resourceTemplates: [
|
||||
{
|
||||
uriTemplate: 'weather://{city}/current', // URI template (RFC 6570)
|
||||
name: 'Current weather for a given city', // Human-readable name
|
||||
mimeType: 'application/json', // Optional MIME type
|
||||
description: 'Real-time weather data for a specified city', // Optional description
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// ReadResourceRequestSchema is used for both static resources and dynamic resource templates
|
||||
this.server.setRequestHandler(
|
||||
ReadResourceRequestSchema,
|
||||
async (request) => {
|
||||
const match = request.params.uri.match(
|
||||
/^weather:\/\/([^/]+)\/current$/
|
||||
);
|
||||
if (!match) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
\`Invalid URI format: \${request.params.uri}\`
|
||||
);
|
||||
}
|
||||
const city = decodeURIComponent(match[1]);
|
||||
|
||||
try {
|
||||
const response = await this.axiosInstance.get(
|
||||
'weather', // current weather
|
||||
{
|
||||
params: { q: city },
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: request.params.uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
temperature: response.data.main.temp,
|
||||
conditions: response.data.weather[0].description,
|
||||
humidity: response.data.main.humidity,
|
||||
wind_speed: response.data.wind.speed,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
\`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* MCP Tools enable servers to expose executable functionality to the system. Through these tools, you can interact with external systems, perform computations, and take actions in the real world.
|
||||
* - Like resources, tools are identified by unique names and can include descriptions to guide their usage. However, unlike resources, tools represent dynamic operations that can modify state or interact with external systems.
|
||||
* - While resources and tools are similar, you should prefer to create tools over resources when possible as they provide more flexibility.
|
||||
*/
|
||||
private setupToolHandlers() {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: 'get_forecast', // Unique identifier
|
||||
description: 'Get weather forecast for a city', // Human-readable description
|
||||
inputSchema: {
|
||||
// JSON Schema for parameters
|
||||
type: 'object',
|
||||
properties: {
|
||||
city: {
|
||||
type: 'string',
|
||||
description: 'City name',
|
||||
},
|
||||
days: {
|
||||
type: 'number',
|
||||
description: 'Number of days (1-5)',
|
||||
minimum: 1,
|
||||
maximum: 5,
|
||||
},
|
||||
},
|
||||
required: ['city'], // Array of required property names
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name !== 'get_forecast') {
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
\`Unknown tool: \${request.params.name}\`
|
||||
);
|
||||
}
|
||||
|
||||
if (!isValidForecastArgs(request.params.arguments)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid forecast arguments'
|
||||
);
|
||||
}
|
||||
|
||||
const city = request.params.arguments.city;
|
||||
const days = Math.min(request.params.arguments.days || 3, 5);
|
||||
|
||||
try {
|
||||
const response = await this.axiosInstance.get<{
|
||||
list: OpenWeatherResponse[];
|
||||
}>('forecast', {
|
||||
params: {
|
||||
q: city,
|
||||
cnt: days * 8,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response.data.list, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: \`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('Weather MCP server running on stdio');
|
||||
}
|
||||
}
|
||||
|
||||
const server = new WeatherServer();
|
||||
server.run().catch(console.error);
|
||||
\`\`\`
|
||||
|
||||
(Remember: This is just an example–you may use different dependencies, break the implementation up into multiple files, etc.)
|
||||
|
||||
3. Build and compile the executable JavaScript file
|
||||
|
||||
\`\`\`bash
|
||||
npm run build
|
||||
\`\`\`
|
||||
|
||||
4. Whenever you need an environment variable such as an API key to configure the MCP server, walk the user through the process of getting the key. For example, they may need to create an account and go to a developer dashboard to generate the key. Provide step-by-step instructions and URLs to make it easy for the user to retrieve the necessary information. Then use the ask_followup_question tool to ask the user for the key, in this case the OpenWeather API key.
|
||||
|
||||
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
|
||||
|
||||
IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false and autoApprove=[].
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"mcpServers": {
|
||||
...,
|
||||
"weather": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/weather-server/build/index.js"],
|
||||
"env": {
|
||||
"OPENWEATHER_API_KEY": "user-provided-api-key"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.)
|
||||
|
||||
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section. (Note: If you encounter a 'not connected' error when testing a newly installed mcp server, a common cause is an incorrect build path in your MCP settings configuration. Since compiled JavaScript files are commonly output to either 'dist/' or 'build/' directories, double-check that the build path in your MCP settings matches where your files are actually being compiled. E.g. If you assumed 'build' as the folder, check tsconfig.json to see if it's using 'dist' instead.)
|
||||
|
||||
7. Now that you have access to these new tools and resources, you may suggest ways the user can command you to invoke them - for example, with this new weather tool now available, you can invite the user to ask "what's the weather in San Francisco?"
|
||||
|
||||
## Editing MCP Servers
|
||||
|
||||
The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' below: ${
|
||||
mcpHub
|
||||
.getServers()
|
||||
.filter((server) => server.status === "connected")
|
||||
.map((server) => server.name)
|
||||
.join(", ") || "(None running currently)"
|
||||
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use replace_in_file to make changes to the files.
|
||||
|
||||
However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server.
|
||||
|
||||
# MCP Servers Are Not Always Necessary
|
||||
|
||||
The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that...").
|
||||
|
||||
Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.`
|
||||
}
|
||||
@@ -4,12 +4,6 @@ import * as path from "path"
|
||||
import { ClineIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/ClineIgnoreController"
|
||||
|
||||
export const formatResponse = {
|
||||
duplicateFileReadNotice: () =>
|
||||
`[[NOTE] This file read has been removed to save space in the context window. Refer to the latest file read for the most up to date version of this file.]`,
|
||||
|
||||
contextTruncationNotice: () =>
|
||||
`[NOTE] Some previous conversation history with the user has been removed to maintain optimal context window length. The initial user task and the most recent exchanges have been retained for continuity, while intermediate conversation history has been removed. Please keep this in mind as you continue assisting the user.`,
|
||||
|
||||
toolDenied: () => `The user denied this operation.`,
|
||||
|
||||
toolError: (error?: string) => `The tool execution failed with the following error:\n<error>\n${error}\n</error>`,
|
||||
@@ -130,8 +124,8 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
cwd: string,
|
||||
wasRecent: boolean | 0 | undefined,
|
||||
responseText?: string,
|
||||
): [string, string] => {
|
||||
const taskResumptionMessage = `[TASK RESUMPTION] ${
|
||||
) => {
|
||||
return `[TASK RESUMPTION] ${
|
||||
mode === "plan"
|
||||
? `This task was interrupted ${agoText}. The conversation may have been incomplete. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful. However you are in PLAN MODE, so rather than continuing the task, you must respond to the user's message.`
|
||||
: `This task was interrupted ${agoText}. It may or may not be complete, so please reassess the task context. Be aware that the project state may have changed since then. The current working directory is now '${cwd.toPosix()}'. If the task has not been completed, retry the last step before interruption and proceed with completing the task.\n\nNote: If you previously attempted a tool use that the user did not provide a result for, you should assume the tool use was not successful and assess whether you should retry. If the last tool was a browser_action, the browser has been closed and you must launch a new browser if needed.`
|
||||
@@ -139,17 +133,13 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
wasRecent
|
||||
? "\n\nIMPORTANT: If the last tool use was a replace_in_file or write_to_file that was interrupted, the file was reverted back to its original state before the interrupted edit, and you do NOT need to re-read the file as you already have its up-to-date contents."
|
||||
: ""
|
||||
}`
|
||||
|
||||
const userResponseMessage = `${
|
||||
}${
|
||||
responseText
|
||||
? `${mode === "plan" ? "New message to respond to with plan_mode_respond tool (be sure to provide your response in the <response> parameter)" : "New instructions for task continuation"}:\n<user_message>\n${responseText}\n</user_message>`
|
||||
? `\n\n${mode === "plan" ? "New message to respond to with plan_mode_respond tool (be sure to provide your response in the <response> parameter)" : "New instructions for task continuation"}:\n<user_message>\n${responseText}\n</user_message>`
|
||||
: mode === "plan"
|
||||
? "(The user did not provide a new message. Consider asking them how they'd like you to proceed, or suggest to them to switch to Act mode to continue with the task.)"
|
||||
? "(The user did not provide a new message. Consider asking them how they'd like you to proceed, or to switch to Act mode to continue with the task.)"
|
||||
: ""
|
||||
}`
|
||||
|
||||
return [taskResumptionMessage, userResponseMessage]
|
||||
},
|
||||
|
||||
planModeInstructions: () => {
|
||||
@@ -196,7 +186,7 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
`This is likely because the SEARCH block content doesn't match exactly with what's in the file, or if you used multiple SEARCH/REPLACE blocks they may not have been in the order they appear in the file.\n\n` +
|
||||
`The file was reverted to its original state:\n\n` +
|
||||
`<file_content path="${relPath.toPosix()}">\n${originalContent}\n</file_content>\n\n` +
|
||||
`Now that you have the latest state of the file, try the operation again with fewer, more precise SEARCH blocks. For large files especially, it may be prudent to try to limit yourself to <5 SEARCH/REPLACE blocks at a time, then wait for the user to respond with the result of the operation before following up with another replace_in_file call to make additional edits.\n(If you run into this error 3 times in a row, you may use the write_to_file tool as a fallback.)`,
|
||||
`Now that you have the latest state of the file, try the operation again with fewer/more precise SEARCH blocks.\n(If you run into this error 3 times in a row, you may use the write_to_file tool as a fallback. Keep in mind, the write_to_file fallback is far from ideal, as this means you'll be re-writing the entire contents of the file just to make a few edits, which takes time and money. So let's bias towards using replace_in_file as effectively as possible)`,
|
||||
|
||||
toolAlreadyUsed: (toolName: string) =>
|
||||
`Tool [${toolName}] was not executed because a tool has already been used in this message. Only one tool may be used per message. You must assess the first tool's result before proceeding to use the next tool.`,
|
||||
@@ -204,13 +194,10 @@ Otherwise, if you have not completed the task and do not need additional informa
|
||||
clineIgnoreInstructions: (content: string) =>
|
||||
`# .clineignore\n\n(The following is provided by a root-level .clineignore file where the user has specified files and directories that should not be accessed. When using list_files, you'll notice a ${LOCK_TEXT_SYMBOL} next to files that are blocked. Attempting to access the file's contents e.g. through read_file will result in an error.)\n\n${content}\n.clineignore`,
|
||||
|
||||
clineRulesGlobalDirectoryInstructions: (content: string) =>
|
||||
`# .clinerules/\n\nThe following is provided by a global .clinerules/ directory where the user has specified instructions:\n\n${content}`,
|
||||
|
||||
clineRulesLocalDirectoryInstructions: (cwd: string, content: string) =>
|
||||
clineRulesDirectoryInstructions: (cwd: string, content: string) =>
|
||||
`# .clinerules/\n\nThe following is provided by a root-level .clinerules/ directory where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
|
||||
|
||||
clineRulesLocalFileInstructions: (cwd: string, content: string) =>
|
||||
clineRulesFileInstructions: (cwd: string, content: string) =>
|
||||
`# .clinerules\n\nThe following is provided by a root-level .clinerules file where the user has specified instructions for this working directory (${cwd.toPosix()})\n\n${content}`,
|
||||
}
|
||||
|
||||
|
||||
+412
-50
@@ -177,6 +177,9 @@ Usage:
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
mcpHub.getMode() !== "off"
|
||||
? `
|
||||
## use_mcp_tool
|
||||
Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.
|
||||
Parameters:
|
||||
@@ -205,6 +208,9 @@ Usage:
|
||||
<server_name>server name here</server_name>
|
||||
<uri>resource URI here</uri>
|
||||
</access_mcp_resource>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
## ask_followup_question
|
||||
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
|
||||
@@ -233,36 +239,19 @@ Your final result description here
|
||||
<command>Command to demonstrate result (optional)</command>
|
||||
</attempt_completion>
|
||||
|
||||
## new_task
|
||||
Description: Request to create a new task with preloaded context. The user will be presented with a preview of the context and can choose to create a new task or keep chatting in the current conversation. The user may choose to start a new task at any point.
|
||||
Parameters:
|
||||
- context: (required) The context to preload the new task with. This should include:
|
||||
* Comprehensively explain what has been accomplished in the current task - mention specific file names that are relevant
|
||||
* The specific next steps or focus for the new task - mention specific file names that are relevant
|
||||
* Any critical information needed to continue the work
|
||||
* Clear indication of how this new task relates to the overall workflow
|
||||
* This should be akin to a long handoff file, enough for a totally new developer to be able to pick up where you left off and know exactly what to do next and which files to look at.
|
||||
Usage:
|
||||
<new_task>
|
||||
<context>context to preload new task with</context>
|
||||
</new_task>
|
||||
|
||||
## plan_mode_respond
|
||||
Description: Respond to the user's inquiry in an effort to plan a solution to the user's task. This tool should be used when you need to provide a response to a question or statement from the user about how you plan to accomplish the task. This tool is only available in PLAN MODE. The environment_details will specify the current mode, if it is not PLAN MODE then you should not use this tool. Depending on the user's message, you may ask questions to get clarification about the user's request, architect a solution to the task, and to brainstorm ideas with the user. For example, if the user's task is to create a website, you may start by asking some clarifying questions, then present a detailed plan for how you will accomplish the task given the context, and perhaps engage in a back and forth to finalize the details before the user switches you to ACT MODE to implement the solution.
|
||||
Parameters:
|
||||
- response: (required) The response to provide to the user. Do not try to use tools in this parameter, this is simply a chat response. (You MUST use the response parameter, do not simply place the response text directly within <plan_mode_respond> tags.)
|
||||
- options: (optional) An array of 2-5 options for the user to choose from. Each option should be a string describing a possible choice or path forward in the planning process. This can help guide the discussion and make it easier for the user to provide input on key decisions. You may not always need to provide options, but it may be helpful in many cases where it can save the user from having to type out a response manually. Do NOT present an option to toggle to Act mode, as this will be something you need to direct the user to do manually themselves.
|
||||
Usage:
|
||||
<plan_mode_respond>
|
||||
<response>Your response here</response>
|
||||
<options>
|
||||
Array of options here (optional), e.g. ["Option 1", "Option 2", "Option 3"]
|
||||
</options>
|
||||
</plan_mode_respond>
|
||||
|
||||
## load_mcp_documentation
|
||||
Description: Load documentation about creating MCP servers. This tool should be used when the user requests to create or install an MCP server (the user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`). The documentation provides detailed information about the MCP server creation process, including setup instructions, best practices, and examples.
|
||||
Parameters: None
|
||||
Usage:
|
||||
<load_mcp_documentation>
|
||||
</load_mcp_documentation>
|
||||
|
||||
# Tool Use Examples
|
||||
|
||||
## Example 1: Requesting to execute a command
|
||||
@@ -294,25 +283,7 @@ Usage:
|
||||
</content>
|
||||
</write_to_file>
|
||||
|
||||
## Example 3: Creating a new task
|
||||
|
||||
<new_task>
|
||||
<context>
|
||||
Authentication System Implementation:
|
||||
- We've implemented the basic user model with email/password
|
||||
- Password hashing is working with bcrypt
|
||||
- Login endpoint is functional with proper validation
|
||||
- JWT token generation is implemented
|
||||
|
||||
Next Steps:
|
||||
- Implement refresh token functionality
|
||||
- Add token validation middleware
|
||||
- Create password reset flow
|
||||
- Implement role-based access control
|
||||
</context>
|
||||
</new_task>
|
||||
|
||||
## Example 4: Requesting to make targeted edits to a file
|
||||
## Example 3: Requesting to make targeted edits to a file
|
||||
|
||||
<replace_in_file>
|
||||
<path>src/components/App.tsx</path>
|
||||
@@ -346,8 +317,11 @@ return (
|
||||
>>>>>>> REPLACE
|
||||
</diff>
|
||||
</replace_in_file>
|
||||
${
|
||||
mcpHub.getMode() !== "off"
|
||||
? `
|
||||
|
||||
## Example 5: Requesting to use an MCP tool
|
||||
## Example 4: Requesting to use an MCP tool
|
||||
|
||||
<use_mcp_tool>
|
||||
<server_name>weather-server</server_name>
|
||||
@@ -360,6 +334,13 @@ return (
|
||||
</arguments>
|
||||
</use_mcp_tool>
|
||||
|
||||
## Example 5: Requesting to access an MCP resource
|
||||
|
||||
<access_mcp_resource>
|
||||
<server_name>weather-server</server_name>
|
||||
<uri>weather://san-francisco/current</uri>
|
||||
</access_mcp_resource>
|
||||
|
||||
## Example 6: Another example of using an MCP tool (where the server name is a unique identifier such as a URL)
|
||||
|
||||
<use_mcp_tool>
|
||||
@@ -375,7 +356,9 @@ return (
|
||||
"assignees": ["octocat"]
|
||||
}
|
||||
</arguments>
|
||||
</use_mcp_tool>
|
||||
</use_mcp_tool>`
|
||||
: ""
|
||||
}
|
||||
|
||||
# Tool Use Guidelines
|
||||
|
||||
@@ -398,6 +381,9 @@ It is crucial to proceed step-by-step, waiting for the user's message after each
|
||||
|
||||
By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
|
||||
|
||||
${
|
||||
mcpHub.getMode() !== "off"
|
||||
? `
|
||||
====
|
||||
|
||||
MCP SERVERS
|
||||
@@ -444,6 +430,374 @@ ${
|
||||
})
|
||||
.join("\n\n")}`
|
||||
: "(No MCP servers currently connected)"
|
||||
}`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
mcpHub.getMode() === "full"
|
||||
? `
|
||||
## Creating an MCP Server
|
||||
|
||||
The user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. You have the ability to create an MCP server and add it to a configuration file that will then expose the tools and resources for you to use with \`use_mcp_tool\` and \`access_mcp_resource\`.
|
||||
|
||||
When creating MCP servers, it's important to understand that they operate in a non-interactive environment. The server cannot initiate OAuth flows, open browser windows, or prompt for user input during runtime. All credentials and authentication tokens must be provided upfront through environment variables in the MCP settings configuration. For example, Spotify's API uses OAuth to get a refresh token for the user, but the MCP server cannot initiate this flow. While you can walk the user through obtaining an application client ID and secret, you may have to create a separate one-time setup script (like get-refresh-token.js) that captures and logs the final piece of the puzzle: the user's refresh token (i.e. you might run the script using execute_command which would open a browser for authentication, and then log the refresh token so that you can see it in the command output for you to use in the MCP settings configuration).
|
||||
|
||||
Unless the user specifies otherwise, new MCP servers should be created in: ${await mcpHub.getMcpServersPath()}
|
||||
|
||||
### Example MCP Server
|
||||
|
||||
For example, if the user wanted to give you the ability to retrieve weather information, you could create an MCP server that uses the OpenWeather API to get weather information, add it to the MCP settings configuration file, and then notice that you now have access to new tools and resources in the system prompt that you might use to show the user your new capabilities.
|
||||
|
||||
The following example demonstrates how to build an MCP server that provides weather data functionality. While this example shows how to implement resources, resource templates, and tools, in practice you should prefer using tools since they are more flexible and can handle dynamic parameters. The resource and resource template implementations are included here mainly for demonstration purposes of the different MCP capabilities, but a real weather server would likely just expose tools for fetching weather data. (The following steps are for macOS)
|
||||
|
||||
1. Use the \`create-typescript-server\` tool to bootstrap a new project in the default MCP servers directory:
|
||||
|
||||
\`\`\`bash
|
||||
cd ${await mcpHub.getMcpServersPath()}
|
||||
npx @modelcontextprotocol/create-server weather-server
|
||||
cd weather-server
|
||||
# Install dependencies
|
||||
npm install axios
|
||||
\`\`\`
|
||||
|
||||
This will create a new project with the following structure:
|
||||
|
||||
\`\`\`
|
||||
weather-server/
|
||||
├── package.json
|
||||
{
|
||||
...
|
||||
"type": "module", // added by default, uses ES module syntax (import/export) rather than CommonJS (require/module.exports) (Important to know if you create additional scripts in this server repository like a get-refresh-token.js script)
|
||||
"scripts": {
|
||||
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
||||
...
|
||||
}
|
||||
...
|
||||
}
|
||||
├── tsconfig.json
|
||||
└── src/
|
||||
└── weather-server/
|
||||
└── index.ts # Main server implementation
|
||||
\`\`\`
|
||||
|
||||
2. Replace \`src/index.ts\` with the following:
|
||||
|
||||
\`\`\`typescript
|
||||
#!/usr/bin/env node
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ErrorCode,
|
||||
ListResourcesRequestSchema,
|
||||
ListResourceTemplatesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
McpError,
|
||||
ReadResourceRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_KEY = process.env.OPENWEATHER_API_KEY; // provided by MCP config
|
||||
if (!API_KEY) {
|
||||
throw new Error('OPENWEATHER_API_KEY environment variable is required');
|
||||
}
|
||||
|
||||
interface OpenWeatherResponse {
|
||||
main: {
|
||||
temp: number;
|
||||
humidity: number;
|
||||
};
|
||||
weather: [{ description: string }];
|
||||
wind: { speed: number };
|
||||
dt_txt?: string;
|
||||
}
|
||||
|
||||
const isValidForecastArgs = (
|
||||
args: any
|
||||
): args is { city: string; days?: number } =>
|
||||
typeof args === 'object' &&
|
||||
args !== null &&
|
||||
typeof args.city === 'string' &&
|
||||
(args.days === undefined || typeof args.days === 'number');
|
||||
|
||||
class WeatherServer {
|
||||
private server: Server;
|
||||
private axiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'example-weather-server',
|
||||
version: '0.1.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
this.axiosInstance = axios.create({
|
||||
baseURL: 'http://api.openweathermap.org/data/2.5',
|
||||
params: {
|
||||
appid: API_KEY,
|
||||
units: 'metric',
|
||||
},
|
||||
});
|
||||
|
||||
this.setupResourceHandlers();
|
||||
this.setupToolHandlers();
|
||||
|
||||
// Error handling
|
||||
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
||||
process.on('SIGINT', async () => {
|
||||
await this.server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
// MCP Resources represent any kind of UTF-8 encoded data that an MCP server wants to make available to clients, such as database records, API responses, log files, and more. Servers define direct resources with a static URI or dynamic resources with a URI template that follows the format \`[protocol]://[host]/[path]\`.
|
||||
private setupResourceHandlers() {
|
||||
// For static resources, servers can expose a list of resources:
|
||||
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
||||
resources: [
|
||||
// This is a poor example since you could use the resource template to get the same information but this demonstrates how to define a static resource
|
||||
{
|
||||
uri: \`weather://San Francisco/current\`, // Unique identifier for San Francisco weather resource
|
||||
name: \`Current weather in San Francisco\`, // Human-readable name
|
||||
mimeType: 'application/json', // Optional MIME type
|
||||
// Optional description
|
||||
description:
|
||||
'Real-time weather data for San Francisco including temperature, conditions, humidity, and wind speed',
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// For dynamic resources, servers can expose resource templates:
|
||||
this.server.setRequestHandler(
|
||||
ListResourceTemplatesRequestSchema,
|
||||
async () => ({
|
||||
resourceTemplates: [
|
||||
{
|
||||
uriTemplate: 'weather://{city}/current', // URI template (RFC 6570)
|
||||
name: 'Current weather for a given city', // Human-readable name
|
||||
mimeType: 'application/json', // Optional MIME type
|
||||
description: 'Real-time weather data for a specified city', // Optional description
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
// ReadResourceRequestSchema is used for both static resources and dynamic resource templates
|
||||
this.server.setRequestHandler(
|
||||
ReadResourceRequestSchema,
|
||||
async (request) => {
|
||||
const match = request.params.uri.match(
|
||||
/^weather:\/\/([^/]+)\/current$/
|
||||
);
|
||||
if (!match) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidRequest,
|
||||
\`Invalid URI format: \${request.params.uri}\`
|
||||
);
|
||||
}
|
||||
const city = decodeURIComponent(match[1]);
|
||||
|
||||
try {
|
||||
const response = await this.axiosInstance.get(
|
||||
'weather', // current weather
|
||||
{
|
||||
params: { q: city },
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{
|
||||
uri: request.params.uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify(
|
||||
{
|
||||
temperature: response.data.main.temp,
|
||||
conditions: response.data.weather[0].description,
|
||||
humidity: response.data.main.humidity,
|
||||
wind_speed: response.data.wind.speed,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InternalError,
|
||||
\`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/* MCP Tools enable servers to expose executable functionality to the system. Through these tools, you can interact with external systems, perform computations, and take actions in the real world.
|
||||
* - Like resources, tools are identified by unique names and can include descriptions to guide their usage. However, unlike resources, tools represent dynamic operations that can modify state or interact with external systems.
|
||||
* - While resources and tools are similar, you should prefer to create tools over resources when possible as they provide more flexibility.
|
||||
*/
|
||||
private setupToolHandlers() {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: 'get_forecast', // Unique identifier
|
||||
description: 'Get weather forecast for a city', // Human-readable description
|
||||
inputSchema: {
|
||||
// JSON Schema for parameters
|
||||
type: 'object',
|
||||
properties: {
|
||||
city: {
|
||||
type: 'string',
|
||||
description: 'City name',
|
||||
},
|
||||
days: {
|
||||
type: 'number',
|
||||
description: 'Number of days (1-5)',
|
||||
minimum: 1,
|
||||
maximum: 5,
|
||||
},
|
||||
},
|
||||
required: ['city'], // Array of required property names
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
if (request.params.name !== 'get_forecast') {
|
||||
throw new McpError(
|
||||
ErrorCode.MethodNotFound,
|
||||
\`Unknown tool: \${request.params.name}\`
|
||||
);
|
||||
}
|
||||
|
||||
if (!isValidForecastArgs(request.params.arguments)) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
'Invalid forecast arguments'
|
||||
);
|
||||
}
|
||||
|
||||
const city = request.params.arguments.city;
|
||||
const days = Math.min(request.params.arguments.days || 3, 5);
|
||||
|
||||
try {
|
||||
const response = await this.axiosInstance.get<{
|
||||
list: OpenWeatherResponse[];
|
||||
}>('forecast', {
|
||||
params: {
|
||||
q: city,
|
||||
cnt: days * 8,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(response.data.list, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: \`Weather API error: \${
|
||||
error.response?.data.message ?? error.message
|
||||
}\`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('Weather MCP server running on stdio');
|
||||
}
|
||||
}
|
||||
|
||||
const server = new WeatherServer();
|
||||
server.run().catch(console.error);
|
||||
\`\`\`
|
||||
|
||||
(Remember: This is just an example–you may use different dependencies, break the implementation up into multiple files, etc.)
|
||||
|
||||
3. Build and compile the executable JavaScript file
|
||||
|
||||
\`\`\`bash
|
||||
npm run build
|
||||
\`\`\`
|
||||
|
||||
4. Whenever you need an environment variable such as an API key to configure the MCP server, walk the user through the process of getting the key. For example, they may need to create an account and go to a developer dashboard to generate the key. Provide step-by-step instructions and URLs to make it easy for the user to retrieve the necessary information. Then use the ask_followup_question tool to ask the user for the key, in this case the OpenWeather API key.
|
||||
|
||||
5. Install the MCP Server by adding the MCP server configuration to the settings file located at '${await mcpHub.getMcpSettingsFilePath()}'. The settings file may have other MCP servers already configured, so you would read it first and then add your new server to the existing \`mcpServers\` object.
|
||||
|
||||
IMPORTANT: Regardless of what else you see in the MCP settings file, you must default any new MCP servers you create to disabled=false and autoApprove=[].
|
||||
|
||||
\`\`\`json
|
||||
{
|
||||
"mcpServers": {
|
||||
...,
|
||||
"weather": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/weather-server/build/index.js"],
|
||||
"env": {
|
||||
"OPENWEATHER_API_KEY": "user-provided-api-key"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
(Note: the user may also ask you to install the MCP server to the Claude desktop app, in which case you would read then modify \`~/Library/Application\ Support/Claude/claude_desktop_config.json\` on macOS for example. It follows the same format of a top level \`mcpServers\` object.)
|
||||
|
||||
6. After you have edited the MCP settings configuration file, the system will automatically run all the servers and expose the available tools and resources in the 'Connected MCP Servers' section. (Note: If you encounter a 'not connected' error when testing a newly installed mcp server, a common cause is an incorrect build path in your MCP settings configuration. Since compiled JavaScript files are commonly output to either 'dist/' or 'build/' directories, double-check that the build path in your MCP settings matches where your files are actually being compiled. E.g. If you assumed 'build' as the folder, check tsconfig.json to see if it's using 'dist' instead.)
|
||||
|
||||
7. Now that you have access to these new tools and resources, you may suggest ways the user can command you to invoke them - for example, with this new weather tool now available, you can invite the user to ask "what's the weather in San Francisco?"
|
||||
|
||||
## Editing MCP Servers
|
||||
|
||||
The user may ask to add tools or resources that may make sense to add to an existing MCP server (listed under 'Connected MCP Servers' below: ${
|
||||
mcpHub
|
||||
.getServers()
|
||||
.filter((server) => server.status === "connected")
|
||||
.map((server) => server.name)
|
||||
.join(", ") || "(None running currently)"
|
||||
}, e.g. if it would use the same API. This would be possible if you can locate the MCP server repository on the user's system by looking at the server arguments for a filepath. You might then use list_files and read_file to explore the files in the repository, and use replace_in_file to make changes to the files.
|
||||
|
||||
However some MCP servers may be running from installed packages rather than a local repository, in which case it may make more sense to create a new MCP server.
|
||||
|
||||
# MCP Servers Are Not Always Necessary
|
||||
|
||||
The user may not always request the use or creation of MCP servers. Instead, they might provide tasks that can be completed with existing tools. While using the MCP SDK to extend your capabilities can be useful, it's important to understand that this is just one specialized type of task you can accomplish. You should only implement MCP servers when the user explicitly requests it (e.g., "add a tool that...").
|
||||
|
||||
Remember: The MCP documentation and example provided above are to help you understand and work with existing MCP servers or create new ones when requested by the user. You already have access to tools and capabilities that can be used to accomplish a wide range of tasks.
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
====
|
||||
@@ -558,7 +912,13 @@ CAPABILITIES
|
||||
? "\n- You can use the browser_action tool to interact with websites (including html files and locally running development servers) through a Puppeteer-controlled browser when you feel it is necessary in accomplishing the user's task. This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. This tool may be useful at key stages of web development tasks-such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. You can analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.\n - For example, if asked to add a component to a react website, you might create the necessary files, use execute_command to run the site locally, then use browser_action to launch the browser, navigate to the local server, and verify the component renders & functions correctly before closing the browser."
|
||||
: ""
|
||||
}
|
||||
${
|
||||
mcpHub.getMode() !== "off"
|
||||
? `
|
||||
- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
====
|
||||
|
||||
@@ -579,7 +939,7 @@ RULES
|
||||
- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it.
|
||||
- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.${
|
||||
supportsComputerUse
|
||||
? `\n- The user may ask generic non-development tasks, such as "what\'s the latest news" or "look up the weather in San Diego", in which case you might use the browser_action tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question. However, if an available MCP server tool or resource can be used instead, you should prefer to use it over browser_action.`
|
||||
? `\n- The user may ask generic non-development tasks, such as "what\'s the latest news" or "look up the weather in San Diego", in which case you might use the browser_action tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question.${mcpHub.getMode() !== "off" ? "However, if an available MCP server tool or resource can be used instead, you should prefer to use it over browser_action." : ""}`
|
||||
: ""
|
||||
}
|
||||
- NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.
|
||||
@@ -594,7 +954,13 @@ RULES
|
||||
? " Then if you want to test your work, you might use browser_action to launch the site, wait for the user's response confirming the site was launched along with a screenshot, then perhaps e.g., click a button to test functionality if needed, wait for the user's response confirming the button was clicked along with a screenshot of the new state, before finally closing the browser."
|
||||
: ""
|
||||
}
|
||||
${
|
||||
mcpHub.getMode() !== "off"
|
||||
? `
|
||||
- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
====
|
||||
|
||||
@@ -619,8 +985,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
|
||||
|
||||
export function addUserInstructions(
|
||||
settingsCustomInstructions?: string,
|
||||
globalClineRulesFileInstructions?: string,
|
||||
localClineRulesFileInstructions?: string,
|
||||
clineRulesFileInstructions?: string,
|
||||
clineIgnoreInstructions?: string,
|
||||
preferredLanguageInstructions?: string,
|
||||
) {
|
||||
@@ -631,11 +996,8 @@ export function addUserInstructions(
|
||||
if (settingsCustomInstructions) {
|
||||
customInstructions += settingsCustomInstructions + "\n\n"
|
||||
}
|
||||
if (globalClineRulesFileInstructions) {
|
||||
customInstructions += globalClineRulesFileInstructions + "\n\n"
|
||||
}
|
||||
if (localClineRulesFileInstructions) {
|
||||
customInstructions += localClineRulesFileInstructions + "\n\n"
|
||||
if (clineRulesFileInstructions) {
|
||||
customInstructions += clineRulesFileInstructions + "\n\n"
|
||||
}
|
||||
if (clineIgnoreInstructions) {
|
||||
customInstructions += clineIgnoreInstructions
|
||||
|
||||
@@ -4,54 +4,13 @@ import fs from "fs/promises"
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { ClineMessage } from "../../shared/ExtensionMessage"
|
||||
import { TaskMetadata } from "../context/context-tracking/ContextTrackerTypes"
|
||||
import os from "os"
|
||||
import { execa } from "execa"
|
||||
|
||||
export const GlobalFileNames = {
|
||||
apiConversationHistory: "api_conversation_history.json",
|
||||
contextHistory: "context_history.json",
|
||||
uiMessages: "ui_messages.json",
|
||||
openRouterModels: "openrouter_models.json",
|
||||
mcpSettings: "cline_mcp_settings.json",
|
||||
clineRules: ".clinerules",
|
||||
taskMetadata: "task_metadata.json",
|
||||
}
|
||||
|
||||
export async function getDocumentsPath(): Promise<string> {
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
const { stdout: docsPath } = await execa("powershell", [
|
||||
"-NoProfile", // Ignore user's PowerShell profile(s)
|
||||
"-Command",
|
||||
"[System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::MyDocuments)",
|
||||
])
|
||||
const trimmedPath = docsPath.trim()
|
||||
if (trimmedPath) {
|
||||
return trimmedPath
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to retrieve Windows Documents path. Falling back to homedir/Documents.")
|
||||
}
|
||||
} else if (process.platform === "linux") {
|
||||
try {
|
||||
// First check if xdg-user-dir exists
|
||||
await execa("which", ["xdg-user-dir"])
|
||||
|
||||
// If it exists, try to get XDG documents path
|
||||
const { stdout } = await execa("xdg-user-dir", ["DOCUMENTS"])
|
||||
const trimmedPath = stdout.trim()
|
||||
if (trimmedPath) {
|
||||
return trimmedPath
|
||||
}
|
||||
} catch {
|
||||
// Log error but continue to fallback
|
||||
console.error("Failed to retrieve XDG Documents path. Falling back to homedir/Documents.")
|
||||
}
|
||||
}
|
||||
|
||||
// Default fallback for all platforms
|
||||
return path.join(os.homedir(), "Documents")
|
||||
}
|
||||
|
||||
export async function ensureTaskDirectoryExists(context: vscode.ExtensionContext, taskId: string): Promise<string> {
|
||||
@@ -61,34 +20,6 @@ export async function ensureTaskDirectoryExists(context: vscode.ExtensionContext
|
||||
return taskDir
|
||||
}
|
||||
|
||||
export async function ensureRulesDirectoryExists(): Promise<string> {
|
||||
const userDocumentsPath = await getDocumentsPath()
|
||||
const clineRulesDir = path.join(userDocumentsPath, "Cline", "Rules")
|
||||
try {
|
||||
await fs.mkdir(clineRulesDir, { recursive: true })
|
||||
} catch (error) {
|
||||
return path.join(os.homedir(), "Documents", "Cline", "Rules") // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine because we will fail gracefully with a path that does not exist
|
||||
}
|
||||
return clineRulesDir
|
||||
}
|
||||
|
||||
export async function ensureMcpServersDirectoryExists(): Promise<string> {
|
||||
const userDocumentsPath = await getDocumentsPath()
|
||||
const mcpServersDir = path.join(userDocumentsPath, "Cline", "MCP")
|
||||
try {
|
||||
await fs.mkdir(mcpServersDir, { recursive: true })
|
||||
} catch (error) {
|
||||
return "~/Documents/Cline/MCP" // in case creating a directory in documents fails for whatever reason (e.g. permissions) - this is fine since this path is only ever used in the system prompt
|
||||
}
|
||||
return mcpServersDir
|
||||
}
|
||||
|
||||
export async function ensureSettingsDirectoryExists(context: vscode.ExtensionContext): Promise<string> {
|
||||
const settingsDir = path.join(context.globalStorageUri.fsPath, "settings")
|
||||
await fs.mkdir(settingsDir, { recursive: true })
|
||||
return settingsDir
|
||||
}
|
||||
|
||||
export async function getSavedApiConversationHistory(
|
||||
context: vscode.ExtensionContext,
|
||||
taskId: string,
|
||||
@@ -140,25 +71,3 @@ export async function saveClineMessages(context: vscode.ExtensionContext, taskId
|
||||
console.error("Failed to save ui messages:", error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTaskMetadata(context: vscode.ExtensionContext, taskId: string): Promise<TaskMetadata> {
|
||||
const filePath = path.join(await ensureTaskDirectoryExists(context, taskId), GlobalFileNames.taskMetadata)
|
||||
try {
|
||||
if (await fileExistsAtPath(filePath)) {
|
||||
return JSON.parse(await fs.readFile(filePath, "utf8"))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to read task metadata:", error)
|
||||
}
|
||||
return { files_in_context: [], model_usage: [] }
|
||||
}
|
||||
|
||||
export async function saveTaskMetadata(context: vscode.ExtensionContext, taskId: string, metadata: TaskMetadata) {
|
||||
try {
|
||||
const taskDir = await ensureTaskDirectoryExists(context, taskId)
|
||||
const filePath = path.join(taskDir, GlobalFileNames.taskMetadata)
|
||||
await fs.writeFile(filePath, JSON.stringify(metadata, null, 2))
|
||||
} catch (error) {
|
||||
console.error("Failed to save task metadata:", error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ export type SecretKey =
|
||||
| "requestyApiKey"
|
||||
| "togetherApiKey"
|
||||
| "qwenApiKey"
|
||||
| "doubaoApiKey"
|
||||
| "mistralApiKey"
|
||||
| "liteLlmApiKey"
|
||||
| "authNonce"
|
||||
@@ -54,20 +53,15 @@ export type GlobalStateKey =
|
||||
| "previousModeApiProvider"
|
||||
| "previousModeModelId"
|
||||
| "previousModeThinkingBudgetTokens"
|
||||
| "previousModeReasoningEffort"
|
||||
| "previousModeVsCodeLmModelSelector"
|
||||
| "previousModeModelInfo"
|
||||
| "liteLlmBaseUrl"
|
||||
| "liteLlmModelId"
|
||||
| "liteLlmUsePromptCache"
|
||||
| "qwenApiLine"
|
||||
| "requestyModelId"
|
||||
| "requestyModelInfo"
|
||||
| "togetherModelId"
|
||||
| "mcpMarketplaceCatalog"
|
||||
| "telemetrySetting"
|
||||
| "asksageApiUrl"
|
||||
| "thinkingBudgetTokens"
|
||||
| "reasoningEffort"
|
||||
| "planActSeparateModelsSetting"
|
||||
| "favoritedModelIds"
|
||||
|
||||
@@ -83,11 +83,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
requestyModelId,
|
||||
requestyModelInfo,
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
@@ -102,14 +100,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
vsCodeLmModelSelector,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmUsePromptCache,
|
||||
userInfo,
|
||||
previousModeApiProvider,
|
||||
previousModeModelId,
|
||||
previousModeModelInfo,
|
||||
previousModeVsCodeLmModelSelector,
|
||||
previousModeThinkingBudgetTokens,
|
||||
previousModeReasoningEffort,
|
||||
qwenApiLine,
|
||||
liteLlmApiKey,
|
||||
telemetrySetting,
|
||||
@@ -117,10 +113,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
thinkingBudgetTokens,
|
||||
reasoningEffort,
|
||||
sambanovaApiKey,
|
||||
planActSeparateModelsSettingRaw,
|
||||
favoritedModelIds,
|
||||
] = await Promise.all([
|
||||
getGlobalState(context, "apiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "apiModelId") as Promise<string | undefined>,
|
||||
@@ -153,11 +147,9 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getSecret(context, "deepSeekApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "requestyApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "requestyModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "requestyModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getSecret(context, "togetherApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "togetherModelId") as Promise<string | undefined>,
|
||||
getSecret(context, "qwenApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "doubaoApiKey") as Promise<string | undefined>,
|
||||
getSecret(context, "mistralApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "azureApiVersion") as Promise<string | undefined>,
|
||||
getGlobalState(context, "openRouterModelId") as Promise<string | undefined>,
|
||||
@@ -172,14 +164,12 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||
getGlobalState(context, "liteLlmBaseUrl") as Promise<string | undefined>,
|
||||
getGlobalState(context, "liteLlmModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "liteLlmUsePromptCache") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "userInfo") as Promise<UserInfo | undefined>,
|
||||
getGlobalState(context, "previousModeApiProvider") as Promise<ApiProvider | undefined>,
|
||||
getGlobalState(context, "previousModeModelId") as Promise<string | undefined>,
|
||||
getGlobalState(context, "previousModeModelInfo") as Promise<ModelInfo | undefined>,
|
||||
getGlobalState(context, "previousModeVsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
|
||||
getGlobalState(context, "previousModeThinkingBudgetTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "previousModeReasoningEffort") as Promise<string | undefined>,
|
||||
getGlobalState(context, "qwenApiLine") as Promise<string | undefined>,
|
||||
getSecret(context, "liteLlmApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "telemetrySetting") as Promise<TelemetrySetting | undefined>,
|
||||
@@ -187,10 +177,8 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
getGlobalState(context, "asksageApiUrl") as Promise<string | undefined>,
|
||||
getSecret(context, "xaiApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "thinkingBudgetTokens") as Promise<number | undefined>,
|
||||
getGlobalState(context, "reasoningEffort") as Promise<string | undefined>,
|
||||
getSecret(context, "sambanovaApiKey") as Promise<string | undefined>,
|
||||
getGlobalState(context, "planActSeparateModelsSetting") as Promise<boolean | undefined>,
|
||||
getGlobalState(context, "favoritedModelIds") as Promise<string[] | undefined>,
|
||||
])
|
||||
|
||||
let apiProvider: ApiProvider
|
||||
@@ -262,12 +250,10 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
requestyModelId,
|
||||
requestyModelInfo,
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
qwenApiLine,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
@@ -276,22 +262,19 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
vsCodeLmModelSelector,
|
||||
o3MiniReasoningEffort,
|
||||
thinkingBudgetTokens,
|
||||
reasoningEffort,
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
sambanovaApiKey,
|
||||
favoritedModelIds,
|
||||
},
|
||||
lastShownAnnouncementId,
|
||||
customInstructions,
|
||||
taskHistory,
|
||||
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
|
||||
browserSettings: { ...DEFAULT_BROWSER_SETTINGS, ...browserSettings }, // this will ensure that older versions of browserSettings (e.g. before remoteBrowserEnabled was added) are merged with the default values (false for remoteBrowserEnabled)
|
||||
browserSettings: browserSettings || DEFAULT_BROWSER_SETTINGS,
|
||||
chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS,
|
||||
userInfo,
|
||||
previousModeApiProvider,
|
||||
@@ -299,7 +282,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext) {
|
||||
previousModeModelInfo,
|
||||
previousModeVsCodeLmModelSelector,
|
||||
previousModeThinkingBudgetTokens,
|
||||
previousModeReasoningEffort,
|
||||
mcpMarketplaceEnabled,
|
||||
telemetrySetting: telemetrySetting || "unset",
|
||||
planActSeparateModelsSetting,
|
||||
@@ -338,11 +320,9 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
deepSeekApiKey,
|
||||
requestyApiKey,
|
||||
requestyModelId,
|
||||
requestyModelInfo,
|
||||
togetherApiKey,
|
||||
togetherModelId,
|
||||
qwenApiKey,
|
||||
doubaoApiKey,
|
||||
mistralApiKey,
|
||||
azureApiVersion,
|
||||
openRouterModelId,
|
||||
@@ -352,16 +332,13 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
liteLlmBaseUrl,
|
||||
liteLlmModelId,
|
||||
liteLlmApiKey,
|
||||
liteLlmUsePromptCache,
|
||||
qwenApiLine,
|
||||
asksageApiKey,
|
||||
asksageApiUrl,
|
||||
xaiApiKey,
|
||||
thinkingBudgetTokens,
|
||||
reasoningEffort,
|
||||
clineApiKey,
|
||||
sambanovaApiKey,
|
||||
favoritedModelIds,
|
||||
} = apiConfiguration
|
||||
await updateGlobalState(context, "apiProvider", apiProvider)
|
||||
await updateGlobalState(context, "apiModelId", apiModelId)
|
||||
@@ -394,7 +371,6 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
await storeSecret(context, "requestyApiKey", requestyApiKey)
|
||||
await storeSecret(context, "togetherApiKey", togetherApiKey)
|
||||
await storeSecret(context, "qwenApiKey", qwenApiKey)
|
||||
await storeSecret(context, "doubaoApiKey", doubaoApiKey)
|
||||
await storeSecret(context, "mistralApiKey", mistralApiKey)
|
||||
await storeSecret(context, "liteLlmApiKey", liteLlmApiKey)
|
||||
await storeSecret(context, "xaiApiKey", xaiApiKey)
|
||||
@@ -405,18 +381,14 @@ export async function updateApiConfiguration(context: vscode.ExtensionContext, a
|
||||
await updateGlobalState(context, "vsCodeLmModelSelector", vsCodeLmModelSelector)
|
||||
await updateGlobalState(context, "liteLlmBaseUrl", liteLlmBaseUrl)
|
||||
await updateGlobalState(context, "liteLlmModelId", liteLlmModelId)
|
||||
await updateGlobalState(context, "liteLlmUsePromptCache", liteLlmUsePromptCache)
|
||||
await updateGlobalState(context, "qwenApiLine", qwenApiLine)
|
||||
await updateGlobalState(context, "requestyModelId", requestyModelId)
|
||||
await updateGlobalState(context, "requestyModelInfo", requestyModelInfo)
|
||||
await updateGlobalState(context, "togetherModelId", togetherModelId)
|
||||
await storeSecret(context, "asksageApiKey", asksageApiKey)
|
||||
await updateGlobalState(context, "asksageApiUrl", asksageApiUrl)
|
||||
await updateGlobalState(context, "thinkingBudgetTokens", thinkingBudgetTokens)
|
||||
await updateGlobalState(context, "reasoningEffort", reasoningEffort)
|
||||
await storeSecret(context, "clineApiKey", clineApiKey)
|
||||
await storeSecret(context, "sambanovaApiKey", sambanovaApiKey)
|
||||
await updateGlobalState(context, "favoritedModelIds", favoritedModelIds)
|
||||
}
|
||||
|
||||
export async function resetExtensionState(context: vscode.ExtensionContext) {
|
||||
@@ -436,7 +408,6 @@ export async function resetExtensionState(context: vscode.ExtensionContext) {
|
||||
"requestyApiKey",
|
||||
"togetherApiKey",
|
||||
"qwenApiKey",
|
||||
"doubaoApiKey",
|
||||
"mistralApiKey",
|
||||
"clineApiKey",
|
||||
"liteLlmApiKey",
|
||||
|
||||
+145
-372
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
|
||||
private readonly outputChannel: vscode.OutputChannel,
|
||||
) {
|
||||
WebviewProvider.activeInstances.add(this)
|
||||
this.controller = new Controller(context, outputChannel, (message) => this.view?.webview.postMessage(message))
|
||||
this.controller = new Controller(context, outputChannel, this)
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
@@ -44,18 +44,6 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
|
||||
return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
|
||||
}
|
||||
|
||||
public static getAllInstances(): WebviewProvider[] {
|
||||
return Array.from(this.activeInstances)
|
||||
}
|
||||
|
||||
public static getSidebarInstance() {
|
||||
return Array.from(this.activeInstances).find((instance) => instance.view && "onDidChangeVisibility" in instance.view)
|
||||
}
|
||||
|
||||
public static getTabInstances(): WebviewProvider[] {
|
||||
return Array.from(this.activeInstances).filter((instance) => instance.view && "onDidChangeViewState" in instance.view)
|
||||
}
|
||||
|
||||
async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
|
||||
this.view = webviewView
|
||||
|
||||
|
||||
+62
-88
@@ -9,8 +9,6 @@ import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
|
||||
import assert from "node:assert"
|
||||
import { telemetryService } from "./services/telemetry/TelemetryService"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import { createTestServer, shutdownTestServer } from "./services/test/TestServer"
|
||||
import { ErrorService } from "./services/error/ErrorService"
|
||||
|
||||
/*
|
||||
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
|
||||
@@ -29,14 +27,12 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
outputChannel = vscode.window.createOutputChannel("Cline")
|
||||
context.subscriptions.push(outputChannel)
|
||||
|
||||
ErrorService.initialize()
|
||||
Logger.initialize(outputChannel)
|
||||
Logger.log("Cline extension activated")
|
||||
|
||||
const sidebarWebview = new WebviewProvider(context, outputChannel)
|
||||
|
||||
vscode.commands.executeCommand("setContext", "cline.isDevMode", IS_DEV && IS_DEV === "true")
|
||||
vscode.commands.executeCommand("setContext", "cline.isTestMode", IS_TEST && IS_TEST === "true")
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(WebviewProvider.sideBarId, sidebarWebview, {
|
||||
@@ -45,37 +41,35 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.plusButtonClicked", async (webview: any) => {
|
||||
const openChat = async (instance?: WebviewProvider) => {
|
||||
await instance?.controller.clearTask()
|
||||
await instance?.controller.postStateToWebview()
|
||||
await instance?.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}
|
||||
const isSidebar = !webview
|
||||
if (isSidebar) {
|
||||
openChat(WebviewProvider.getSidebarInstance())
|
||||
} else {
|
||||
WebviewProvider.getTabInstances().forEach(openChat)
|
||||
vscode.commands.registerCommand("cline.plusButtonClicked", async () => {
|
||||
Logger.log("Plus button Clicked")
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
Logger.log("Cannot find any visible Cline instances.")
|
||||
return
|
||||
}
|
||||
|
||||
await visibleWebview.controller.clearTask()
|
||||
await visibleWebview.controller.postStateToWebview()
|
||||
await visibleWebview.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.mcpButtonClicked", (webview: any) => {
|
||||
const openMcp = (instance?: WebviewProvider) =>
|
||||
instance?.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "mcpButtonClicked",
|
||||
})
|
||||
const isSidebar = !webview
|
||||
if (isSidebar) {
|
||||
openMcp(WebviewProvider.getSidebarInstance())
|
||||
} else {
|
||||
WebviewProvider.getTabInstances().forEach(openMcp)
|
||||
vscode.commands.registerCommand("cline.mcpButtonClicked", () => {
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
Logger.log("Cannot find any visible Cline instances.")
|
||||
return
|
||||
}
|
||||
|
||||
visibleWebview.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "mcpButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -116,58 +110,47 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
context.subscriptions.push(vscode.commands.registerCommand("cline.openInNewTab", openClineInNewTab))
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.settingsButtonClicked", (webview: any) => {
|
||||
WebviewProvider.getAllInstances().forEach((instance) => {
|
||||
const openSettings = async (instance?: WebviewProvider) => {
|
||||
instance?.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "settingsButtonClicked",
|
||||
})
|
||||
}
|
||||
const isSidebar = !webview
|
||||
if (isSidebar) {
|
||||
openSettings(WebviewProvider.getSidebarInstance())
|
||||
} else {
|
||||
WebviewProvider.getTabInstances().forEach(openSettings)
|
||||
}
|
||||
vscode.commands.registerCommand("cline.settingsButtonClicked", () => {
|
||||
//vscode.window.showInformationMessage(message)
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
Logger.log("Cannot find any visible Cline instances.")
|
||||
return
|
||||
}
|
||||
|
||||
visibleWebview.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "settingsButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.historyButtonClicked", (webview: any) => {
|
||||
WebviewProvider.getAllInstances().forEach((instance) => {
|
||||
const openHistory = async (instance?: WebviewProvider) => {
|
||||
instance?.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "historyButtonClicked",
|
||||
})
|
||||
}
|
||||
const isSidebar = !webview
|
||||
if (isSidebar) {
|
||||
openHistory(WebviewProvider.getSidebarInstance())
|
||||
} else {
|
||||
WebviewProvider.getTabInstances().forEach(openHistory)
|
||||
}
|
||||
vscode.commands.registerCommand("cline.historyButtonClicked", () => {
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
Logger.log("Cannot find any visible Cline instances.")
|
||||
return
|
||||
}
|
||||
|
||||
visibleWebview.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "historyButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.accountButtonClicked", (webview: any) => {
|
||||
WebviewProvider.getAllInstances().forEach((instance) => {
|
||||
const openAccount = async (instance?: WebviewProvider) => {
|
||||
instance?.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "accountButtonClicked",
|
||||
})
|
||||
}
|
||||
const isSidebar = !webview
|
||||
if (isSidebar) {
|
||||
openAccount(WebviewProvider.getSidebarInstance())
|
||||
} else {
|
||||
WebviewProvider.getTabInstances().forEach(openAccount)
|
||||
}
|
||||
vscode.commands.registerCommand("cline.accountButtonClicked", () => {
|
||||
const visibleWebview = WebviewProvider.getVisibleInstance()
|
||||
if (!visibleWebview) {
|
||||
Logger.log("Cannot find any visible Cline instances.")
|
||||
return
|
||||
}
|
||||
|
||||
visibleWebview.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "accountButtonClicked",
|
||||
})
|
||||
}),
|
||||
)
|
||||
@@ -381,7 +364,7 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// Register the command handler
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.fixWithCline", async (range: vscode.Range, diagnostics: vscode.Diagnostic[]) => {
|
||||
vscode.commands.registerCommand("cline.fixWithCline", async (range: vscode.Range, diagnostics: any[]) => {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) {
|
||||
return
|
||||
@@ -397,32 +380,23 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
}),
|
||||
)
|
||||
|
||||
// Set up test server if in test mode
|
||||
if (IS_TEST === "true") {
|
||||
createTestServer(sidebarWebview)
|
||||
}
|
||||
|
||||
return createClineAPI(outputChannel, sidebarWebview.controller)
|
||||
}
|
||||
|
||||
// This method is called when your extension is deactivated
|
||||
export function deactivate() {
|
||||
telemetryService.shutdown()
|
||||
Logger.log("Cline extension deactivated")
|
||||
}
|
||||
|
||||
// TODO: Find a solution for automatically removing DEV related content from production builds.
|
||||
// This type of code is fine in production to keep. We just will want to remove it from production builds
|
||||
// to bring down built asset sizes.
|
||||
//
|
||||
// This is a workaround to reload the extension when the source code changes
|
||||
// since vscode doesn't support hot reload for extensions
|
||||
const { IS_DEV, DEV_WORKSPACE_FOLDER, IS_TEST } = process.env
|
||||
const { IS_DEV, DEV_WORKSPACE_FOLDER } = process.env
|
||||
|
||||
// This method is called when your extension is deactivated
|
||||
export function deactivate() {
|
||||
// Shutdown the test server if it exists
|
||||
shutdownTestServer()
|
||||
|
||||
telemetryService.shutdown()
|
||||
Logger.log("Cline extension deactivated")
|
||||
}
|
||||
|
||||
// Set up development mode file watcher
|
||||
if (IS_DEV && IS_DEV !== "false") {
|
||||
assert(DEV_WORKSPACE_FOLDER, "DEV_WORKSPACE_FOLDER must be set in development")
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(DEV_WORKSPACE_FOLDER, "src/**/*"))
|
||||
|
||||
@@ -90,11 +90,7 @@ export class GitOperations {
|
||||
const lfsPatterns = await getLfsPatterns(cwd)
|
||||
await writeExcludesFile(gitPath, lfsPatterns)
|
||||
|
||||
const addFilesResult = await this.addCheckpointFiles(git)
|
||||
if (!addFilesResult.success) {
|
||||
console.error("Failed to add at least one file(s) to checkpoints shadow git")
|
||||
throw new Error("Failed to add at least one file(s) to checkpoints shadow git")
|
||||
}
|
||||
await this.addCheckpointFiles(git)
|
||||
|
||||
// Initial commit only on first repo creation
|
||||
await git.commit("initial commit", { "--allow-empty": null })
|
||||
@@ -146,7 +142,6 @@ export class GitOperations {
|
||||
ignore: [".git"], // Ignore root level .git
|
||||
dot: true,
|
||||
markDirectories: false,
|
||||
suppressErrors: true,
|
||||
})
|
||||
|
||||
// For each nested .git directory, rename it based on operation
|
||||
@@ -195,18 +190,18 @@ export class GitOperations {
|
||||
await this.renameNestedGitRepos(true)
|
||||
console.info("Starting checkpoint add operation...")
|
||||
|
||||
// Attempt to add all files. Any files with permissions errors will not be added,
|
||||
// but the process will proceed and add the rest (--ignore-errors).
|
||||
try {
|
||||
await git.add([".", "--ignore-errors"])
|
||||
await git.add(".")
|
||||
const durationMs = Math.round(performance.now() - startTime)
|
||||
console.debug(`Checkpoint add operation completed in ${durationMs}ms`)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return { success: false }
|
||||
console.error("Checkpoint add operation failed:", error)
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false }
|
||||
console.error("Failed to add files to checkpoint", error)
|
||||
throw error
|
||||
} finally {
|
||||
await this.renameNestedGitRepos(false)
|
||||
}
|
||||
|
||||
@@ -165,10 +165,7 @@ class CheckpointTracker {
|
||||
|
||||
console.info(`Using shadow git at: ${gitPath}`)
|
||||
|
||||
const addFilesResult = await this.gitOperations.addCheckpointFiles(git)
|
||||
if (!addFilesResult.success) {
|
||||
console.error("Failed to add at least one file(s) to checkpoints shadow git")
|
||||
}
|
||||
await this.gitOperations.addCheckpointFiles(git)
|
||||
|
||||
const commitMessage = "checkpoint-" + this.cwdHash + "-" + this.taskId
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdir, access, constants } from "fs/promises"
|
||||
import { mkdir } from "fs/promises"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import os from "os"
|
||||
@@ -31,9 +31,7 @@ export async function getShadowGitPath(globalStoragePath: string, taskId: string
|
||||
/**
|
||||
* Gets the current working directory from the VS Code workspace.
|
||||
* Validates that checkpoints are not being used in protected directories
|
||||
* like home, Desktop, Documents, or Downloads. Checks to confirm that the workspace
|
||||
* is accessible and that we will not encounter breaking permissions issues when
|
||||
* creating checkpoints.
|
||||
* like home, Desktop, Documents, or Downloads.
|
||||
*
|
||||
* Protected directories:
|
||||
* - User's home directory
|
||||
@@ -42,23 +40,13 @@ export async function getShadowGitPath(globalStoragePath: string, taskId: string
|
||||
* - Downloads
|
||||
*
|
||||
* @returns Promise<string> The absolute path to the current working directory
|
||||
* @throws Error if no workspace is detected, if in a protected directory, or if no read access
|
||||
* @throws Error if no workspace is detected or if in a protected directory
|
||||
*/
|
||||
export async function getWorkingDirectory(): Promise<string> {
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
if (!cwd) {
|
||||
throw new Error("No workspace detected. Please open Cline in a workspace to use checkpoints.")
|
||||
}
|
||||
|
||||
// Check if directory exists and we have read permissions
|
||||
try {
|
||||
await access(cwd, constants.R_OK)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Cannot access workspace directory. Please ensure VS Code has permission to access your workspace. Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
const homedir = os.homedir()
|
||||
const desktopPath = path.join(homedir, "Desktop")
|
||||
const documentsPath = path.join(homedir, "Documents")
|
||||
|
||||
@@ -79,15 +79,6 @@ export class DiffViewProvider {
|
||||
if (!this.relPath || !this.activeLineController || !this.fadedOverlayController) {
|
||||
throw new Error("Required values not set")
|
||||
}
|
||||
|
||||
// --- Fix to prevent duplicate BOM ---
|
||||
// Strip potential BOM from incoming content. VS Code's `applyEdit` might implicitly handle the BOM
|
||||
// when replacing from the start (0,0), and we want to avoid duplication.
|
||||
// Final BOM is handled in `saveChanges`.
|
||||
if (accumulatedContent.startsWith("\ufeff")) {
|
||||
accumulatedContent = accumulatedContent.slice(1) // Remove the BOM character
|
||||
}
|
||||
|
||||
this.newContent = accumulatedContent
|
||||
const accumulatedLines = accumulatedContent.split("\n")
|
||||
if (!isFinal) {
|
||||
@@ -105,11 +96,8 @@ export class DiffViewProvider {
|
||||
const beginningOfDocument = new vscode.Position(0, 0)
|
||||
diffEditor.selection = new vscode.Selection(beginningOfDocument, beginningOfDocument)
|
||||
|
||||
// Instead of animating each line, we'll update in larger chunks
|
||||
const currentLine = this.streamedLines.length + diffLines.length - 1
|
||||
if (currentLine >= 0) {
|
||||
// Only proceed if we have new lines
|
||||
|
||||
for (let i = 0; i < diffLines.length; i++) {
|
||||
const currentLine = this.streamedLines.length + i
|
||||
// Replace all content up to the current line with accumulated lines
|
||||
// This is necessary (as compared to inserting one line at a time) to handle cases where html tags on previous lines are auto closed for example
|
||||
const edit = new vscode.WorkspaceEdit()
|
||||
@@ -117,33 +105,12 @@ export class DiffViewProvider {
|
||||
const contentToReplace = accumulatedLines.slice(0, currentLine + 1).join("\n") + "\n"
|
||||
edit.replace(document.uri, rangeToReplace, contentToReplace)
|
||||
await vscode.workspace.applyEdit(edit)
|
||||
|
||||
// Update decorations for the entire changed section
|
||||
// Update decorations
|
||||
this.activeLineController.setActiveLine(currentLine)
|
||||
this.fadedOverlayController.updateOverlayAfterLine(currentLine, document.lineCount)
|
||||
|
||||
// Scroll to the last changed line
|
||||
if (diffLines.length <= 5) {
|
||||
// For small changes, just jump directly to the line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
} else {
|
||||
// For larger changes, create a quick scrolling animation
|
||||
const startLine = this.streamedLines.length
|
||||
const endLine = currentLine
|
||||
const totalLines = endLine - startLine
|
||||
const numSteps = 10 // Adjust this number to control animation speed
|
||||
const stepSize = Math.max(1, Math.floor(totalLines / numSteps))
|
||||
|
||||
// Create and await the smooth scrolling animation
|
||||
for (let line = startLine; line <= endLine; line += stepSize) {
|
||||
this.activeDiffEditor?.revealRange(new vscode.Range(line, 0, line, 0), vscode.TextEditorRevealType.InCenter)
|
||||
await new Promise((resolve) => setTimeout(resolve, 16)) // ~60fps
|
||||
}
|
||||
// Ensure we end at the final line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
}
|
||||
// Scroll to the current line
|
||||
this.scrollEditorToLine(currentLine)
|
||||
}
|
||||
|
||||
// Update the streamedLines with the new accumulated content
|
||||
this.streamedLines = accumulatedLines
|
||||
if (isFinal) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function openImage(dataUri: string) {
|
||||
const imageBuffer = Buffer.from(base64Data, "base64")
|
||||
const tempFilePath = path.join(os.tmpdir(), `temp_image_${Date.now()}.${format}`)
|
||||
try {
|
||||
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), new Uint8Array(imageBuffer))
|
||||
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), imageBuffer)
|
||||
await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath))
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Error opening image: ${error}`)
|
||||
|
||||
@@ -2,17 +2,17 @@ import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import { listFiles } from "../../services/glob/list-files"
|
||||
import { Controller } from "../../core/controller"
|
||||
import { ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
|
||||
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
|
||||
|
||||
// Note: this is not a drop-in replacement for listFiles at the start of tasks, since that will be done for Desktops when there is no workspace selected
|
||||
class WorkspaceTracker {
|
||||
private controllerRef: WeakRef<Controller>
|
||||
private disposables: vscode.Disposable[] = []
|
||||
private filePaths: Set<string> = new Set()
|
||||
|
||||
constructor(private readonly postMessageToWebview: (message: ExtensionMessage) => Promise<void>) {
|
||||
this.postMessageToWebview = postMessageToWebview
|
||||
constructor(controller: Controller) {
|
||||
this.controllerRef = new WeakRef(controller)
|
||||
this.registerListeners()
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class WorkspaceTracker {
|
||||
if (!cwd) {
|
||||
return
|
||||
}
|
||||
this.postMessageToWebview({
|
||||
this.controllerRef.deref()?.postMessageToWebview({
|
||||
type: "workspaceUpdated",
|
||||
filePaths: Array.from(this.filePaths).map((file) => {
|
||||
const relativePath = path.relative(cwd, file).toPosix()
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
|
||||
import { Controller } from "../../core/controller"
|
||||
import type { BalanceResponse, PaymentTransaction, UsageTransaction } from "../../shared/ClineAccount"
|
||||
import { ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
|
||||
export class ClineAccountService {
|
||||
private readonly baseUrl = "https://api.cline.bot/v1"
|
||||
private postMessageToWebview: (message: ExtensionMessage) => Promise<void>
|
||||
private getClineApiKey: () => Promise<string | undefined>
|
||||
private controllerRef: WeakRef<Controller>
|
||||
|
||||
constructor(
|
||||
postMessageToWebview: (message: ExtensionMessage) => Promise<void>,
|
||||
getClineApiKey: () => Promise<string | undefined>,
|
||||
) {
|
||||
this.postMessageToWebview = postMessageToWebview
|
||||
this.getClineApiKey = getClineApiKey
|
||||
constructor(controller: Controller) {
|
||||
this.controllerRef = new WeakRef(controller)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's Cline Account key from the apiConfiguration
|
||||
*/
|
||||
private async getClineApiKey(): Promise<string | undefined> {
|
||||
const provider = this.controllerRef.deref()
|
||||
if (!provider) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { apiConfiguration } = await provider.getStateToPostToWebview()
|
||||
return apiConfiguration?.clineApiKey
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +64,7 @@ export class ClineAccountService {
|
||||
const data = await this.authenticatedRequest<BalanceResponse>("/user/credits/balance")
|
||||
|
||||
// Post to webview
|
||||
await this.postMessageToWebview({
|
||||
await this.controllerRef.deref()?.postMessageToWebview({
|
||||
type: "userCreditsBalance",
|
||||
userCreditsBalance: data,
|
||||
})
|
||||
@@ -76,7 +84,7 @@ export class ClineAccountService {
|
||||
const data = await this.authenticatedRequest<UsageTransaction[]>("/user/credits/usage")
|
||||
|
||||
// Post to webview
|
||||
await this.postMessageToWebview({
|
||||
await this.controllerRef.deref()?.postMessageToWebview({
|
||||
type: "userCreditsUsage",
|
||||
userCreditsUsage: data,
|
||||
})
|
||||
@@ -96,7 +104,7 @@ export class ClineAccountService {
|
||||
const data = await this.authenticatedRequest<PaymentTransaction[]>("/user/credits/payments")
|
||||
|
||||
// Post to webview
|
||||
await this.postMessageToWebview({
|
||||
await this.controllerRef.deref()?.postMessageToWebview({
|
||||
type: "userCreditsPayments",
|
||||
userCreditsPayments: data,
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user